From 3d7d28d402fcbb5763cf399960d71a123ed4f90a Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Mon, 10 Feb 2020 10:44:59 -0500 Subject: [PATCH] check: Verify sidebar navigation for missing links and mismatched link text (if legacy directory structure) (#29) Reference: https://github.com/bflad/tfproviderdocs/issues/27 Reference: https://github.com/bflad/tfproviderdocs/issues/28 --- CHANGELOG.md | 6 + check/check.go | 23 +++- check/check_test.go | 8 ++ check/file_extension.go | 1 + check/sidenavigation/category.go | 72 +++++++++++ check/sidenavigation/html.go | 61 ++++++++++ check/sidenavigation/link.go | 106 ++++++++++++++++ check/sidenavigation/sidenavigation.go | 161 +++++++++++++++++++++++++ check/sidenavigation_link.go | 50 ++++++++ check/sidenavigation_mismatch.go | 69 +++++++++++ check/sidenavigation_options.go | 30 +++++ command/check.go | 22 +++- go.mod | 1 + go.sum | 1 + 14 files changed, 606 insertions(+), 5 deletions(-) create mode 100644 check/sidenavigation/category.go create mode 100644 check/sidenavigation/html.go create mode 100644 check/sidenavigation/link.go create mode 100644 check/sidenavigation/sidenavigation.go create mode 100644 check/sidenavigation_link.go create mode 100644 check/sidenavigation_mismatch.go create mode 100644 check/sidenavigation_options.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f75902..9837e04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# v0.5.0 + +ENHANCEMENTS + +* check: Verify sidebar navigation for missing links and mismatched link text (if legacy directory structure) + # v0.4.1 BUG FIXES diff --git a/check/check.go b/check/check.go index 9ccaf32..fabca36 100644 --- a/check/check.go +++ b/check/check.go @@ -36,6 +36,8 @@ type CheckOptions struct { SchemaDataSources map[string]*tfjson.Schema SchemaResources map[string]*tfjson.Schema + + SideNavigation *SideNavigationOptions } func NewCheck(opts *CheckOptions) *Check { @@ -117,8 +119,11 @@ func (check *Check) Run(directories map[string][]string) error { } } - if files, ok := directories[LegacyDataSourcesDirectory]; ok { - if err := NewLegacyDataSourceFileCheck(check.Options.LegacyDataSourceFile).RunAll(files); err != nil { + legacyDataSourcesFiles, legacyDataSourcesOk := directories[LegacyDataSourcesDirectory] + legacyResourcesFiles, legacyResourcesOk := directories[LegacyResourcesDirectory] + + if legacyDataSourcesOk { + if err := NewLegacyDataSourceFileCheck(check.Options.LegacyDataSourceFile).RunAll(legacyDataSourcesFiles); err != nil { result = multierror.Append(result, err) } } @@ -135,8 +140,18 @@ func (check *Check) Run(directories map[string][]string) error { } } - if files, ok := directories[LegacyResourcesDirectory]; ok { - if err := NewLegacyResourceFileCheck(check.Options.LegacyResourceFile).RunAll(files); err != nil { + if legacyResourcesOk { + if err := NewLegacyResourceFileCheck(check.Options.LegacyResourceFile).RunAll(legacyResourcesFiles); err != nil { + result = multierror.Append(result, err) + } + } + + if legacyDataSourcesOk || legacyResourcesOk { + if err := SideNavigationLinkCheck(check.Options.SideNavigation); err != nil { + result = multierror.Append(result, err) + } + + if err := SideNavigationMismatchCheck(check.Options.SideNavigation, legacyDataSourcesFiles, legacyResourcesFiles); err != nil { result = multierror.Append(result, err) } } diff --git a/check/check_test.go b/check/check_test.go index aa20b33..452dff1 100644 --- a/check/check_test.go +++ b/check/check_test.go @@ -105,6 +105,14 @@ func TestCheck(t *testing.T) { testCase.Options.RegistryResourceFile.FileOptions = fileOpts } + if testCase.Options.SideNavigation == nil { + testCase.Options.SideNavigation = &SideNavigationOptions{} + } + + if testCase.Options.SideNavigation.FileOptions == nil { + testCase.Options.SideNavigation.FileOptions = fileOpts + } + directories, err := GetDirectories(testCase.BasePath) if err != nil { diff --git a/check/file_extension.go b/check/file_extension.go index e1ac831..01ee145 100644 --- a/check/file_extension.go +++ b/check/file_extension.go @@ -7,6 +7,7 @@ import ( ) const ( + FileExtensionErb = `.erb` FileExtensionHtmlMarkdown = `.html.markdown` FileExtensionHtmlMd = `.html.md` FileExtensionMarkdown = `.markdown` diff --git a/check/sidenavigation/category.go b/check/sidenavigation/category.go new file mode 100644 index 0000000..be7c67b --- /dev/null +++ b/check/sidenavigation/category.go @@ -0,0 +1,72 @@ +package sidenavigation + +import ( + "golang.org/x/net/html" +) + +type Category struct { + Name string + Node *html.Node + + DataSourceLinks []*Link + ExternalLinks []*Link + GuideLinks []*Link + ResourceLinks []*Link +} + +func NewCategory(name string, listNode *html.Node) *Category { + category := &Category{ + DataSourceLinks: make([]*Link, 0), + ExternalLinks: make([]*Link, 0), + GuideLinks: make([]*Link, 0), + Name: name, + Node: listNode, + ResourceLinks: make([]*Link, 0), + } + + category.processList() + + return category +} + +func (category *Category) processList() { + for child := category.Node.FirstChild; child != nil; child = child.NextSibling { + if isListItem(child) { + category.processListItem(child) + } + } +} + +func (category *Category) processListItem(node *html.Node) { + linkNode := childLinkNode(node) + + if linkNode == nil { + return + } + + link := NewLink(linkNode) + listNode := childUnorderedListNode(node) + + if listNode == nil { + switch link.Type { + case LinkTypeDataSource: + category.DataSourceLinks = append(category.DataSourceLinks, link) + case LinkTypeExternal: + category.ExternalLinks = append(category.ExternalLinks, link) + case LinkTypeGuide: + category.GuideLinks = append(category.GuideLinks, link) + case LinkTypeResource: + category.ResourceLinks = append(category.ResourceLinks, link) + } + + return + } + + // Categories can contain one single subcategory (e.g. Service Name > Resources) + subCategory := NewCategory(link.Text, listNode) + + category.DataSourceLinks = append(category.DataSourceLinks, subCategory.DataSourceLinks...) + category.ExternalLinks = append(category.ExternalLinks, subCategory.ExternalLinks...) + category.GuideLinks = append(category.GuideLinks, subCategory.GuideLinks...) + category.ResourceLinks = append(category.ResourceLinks, subCategory.ResourceLinks...) +} diff --git a/check/sidenavigation/html.go b/check/sidenavigation/html.go new file mode 100644 index 0000000..3b5ea89 --- /dev/null +++ b/check/sidenavigation/html.go @@ -0,0 +1,61 @@ +package sidenavigation + +import ( + "golang.org/x/net/html" +) + +func childLinkNode(node *html.Node) *html.Node { + for child := node.FirstChild; child != nil; child = child.NextSibling { + if isLink(child) { + return child + } + } + + return nil +} + +func childText(node *html.Node) string { + if node.Type == html.TextNode { + return node.Data + } + + for child := node.FirstChild; child != nil; child = child.NextSibling { + return childText(child) + } + + return "" +} + +func childUnorderedListNode(node *html.Node) *html.Node { + for child := node.FirstChild; child != nil; child = child.NextSibling { + if isUnorderedList(child) { + return child + } + } + + return nil +} + +func isLink(node *html.Node) bool { + if node.Type != html.ElementNode { + return false + } + + return node.Data == "a" +} + +func isListItem(node *html.Node) bool { + if node.Type != html.ElementNode { + return false + } + + return node.Data == "li" +} + +func isUnorderedList(node *html.Node) bool { + if node.Type != html.ElementNode { + return false + } + + return node.Data == "ul" +} diff --git a/check/sidenavigation/link.go b/check/sidenavigation/link.go new file mode 100644 index 0000000..a373f00 --- /dev/null +++ b/check/sidenavigation/link.go @@ -0,0 +1,106 @@ +package sidenavigation + +import ( + "fmt" + "strings" + + "golang.org/x/net/html" +) + +type Link struct { + Text string + Type LinkType + Url string +} + +type LinkType int + +const ( + LinkTypeAnchor LinkType = iota + LinkTypeExternal + LinkTypeDataSource + LinkTypeGuide + LinkTypeResource +) + +func (typ LinkType) String() string { + return [...]string{"Anchor", "External", "Data Source", "Guide", "Resource"}[typ] +} + +func NewLink(node *html.Node) *Link { + link := &Link{ + Text: childText(node), + } + + for _, attr := range node.Attr { + if attr.Key == "href" { + link.Url = attr.Val + + if strings.HasPrefix(link.Url, "#") { + link.Type = LinkTypeAnchor + } else if strings.Contains(link.Url, "/d/") { + link.Type = LinkTypeDataSource + } else if strings.Contains(link.Url, "/guides/") { + link.Type = LinkTypeGuide + } else if strings.Contains(link.Url, "/r/") { + link.Type = LinkTypeResource + } else { + link.Type = LinkTypeExternal + } + + break + } + } + + return link +} + +func (link *Link) Validate(expectedProviderName string) error { + if link.Type != LinkTypeDataSource && link.Type != LinkTypeResource { + return nil + } + + var linkTypeUrlPart string + + switch link.Type { + case LinkTypeDataSource: + linkTypeUrlPart = "d" + case LinkTypeResource: + linkTypeUrlPart = "r" + } + + if !strings.Contains(link.Url, "/") { + return fmt.Errorf("link URL (%s) is missing / separators, should be in form: /docs/providers/PROVIDER/%s/NAME.html", link.Url, linkTypeUrlPart) + } + + urlParts := strings.Split(link.Url, "/") + + if len(urlParts) < 6 { + return fmt.Errorf("link URL (%s) is missing path parts, e.g. /docs/providers/PROVIDER/%s/NAME.html", link.Url, linkTypeUrlPart) + } else if len(urlParts) > 6 { + return fmt.Errorf("link URL (%s) has too many path parts, should be in form: /docs/providers/PROVIDER/%s/NAME.html", link.Url, linkTypeUrlPart) + } + + urlProviderName := urlParts[3] + + if expectedProviderName != "" && urlProviderName != expectedProviderName { + return fmt.Errorf("link URL (%s) has incorrect provider name (%s), expected: %s", link.Url, urlProviderName, expectedProviderName) + } + + urlResourceName := urlParts[len(urlParts)-1] + urlResourceName = strings.TrimSuffix(urlResourceName, ".html") + + if expectedProviderName != "" { + expectedText := fmt.Sprintf("%s_%s", expectedProviderName, urlResourceName) + if link.Text != expectedText { + return fmt.Errorf("link URL (%s) has incorrect text (%s), expected: %s", link.Url, link.Text, expectedText) + } + } else { + expectedSuffix := fmt.Sprintf("_%s", urlResourceName) + if !strings.HasSuffix(link.Text, expectedSuffix) { + return fmt.Errorf("link URL (%s) has incorrect text (%s), expected: PROVIDER%s", link.Url, link.Text, expectedSuffix) + } + } + + return nil +} diff --git a/check/sidenavigation/sidenavigation.go b/check/sidenavigation/sidenavigation.go new file mode 100644 index 0000000..7cf2741 --- /dev/null +++ b/check/sidenavigation/sidenavigation.go @@ -0,0 +1,161 @@ +package sidenavigation + +import ( + "fmt" + "io/ioutil" + "log" + "regexp" + "strings" + + "golang.org/x/net/html" +) + +const erbRegexpPattern = `<%.*%>` + +type SideNavigation struct { + Categories []*Category + Node *html.Node + + ExternalLinks []*Link + + // These include all sub-categories + DataSourceLinks []*Link + GuideLinks []*Link + ResourceLinks []*Link +} + +func Find(node *html.Node) *SideNavigation { + if node == nil { + return nil + } + + if isSideNavigationList(node) { + return New(node) + } + + for child := node.FirstChild; child != nil; child = child.NextSibling { + sideNavigation := Find(child) + + if sideNavigation != nil { + return sideNavigation + } + } + + return nil +} + +func FindFile(path string) (*SideNavigation, error) { + fileContents, err := ioutil.ReadFile(path) + + if err != nil { + log.Fatalf("error opening file (%s): %w", path, err) + } + + return FindString(string(fileContents)) +} + +func FindString(fileContents string) (*SideNavigation, error) { + strippedFileContents := regexp.MustCompile(erbRegexpPattern).ReplaceAllString(string(fileContents), "") + + doc, err := html.Parse(strings.NewReader(strippedFileContents)) + + if err != nil { + return nil, fmt.Errorf("error HTML parsing file: %w", err) + } + + return Find(doc), nil +} + +func New(node *html.Node) *SideNavigation { + sn := &SideNavigation{ + Categories: make([]*Category, 0), + DataSourceLinks: make([]*Link, 0), + ExternalLinks: make([]*Link, 0), + GuideLinks: make([]*Link, 0), + Node: node, + ResourceLinks: make([]*Link, 0), + } + + sn.processList() + + return sn +} + +func (sn *SideNavigation) HasDataSourceLink(name string) bool { + for _, link := range sn.DataSourceLinks { + if link.Text == name { + return true + } + } + + return false +} + +func (sn *SideNavigation) HasResourceLink(name string) bool { + for _, link := range sn.ResourceLinks { + if link.Text == name { + return true + } + } + + return false +} + +func (sn *SideNavigation) processList() { + for child := sn.Node.FirstChild; child != nil; child = child.NextSibling { + if isListItem(child) { + sn.processListItem(child) + } + } +} + +func (sn *SideNavigation) processListItem(node *html.Node) { + linkNode := childLinkNode(node) + + if linkNode == nil { + return + } + + link := NewLink(linkNode) + listNode := childUnorderedListNode(node) + + if listNode == nil { + switch link.Type { + case LinkTypeDataSource: + sn.DataSourceLinks = append(sn.DataSourceLinks, link) + case LinkTypeExternal: + sn.ExternalLinks = append(sn.ExternalLinks, link) + case LinkTypeGuide: + sn.GuideLinks = append(sn.GuideLinks, link) + case LinkTypeResource: + sn.ResourceLinks = append(sn.ResourceLinks, link) + } + + return + } + + category := NewCategory(link.Text, listNode) + + sn.DataSourceLinks = append(sn.DataSourceLinks, category.DataSourceLinks...) + sn.ExternalLinks = append(sn.ExternalLinks, category.ExternalLinks...) + sn.GuideLinks = append(sn.GuideLinks, category.GuideLinks...) + sn.ResourceLinks = append(sn.ResourceLinks, category.ResourceLinks...) +} + +func isSideNavigationList(node *html.Node) bool { + if node.Type != html.ElementNode { + return false + } + + if node.Data != "ul" { + return false + } + + for _, attr := range node.Attr { + if attr.Key == "class" && strings.Contains(attr.Val, "docs-sidenav") { + return true + } + } + + return false +} diff --git a/check/sidenavigation_link.go b/check/sidenavigation_link.go new file mode 100644 index 0000000..99e61a7 --- /dev/null +++ b/check/sidenavigation_link.go @@ -0,0 +1,50 @@ +package check + +import ( + "fmt" + + "github.com/bflad/tfproviderdocs/check/sidenavigation" + "github.com/hashicorp/go-multierror" +) + +func SideNavigationLinkCheck(opts *SideNavigationOptions) error { + if opts == nil || opts.ProviderName == "" { + return nil + } + + path := fmt.Sprintf("website/%s%s", opts.ProviderName, FileExtensionErb) + + sideNavigation, err := sidenavigation.FindFile(opts.FullPath(path)) + + if err != nil { + return fmt.Errorf("%s: error finding side navigation: %s", path, err) + } + + if sideNavigation == nil { + return fmt.Errorf("%s: error finding side navigation: not found in file", path) + } + + var errors *multierror.Error + + for _, link := range sideNavigation.DataSourceLinks { + if opts.ShouldIgnoreDataSource(link.Text) { + continue + } + + if err := link.Validate(opts.ProviderName); err != nil { + errors = multierror.Append(errors, fmt.Errorf("%s: error validating link: %s", path, err)) + } + } + + for _, link := range sideNavigation.ResourceLinks { + if opts.ShouldIgnoreResource(link.Text) { + continue + } + + if err := link.Validate(opts.ProviderName); err != nil { + errors = multierror.Append(errors, fmt.Errorf("%s: error validating link: %s", path, err)) + } + } + + return errors.ErrorOrNil() +} diff --git a/check/sidenavigation_mismatch.go b/check/sidenavigation_mismatch.go new file mode 100644 index 0000000..be030ed --- /dev/null +++ b/check/sidenavigation_mismatch.go @@ -0,0 +1,69 @@ +package check + +import ( + "fmt" + + "github.com/bflad/tfproviderdocs/check/sidenavigation" + "github.com/hashicorp/go-multierror" +) + +func SideNavigationMismatchCheck(opts *SideNavigationOptions, dataSourceFiles []string, resourceFiles []string) error { + if opts == nil || opts.ProviderName == "" { + return nil + } + + path := fmt.Sprintf("website/%s%s", opts.ProviderName, FileExtensionErb) + + sideNavigation, err := sidenavigation.FindFile(opts.FullPath(path)) + + if err != nil { + return fmt.Errorf("%s: error finding side navigation: %s", path, err) + } + + if sideNavigation == nil { + return fmt.Errorf("%s: error finding side navigation: not found in file", path) + } + + var missingDataSources, missingResources []string + var result *multierror.Error + + for _, dataSourceFile := range dataSourceFiles { + dataSourceName := fileResourceName(opts.ProviderName, dataSourceFile) + + if sideNavigation.HasDataSourceLink(dataSourceName) { + continue + } + + missingDataSources = append(missingDataSources, dataSourceName) + } + + for _, resourceFile := range resourceFiles { + resourceName := fileResourceName(opts.ProviderName, resourceFile) + + if sideNavigation.HasResourceLink(resourceName) { + continue + } + + missingResources = append(missingResources, resourceName) + } + + for _, missingDataSource := range missingDataSources { + if opts.ShouldIgnoreDataSource(missingDataSource) { + continue + } + + err := fmt.Errorf("%s: missing side navigation link for data source: %s", path, missingDataSource) + result = multierror.Append(result, err) + } + + for _, missingResource := range missingResources { + if opts.ShouldIgnoreResource(missingResource) { + continue + } + + err := fmt.Errorf("%s: missing side navigation link for resource: %s", path, missingResource) + result = multierror.Append(result, err) + } + + return result.ErrorOrNil() +} diff --git a/check/sidenavigation_options.go b/check/sidenavigation_options.go new file mode 100644 index 0000000..df9140c --- /dev/null +++ b/check/sidenavigation_options.go @@ -0,0 +1,30 @@ +package check + +type SideNavigationOptions struct { + *FileOptions + + IgnoreDataSources []string + IgnoreResources []string + + ProviderName string +} + +func (opts *SideNavigationOptions) ShouldIgnoreDataSource(name string) bool { + for _, ignoreDataSource := range opts.IgnoreDataSources { + if ignoreDataSource == name { + return true + } + } + + return false +} + +func (opts *SideNavigationOptions) ShouldIgnoreResource(name string) bool { + for _, ignoreResource := range opts.IgnoreResources { + if ignoreResource == name { + return true + } + } + + return false +} diff --git a/command/check.go b/command/check.go index 09a2f49..d83d8ac 100644 --- a/command/check.go +++ b/command/check.go @@ -24,6 +24,8 @@ type CheckCommandConfig struct { AllowedGuideSubcategoriesFile string AllowedResourceSubcategories string AllowedResourceSubcategoriesFile string + IgnoreSideNavigationDataSources string + IgnoreSideNavigationResources string LogLevel string Path string ProviderName string @@ -45,6 +47,8 @@ func (*CheckCommand) Help() string { fmt.Fprintf(opts, CommandHelpOptionFormat, "-allowed-guide-subcategories-file", "Path to newline separated file of allowed guide frontmatter subcategories.") fmt.Fprintf(opts, CommandHelpOptionFormat, "-allowed-resource-subcategories", "Comma separated list of allowed data source and resource frontmatter subcategories.") fmt.Fprintf(opts, CommandHelpOptionFormat, "-allowed-resource-subcategories-file", "Path to newline separated file of allowed data source and resource frontmatter subcategories.") + fmt.Fprintf(opts, CommandHelpOptionFormat, "-ignore-side-navigation-data-sources", "Comma separated list of data sources to ignore side navigation validation.") + fmt.Fprintf(opts, CommandHelpOptionFormat, "-ignore-side-navigation-resources", "Comma separated list of resources to ignore side navigation validation.") fmt.Fprintf(opts, CommandHelpOptionFormat, "-provider-name", "Terraform Provider name. Automatically determined if current working directory or provided path is prefixed with terraform-provider-*.") fmt.Fprintf(opts, CommandHelpOptionFormat, "-providers-schema-json", "Path to terraform providers schema -json file. Enables enhanced validations.") fmt.Fprintf(opts, CommandHelpOptionFormat, "-require-guide-subcategory", "Require guide frontmatter subcategory.") @@ -76,6 +80,8 @@ func (c *CheckCommand) Run(args []string) int { flags.StringVar(&config.AllowedGuideSubcategoriesFile, "allowed-guide-subcategories-file", "", "") flags.StringVar(&config.AllowedResourceSubcategories, "allowed-resource-subcategories", "", "") flags.StringVar(&config.AllowedResourceSubcategoriesFile, "allowed-resource-subcategories-file", "", "") + flags.StringVar(&config.IgnoreSideNavigationDataSources, "ignore-side-navigation-data-sources", "", "") + flags.StringVar(&config.IgnoreSideNavigationResources, "ignore-side-navigation-resources", "", "") flags.StringVar(&config.ProviderName, "provider-name", "", "") flags.StringVar(&config.ProvidersSchemaJson, "providers-schema-json", "", "") flags.BoolVar(&config.RequireGuideSubcategory, "require-guide-subcategory", false, "") @@ -125,7 +131,7 @@ func (c *CheckCommand) Run(args []string) int { return 1 } - var allowedGuideSubcategories, allowedResourceSubcategories []string + var allowedGuideSubcategories, allowedResourceSubcategories, ignoreSideNavigationDataSources, ignoreSideNavigationResources []string if v := config.AllowedGuideSubcategories; v != "" { allowedGuideSubcategories = strings.Split(v, ",") @@ -155,6 +161,14 @@ func (c *CheckCommand) Run(args []string) int { } } + if v := config.IgnoreSideNavigationDataSources; v != "" { + ignoreSideNavigationDataSources = strings.Split(v, ",") + } + + if v := config.IgnoreSideNavigationResources; v != "" { + ignoreSideNavigationResources = strings.Split(v, ",") + } + fileOpts := &check.FileOptions{ BasePath: config.Path, } @@ -208,6 +222,12 @@ func (c *CheckCommand) Run(args []string) int { RequireSubcategory: config.RequireResourceSubcategory, }, }, + SideNavigation: &check.SideNavigationOptions{ + FileOptions: fileOpts, + IgnoreDataSources: ignoreSideNavigationDataSources, + IgnoreResources: ignoreSideNavigationResources, + ProviderName: config.ProviderName, + }, } if config.ProvidersSchemaJson != "" { diff --git a/go.mod b/go.mod index 2e77b09..11196dd 100644 --- a/go.mod +++ b/go.mod @@ -9,5 +9,6 @@ require ( github.com/hashicorp/terraform-json v0.3.1 github.com/mattn/go-colorable v0.1.4 github.com/mitchellh/cli v1.0.0 + golang.org/x/net v0.0.0-20180811021610-c39426892332 gopkg.in/yaml.v2 v2.2.7 ) diff --git a/go.sum b/go.sum index e7a41f1..915d345 100644 --- a/go.sum +++ b/go.sum @@ -42,6 +42,7 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/zclconf/go-cty v0.0.0-20190430221426-d36a6f0dbffd h1:NZOOU7h+pDtcKo6xlqm8PwnarS8nJ+6+I83jT8ZfLPI= github.com/zclconf/go-cty v0.0.0-20190430221426-d36a6f0dbffd/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= +golang.org/x/net v0.0.0-20180811021610-c39426892332 h1:efGso+ep0DjyCBJPjvoz0HI6UldX4Md2F1rZFe1ir0E= golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=