From 93bdafc067ace2b6b76ebe4ae5e152b3ac1c332a Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Tue, 17 Dec 2019 12:53:29 -0500 Subject: [PATCH] check: Verify data source and resource file mismatches Closes #13 Closes #14 If `-providers-schema-json` flag is provided, verifies all known data sources and resources have an associated documentation file. Also, verifies that no extraneous or incorrectly named documentation files exist. --- CHANGELOG.md | 2 + README.md | 2 + check/check.go | 31 +++++ check/file_extension.go | 17 +++ check/file_extension_test.go | 45 +++++++ check/file_mismatch.go | 83 +++++++++++++ check/file_mismatch_test.go | 221 +++++++++++++++++++++++++++++++++++ 7 files changed, 401 insertions(+) create mode 100644 check/file_mismatch.go create mode 100644 check/file_mismatch_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index ca3b902..b37470d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ENHANCEMENTS * check: Verify number of documentation files for Terraform Registry storage limits * check: Verify size of documentation files for Terraform Registry storage limits +* check: Verify all known data sources and resources have an associated documentation file (if `-providers-schema-json` is provided) +* check: Verify no extraneous or incorrectly named documentation files exist (if `-providers-schema-json` is provided) # v0.1.2 diff --git a/README.md b/README.md index 2d484c9..f2c3839 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,8 @@ The `tfproviderdocs check` command verifies the Terraform Provider documentation - Verifies that no invalid directories are found in the documentation directory structure. - Ensures that there is not a mix (legacy and Terraform Registry) of directory structures, which is not supported during Terraform Registry documentation ingress. - Verifies number of documentation files is below Terraform Registry storage limits. +- Verifies all known data sources and resources have an associated documentation file (if `-providers-schema-json` is provided) +- Verifies no extraneous or incorrectly named documentation files exist (if `-providers-schema-json` is provided) - Verifies each file in the documentation directories is valid. The validity of files is checked with the following rules: diff --git a/check/check.go b/check/check.go index f97000a..9ccaf32 100644 --- a/check/check.go +++ b/check/check.go @@ -8,6 +8,9 @@ import ( ) const ( + ResourceTypeDataSource = "data source" + ResourceTypeResource = "resource" + // Terraform Registry Storage Limits // https://www.terraform.io/docs/registry/providers/docs.html#storage-limits RegistryMaximumNumberOfFiles = 1000 @@ -60,6 +63,34 @@ func (check *Check) Run(directories map[string][]string) error { return err } + if len(check.Options.SchemaDataSources) > 0 && false { + var dataSourceFiles []string + + if files, ok := directories[RegistryDataSourcesDirectory]; ok { + dataSourceFiles = files + } else if files, ok := directories[LegacyDataSourcesDirectory]; ok { + dataSourceFiles = files + } + + if err := ResourceFileMismatchCheck(check.Options.ProviderName, ResourceTypeDataSource, check.Options.SchemaDataSources, dataSourceFiles); err != nil { + return err + } + } + + if len(check.Options.SchemaResources) > 0 { + var resourceFiles []string + + if files, ok := directories[RegistryResourcesDirectory]; ok { + resourceFiles = files + } else if files, ok := directories[LegacyResourcesDirectory]; ok { + resourceFiles = files + } + + if err := ResourceFileMismatchCheck(check.Options.ProviderName, ResourceTypeResource, check.Options.SchemaResources, resourceFiles); err != nil { + return err + } + } + var result *multierror.Error if files, ok := directories[RegistryDataSourcesDirectory]; ok { diff --git a/check/file_extension.go b/check/file_extension.go index 45096d4..a0e8a47 100644 --- a/check/file_extension.go +++ b/check/file_extension.go @@ -81,3 +81,20 @@ func IsValidRegistryFileExtension(fileExtension string) bool { return false } + +// TrimFileExtension removes file extensions including those with multiple periods. +func TrimFileExtension(path string) string { + filename := filepath.Base(path) + + if filename == "." { + return "" + } + + dotIndex := strings.IndexByte(filename, '.') + + if dotIndex > 0 { + return filename[:dotIndex] + } + + return filename +} diff --git a/check/file_extension_test.go b/check/file_extension_test.go index ba20358..f9f0453 100644 --- a/check/file_extension_test.go +++ b/check/file_extension_test.go @@ -48,3 +48,48 @@ func TestGetFileExtension(t *testing.T) { }) } } + +func TestTrimFileExtension(t *testing.T) { + testCases := []struct { + Name string + Path string + Expect string + }{ + { + Name: "empty path", + Path: "", + Expect: "", + }, + { + Name: "filename with single extension", + Path: "file.md", + Expect: "file", + }, + { + Name: "filename with multiple extensions", + Path: "file.html.markdown", + Expect: "file", + }, + { + Name: "full path with single extensions", + Path: "docs/resource/thing.md", + Expect: "thing", + }, + { + Name: "full path with multiple extensions", + Path: "website/docs/r/thing.html.markdown", + Expect: "thing", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + got := TrimFileExtension(testCase.Path) + want := testCase.Expect + + if got != want { + t.Errorf("expected %s, got %s", want, got) + } + }) + } +} diff --git a/check/file_mismatch.go b/check/file_mismatch.go new file mode 100644 index 0000000..b5c4868 --- /dev/null +++ b/check/file_mismatch.go @@ -0,0 +1,83 @@ +package check + +import ( + "fmt" + "sort" + + "github.com/hashicorp/go-multierror" + tfjson "github.com/hashicorp/terraform-json" +) + +func ResourceFileMismatchCheck(providerName string, resourceType string, schemaResources map[string]*tfjson.Schema, files []string) error { + var extraFiles []string + var missingFiles []string + + for _, file := range files { + if fileHasResource(schemaResources, providerName, file) { + continue + } + + extraFiles = append(extraFiles, file) + } + + for _, resourceName := range resourceNames(schemaResources) { + if resourceHasFile(files, providerName, resourceName) { + continue + } + + missingFiles = append(missingFiles, resourceName) + } + + var result *multierror.Error + + for _, extraFile := range extraFiles { + err := fmt.Errorf("matching %s for documentation file (%s) not found, file is extraneous or incorrectly named", resourceType, extraFile) + result = multierror.Append(result, err) + } + + for _, missingFile := range missingFiles { + err := fmt.Errorf("missing documentation file for %s: %s", resourceType, missingFile) + result = multierror.Append(result, err) + } + + return result.ErrorOrNil() +} + +func fileHasResource(schemaResources map[string]*tfjson.Schema, providerName, file string) bool { + if _, ok := schemaResources[fileResourceName(providerName, file)]; ok { + return true + } + + return false +} + +func fileResourceName(providerName, fileName string) string { + resourceSuffix := TrimFileExtension(fileName) + + return fmt.Sprintf("%s_%s", providerName, resourceSuffix) +} + +func resourceHasFile(files []string, providerName, resourceName string) bool { + var found bool + + for _, file := range files { + if fileResourceName(providerName, file) == resourceName { + found = true + break + } + } + + return found +} + +func resourceNames(resources map[string]*tfjson.Schema) []string { + names := make([]string, 0, len(resources)) + + for name := range resources { + names = append(names, name) + } + + sort.Strings(names) + + return names +} diff --git a/check/file_mismatch_test.go b/check/file_mismatch_test.go new file mode 100644 index 0000000..d50758f --- /dev/null +++ b/check/file_mismatch_test.go @@ -0,0 +1,221 @@ +package check + +import ( + "reflect" + "testing" + + tfjson "github.com/hashicorp/terraform-json" +) + +func TestFileHasResource(t *testing.T) { + testCases := []struct { + Name string + File string + Resources map[string]*tfjson.Schema + Expect bool + }{ + { + Name: "found", + File: "resource1.md", + Resources: map[string]*tfjson.Schema{ + "test_resource1": &tfjson.Schema{}, + "test_resource2": &tfjson.Schema{}, + }, + Expect: true, + }, + { + Name: "not found", + File: "resource1.md", + Resources: map[string]*tfjson.Schema{ + "test_resource2": &tfjson.Schema{}, + "test_resource3": &tfjson.Schema{}, + }, + Expect: false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + got := fileHasResource(testCase.Resources, "test", testCase.File) + want := testCase.Expect + + if got != want { + t.Errorf("expected %t, got %t", want, got) + } + }) + } +} + +func TestFileResourceName(t *testing.T) { + testCases := []struct { + Name string + File string + Expect string + }{ + { + Name: "filename with single extension", + File: "file.md", + Expect: "test_file", + }, + { + Name: "filename with multiple extensions", + File: "file.html.markdown", + Expect: "test_file", + }, + { + Name: "full path with single extensions", + File: "docs/resource/thing.md", + Expect: "test_thing", + }, + { + Name: "full path with multiple extensions", + File: "website/docs/r/thing.html.markdown", + Expect: "test_thing", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + got := fileResourceName("test", testCase.File) + want := testCase.Expect + + if got != want { + t.Errorf("expected %s, got %s", want, got) + } + }) + } +} + +func TestResourceFileMismatchCheck(t *testing.T) { + testCases := []struct { + Name string + Files []string + Resources map[string]*tfjson.Schema + ExpectError bool + }{ + { + Name: "all found", + Files: []string{ + "resource1.md", + "resource2.md", + }, + Resources: map[string]*tfjson.Schema{ + "test_resource1": &tfjson.Schema{}, + "test_resource2": &tfjson.Schema{}, + }, + }, + { + Name: "extra", + Files: []string{ + "resource1.md", + "resource2.md", + "resource3.md", + }, + Resources: map[string]*tfjson.Schema{ + "test_resource1": &tfjson.Schema{}, + "test_resource2": &tfjson.Schema{}, + }, + ExpectError: true, + }, + { + Name: "missing", + Files: []string{ + "resource1.md", + }, + Resources: map[string]*tfjson.Schema{ + "test_resource1": &tfjson.Schema{}, + "test_resource2": &tfjson.Schema{}, + }, + ExpectError: true, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + got := ResourceFileMismatchCheck("test", "resource", testCase.Resources, testCase.Files) + + if got == nil && testCase.ExpectError { + t.Errorf("expected error, got no error") + } + + if got != nil && !testCase.ExpectError { + t.Errorf("expected no error, got error: %s", got) + } + }) + } +} + +func TestResourceHasFile(t *testing.T) { + testCases := []struct { + Name string + Files []string + ResourceName string + Expect bool + }{ + { + Name: "found", + Files: []string{ + "resource1.md", + "resource2.md", + }, + ResourceName: "test_resource1", + Expect: true, + }, + { + Name: "not found", + Files: []string{ + "resource1.md", + "resource2.md", + }, + ResourceName: "test_resource3", + Expect: false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + got := resourceHasFile(testCase.Files, "test", testCase.ResourceName) + want := testCase.Expect + + if got != want { + t.Errorf("expected %t, got %t", want, got) + } + }) + } +} + +func TestResourceNames(t *testing.T) { + testCases := []struct { + Name string + Resources map[string]*tfjson.Schema + Expect []string + }{ + { + Name: "empty", + Resources: map[string]*tfjson.Schema{}, + Expect: []string{}, + }, + { + Name: "multiple", + Resources: map[string]*tfjson.Schema{ + "test_resource1": &tfjson.Schema{}, + "test_resource2": &tfjson.Schema{}, + }, + Expect: []string{ + "test_resource1", + "test_resource2", + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + got := resourceNames(testCase.Resources) + want := testCase.Expect + + if !reflect.DeepEqual(got, want) { + t.Errorf("expected %v, got %v", want, got) + } + }) + } +}