diff --git a/check/check.go b/check/check.go index cfd7838..f97000a 100644 --- a/check/check.go +++ b/check/check.go @@ -4,6 +4,7 @@ import ( "sort" "github.com/hashicorp/go-multierror" + tfjson "github.com/hashicorp/terraform-json" ) const ( @@ -23,10 +24,15 @@ type CheckOptions struct { LegacyIndexFile *LegacyIndexFileOptions LegacyResourceFile *LegacyResourceFileOptions + ProviderName string + RegistryDataSourceFile *RegistryDataSourceFileOptions RegistryGuideFile *RegistryGuideFileOptions RegistryIndexFile *RegistryIndexFileOptions RegistryResourceFile *RegistryResourceFileOptions + + SchemaDataSources map[string]*tfjson.Schema + SchemaResources map[string]*tfjson.Schema } func NewCheck(opts *CheckOptions) *Check { diff --git a/command/check.go b/command/check.go index 8ed75c4..973a46b 100644 --- a/command/check.go +++ b/command/check.go @@ -2,12 +2,19 @@ package command import ( "bytes" + "encoding/json" "flag" "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "sort" "strings" "text/tabwriter" "github.com/bflad/tfproviderdocs/check" + tfjson "github.com/hashicorp/terraform-json" "github.com/mitchellh/cli" ) @@ -16,6 +23,8 @@ type CheckCommandConfig struct { AllowedResourceSubcategories string LogLevel string Path string + ProviderName string + ProvidersSchemaJson string RequireGuideSubcategory bool RequireResourceSubcategory bool } @@ -31,6 +40,8 @@ func (*CheckCommand) Help() string { LogLevelFlagHelp(opts) fmt.Fprintf(opts, CommandHelpOptionFormat, "-allowed-guide-subcategories", "Comma separated list 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, "-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.") fmt.Fprintf(opts, CommandHelpOptionFormat, "-require-resource-subcategory", "Require data source and resource frontmatter subcategory.") opts.Flush() @@ -58,6 +69,8 @@ func (c *CheckCommand) Run(args []string) int { LogLevelFlag(flags, &config.LogLevel) flags.StringVar(&config.AllowedGuideSubcategories, "allowed-guide-subcategories", "", "") flags.StringVar(&config.AllowedResourceSubcategories, "allowed-resource-subcategories", "", "") + flags.StringVar(&config.ProviderName, "provider-name", "", "") + flags.StringVar(&config.ProvidersSchemaJson, "providers-schema-json", "", "") flags.BoolVar(&config.RequireGuideSubcategory, "require-guide-subcategory", false, "") flags.BoolVar(&config.RequireResourceSubcategory, "require-resource-subcategory", false, "") @@ -74,6 +87,20 @@ func (c *CheckCommand) Run(args []string) int { ConfigureLogging(c.Name(), config.LogLevel) + if config.ProviderName == "" { + if config.Path == "" { + config.ProviderName = providerNameFromCurrentDirectory() + } else { + config.ProviderName = providerNameFromPath(config.Path) + } + + if config.ProviderName == "" { + log.Printf("[WARN] Unable to determine provider name. Enhanced validations may fail.") + } else { + log.Printf("[DEBUG] Found provider name: %s", config.ProviderName) + } + } + directories, err := check.GetDirectories(config.Path) if err != nil { @@ -129,6 +156,7 @@ func (c *CheckCommand) Run(args []string) int { RequireSubcategory: config.RequireResourceSubcategory, }, }, + ProviderName: config.ProviderName, RegistryDataSourceFile: &check.RegistryDataSourceFileOptions{ FileOptions: fileOpts, FrontMatter: &check.FrontMatterOptions{ @@ -155,6 +183,26 @@ func (c *CheckCommand) Run(args []string) int { }, } + if config.ProvidersSchemaJson != "" { + ps, err := providerSchemas(config.ProvidersSchemaJson) + + if err != nil { + c.Ui.Error(fmt.Sprintf("Error enabling Terraform Provider schema checks: %s", err)) + return 1 + } + + if config.ProviderName == "" { + msg := `Unknown provider name for enabling Terraform Provider schema checks. + +Check that the current working directory or provided path is prefixed with terraform-provider-*.` + c.Ui.Error(msg) + return 1 + } + + checkOpts.SchemaDataSources = providerSchemasDataSources(ps, config.ProviderName) + checkOpts.SchemaResources = providerSchemasResources(ps, config.ProviderName) + } + if err := check.NewCheck(checkOpts).Run(directories); err != nil { c.Ui.Error(fmt.Sprintf("Error checking Terraform Provider documentation: %s", err)) return 1 @@ -166,3 +214,98 @@ func (c *CheckCommand) Run(args []string) int { func (c *CheckCommand) Synopsis() string { return "Checks Terraform Provider documentation" } + +func providerNameFromCurrentDirectory() string { + path, _ := os.Getwd() + + return providerNameFromPath(path) +} + +func providerNameFromPath(path string) string { + base := filepath.Base(path) + + if strings.ContainsAny(base, "./") { + return "" + } + + if !strings.HasPrefix(base, "terraform-provider-") { + return "" + } + + return strings.TrimPrefix(base, "terraform-provider-") +} + +// providerSchemas reads, parses, and validates a provided terraform provider schema -json path. +func providerSchemas(path string) (*tfjson.ProviderSchemas, error) { + log.Printf("[DEBUG] Loading providers schema JSON file: %s", path) + + content, err := ioutil.ReadFile(path) + + if err != nil { + return nil, fmt.Errorf("error reading providers schema JSON file (%s): %w", path, err) + } + + var ps tfjson.ProviderSchemas + + if err := json.Unmarshal(content, &ps); err != nil { + return nil, fmt.Errorf("error parsing providers schema JSON file (%s): %w", path, err) + } + + if err := ps.Validate(); err != nil { + return nil, fmt.Errorf("error validating providers schema JSON file (%s): %w", path, err) + } + + return &ps, nil +} + +// providerSchemasDataSources returns all data sources from a terraform providers schema -json provider. +func providerSchemasDataSources(ps *tfjson.ProviderSchemas, providerName string) map[string]*tfjson.Schema { + if ps == nil || providerName == "" { + return nil + } + + provider, ok := ps.Schemas[providerName] + + if !ok { + log.Printf("[WARN] Provider name (%s) not found in provider schema", providerName) + return nil + } + + dataSources := make([]string, 0, len(provider.DataSourceSchemas)) + + for name := range provider.DataSourceSchemas { + dataSources = append(dataSources, name) + } + + sort.Strings(dataSources) + + log.Printf("[DEBUG] Found provider schema data sources: %v", dataSources) + + return provider.DataSourceSchemas +} + +// providerSchemasResources returns all resources from a terraform providers schema -json provider. +func providerSchemasResources(ps *tfjson.ProviderSchemas, providerName string) map[string]*tfjson.Schema { + if ps == nil || providerName == "" { + return nil + } + + provider, ok := ps.Schemas[providerName] + + if !ok { + log.Printf("[WARN] Provider name (%s) not found in provider schema", providerName) + return nil + } + + resources := make([]string, 0, len(provider.ResourceSchemas)) + + for name := range provider.ResourceSchemas { + resources = append(resources, name) + } + + sort.Strings(resources) + + log.Printf("[DEBUG] Found provider schema data sources: %v", resources) + + return provider.ResourceSchemas +} diff --git a/command/check_test.go b/command/check_test.go new file mode 100644 index 0000000..d8e0198 --- /dev/null +++ b/command/check_test.go @@ -0,0 +1,208 @@ +package command + +import ( + "reflect" + "testing" + + tfjson "github.com/hashicorp/terraform-json" +) + +func TestProviderNameFromPath(t *testing.T) { + testCases := []struct { + Name string + Path string + Expect string + }{ + { + Name: "full path without prefix", + Path: "/path/to/test", + Expect: "", + }, + { + Name: "full path with prefix", + Path: "/path/to/terraform-provider-test", + Expect: "test", + }, + { + Name: "relative path without prefix", + Path: "test", + Expect: "", + }, + { + Name: "relative path with prefix", + Path: "terraform-provider-test", + Expect: "test", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + want := testCase.Expect + got := providerNameFromPath(testCase.Path) + + if want != got { + t.Errorf("expected: %s, got: %s", want, got) + } + }) + } +} + +func TestProviderSchemas(t *testing.T) { + testCases := []struct { + Name string + Path string + ExpectError bool + }{ + { + Name: "valid", + Path: "testdata/valid-providers-schema.json", + }, + { + Name: "invalid path", + Path: "testdata/does-not-exist.json", + ExpectError: true, + }, + { + Name: "invalid json", + Path: "testdata/not-providers-schema.json", + ExpectError: true, + }, + { + Name: "invalid format version", + Path: "testdata/invalid-providers-schema-version.json", + ExpectError: true, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + _, err := providerSchemas(testCase.Path) + + if err == nil && testCase.ExpectError { + t.Errorf("expected error, got no error") + } + + if err != nil && !testCase.ExpectError { + t.Errorf("expected no error, got error: %s", err) + } + }) + } +} + +func TestProviderSchemasDataSources(t *testing.T) { + testCases := []struct { + Name string + ProvidersSchema *tfjson.ProviderSchemas + Expect map[string]*tfjson.Schema + }{ + { + Name: "no providers schemas", + ProvidersSchema: &tfjson.ProviderSchemas{}, + Expect: nil, + }, + { + Name: "provider not found", + ProvidersSchema: &tfjson.ProviderSchemas{ + Schemas: map[string]*tfjson.ProviderSchema{ + "incorrect": &tfjson.ProviderSchema{}, + }, + }, + Expect: nil, + }, + { + Name: "provider found", + ProvidersSchema: &tfjson.ProviderSchemas{ + Schemas: map[string]*tfjson.ProviderSchema{ + "incorrect": &tfjson.ProviderSchema{}, + "test": &tfjson.ProviderSchema{ + DataSourceSchemas: map[string]*tfjson.Schema{ + "test_data_source1": &tfjson.Schema{}, + "test_data_source2": &tfjson.Schema{}, + "test_data_source3": &tfjson.Schema{}, + }, + ResourceSchemas: map[string]*tfjson.Schema{ + "test_resource1": &tfjson.Schema{}, + "test_resource2": &tfjson.Schema{}, + "test_resource3": &tfjson.Schema{}, + }, + }, + }, + }, + Expect: map[string]*tfjson.Schema{ + "test_data_source1": &tfjson.Schema{}, + "test_data_source2": &tfjson.Schema{}, + "test_data_source3": &tfjson.Schema{}, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + want := testCase.Expect + got := providerSchemasDataSources(testCase.ProvidersSchema, "test") + + if !reflect.DeepEqual(want, got) { + t.Errorf("mismatch:\n\nwant:\n\n%v\n\ngot:\n\n%v\n\n", want, got) + } + }) + } +} + +func TestProviderSchemasResources(t *testing.T) { + testCases := []struct { + Name string + ProvidersSchema *tfjson.ProviderSchemas + Expect map[string]*tfjson.Schema + }{ + { + Name: "no providers schemas", + ProvidersSchema: &tfjson.ProviderSchemas{}, + Expect: nil, + }, + { + Name: "provider not found", + ProvidersSchema: &tfjson.ProviderSchemas{ + Schemas: map[string]*tfjson.ProviderSchema{ + "incorrect": &tfjson.ProviderSchema{}, + }, + }, + Expect: nil, + }, + { + Name: "provider found", + ProvidersSchema: &tfjson.ProviderSchemas{ + Schemas: map[string]*tfjson.ProviderSchema{ + "incorrect": &tfjson.ProviderSchema{}, + "test": &tfjson.ProviderSchema{ + DataSourceSchemas: map[string]*tfjson.Schema{ + "test_data_source1": &tfjson.Schema{}, + "test_data_source2": &tfjson.Schema{}, + "test_data_source3": &tfjson.Schema{}, + }, + ResourceSchemas: map[string]*tfjson.Schema{ + "test_resource1": &tfjson.Schema{}, + "test_resource2": &tfjson.Schema{}, + "test_resource3": &tfjson.Schema{}, + }, + }, + }, + }, + Expect: map[string]*tfjson.Schema{ + "test_resource1": &tfjson.Schema{}, + "test_resource2": &tfjson.Schema{}, + "test_resource3": &tfjson.Schema{}, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + want := testCase.Expect + got := providerSchemasResources(testCase.ProvidersSchema, "test") + + if !reflect.DeepEqual(want, got) { + t.Errorf("mismatch:\n\nwant:\n\n%v\n\ngot:\n\n%v\n\n", want, got) + } + }) + } +} diff --git a/command/testdata/invalid-providers-schema-version.json b/command/testdata/invalid-providers-schema-version.json new file mode 100644 index 0000000..bc1bf4c --- /dev/null +++ b/command/testdata/invalid-providers-schema-version.json @@ -0,0 +1,69 @@ +{ + "format_version": "999.9", + "provider_schemas": { + "null": { + "provider": { + "version": 0, + "block": {} + }, + "resource_schemas": { + "null_resource": { + "version": 0, + "block": { + "attributes": { + "id": { + "type": "string", + "optional": true, + "computed": true + }, + "triggers": { + "type": [ + "map", + "string" + ], + "optional": true + } + } + } + } + }, + "data_source_schemas": { + "null_data_source": { + "version": 0, + "block": { + "attributes": { + "has_computed_default": { + "type": "string", + "optional": true, + "computed": true + }, + "id": { + "type": "string", + "optional": true, + "computed": true + }, + "inputs": { + "type": [ + "map", + "string" + ], + "optional": true + }, + "outputs": { + "type": [ + "map", + "string" + ], + "computed": true + }, + "random": { + "type": "string", + "computed": true + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/command/testdata/not-providers-schema.json b/command/testdata/not-providers-schema.json new file mode 100644 index 0000000..736928d --- /dev/null +++ b/command/testdata/not-providers-schema.json @@ -0,0 +1,3 @@ +{ + "invalid": "true" +} \ No newline at end of file diff --git a/command/testdata/valid-providers-schema.json b/command/testdata/valid-providers-schema.json new file mode 100644 index 0000000..5b8d4f2 --- /dev/null +++ b/command/testdata/valid-providers-schema.json @@ -0,0 +1,69 @@ +{ + "format_version": "0.1", + "provider_schemas": { + "null": { + "provider": { + "version": 0, + "block": {} + }, + "resource_schemas": { + "null_resource": { + "version": 0, + "block": { + "attributes": { + "id": { + "type": "string", + "optional": true, + "computed": true + }, + "triggers": { + "type": [ + "map", + "string" + ], + "optional": true + } + } + } + } + }, + "data_source_schemas": { + "null_data_source": { + "version": 0, + "block": { + "attributes": { + "has_computed_default": { + "type": "string", + "optional": true, + "computed": true + }, + "id": { + "type": "string", + "optional": true, + "computed": true + }, + "inputs": { + "type": [ + "map", + "string" + ], + "optional": true + }, + "outputs": { + "type": [ + "map", + "string" + ], + "computed": true + }, + "random": { + "type": "string", + "computed": true + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/go.mod b/go.mod index 79b45b8..2e77b09 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/bmatcuk/doublestar v1.2.1 github.com/hashicorp/go-hclog v0.10.0 github.com/hashicorp/go-multierror v1.0.0 + github.com/hashicorp/terraform-json v0.3.1 github.com/mattn/go-colorable v0.1.4 github.com/mitchellh/cli v1.0.0 gopkg.in/yaml.v2 v2.2.7 diff --git a/go.sum b/go.sum index e284f35..e7a41f1 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 h1:BUAU3CGlLvorLI26FmByPp2eC2qla6E1Tw+scpcg/to= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= @@ -8,12 +9,21 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-hclog v0.10.0 h1:b86HUuA126IcSHyC55WjPo7KtCOVeTCKIjr+3lBhPxI= github.com/hashicorp/go-hclog v0.10.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/terraform-json v0.3.1 h1:vRiOLck4YX4UqzljVhdQKsVLixX4L+Pgnm/q+xu6QvE= +github.com/hashicorp/terraform-json v0.3.1/go.mod h1:MdwQStcJb00ht55L/2YH0ypAO9RNtczJ1MaUlf+gJcg= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= @@ -29,11 +39,21 @@ github.com/posener/complete v1.1.1 h1:ccV59UEOTzVDnDUEFdT95ZzHVZ+5+158q8+SJb2QV5 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +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/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= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191008105621-543471e840be h1:QAcqgptGM8IQBC9K/RC4o+O9YmqEm0diQn9QmZw/0mU= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=