diff --git a/.changes/unreleased/FEATURES-20240305-135426.yaml b/.changes/unreleased/FEATURES-20240305-135426.yaml new file mode 100644 index 00000000..12267b80 --- /dev/null +++ b/.changes/unreleased/FEATURES-20240305-135426.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'validate: Added support for Provider-defined Function documentation to all checks' +time: 2024-03-05T13:54:26.307742-05:00 +custom: + Issue: "341" diff --git a/.changes/unreleased/FEATURES-20240305-135726.yaml b/.changes/unreleased/FEATURES-20240305-135726.yaml new file mode 100644 index 00000000..fcd2dcab --- /dev/null +++ b/.changes/unreleased/FEATURES-20240305-135726.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'validate: Added `InvalidDirectoriesCheck` which checks for valid provider documentation + folder structure' +time: 2024-03-05T13:57:26.273538-05:00 +custom: + Issue: "341" diff --git a/.changes/unreleased/FEATURES-20240305-135933.yaml b/.changes/unreleased/FEATURES-20240305-135933.yaml new file mode 100644 index 00000000..3660390d --- /dev/null +++ b/.changes/unreleased/FEATURES-20240305-135933.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'validate: Added `MixedDirectoriesCheck` which throws an error if both legacy + documentation and registry documentation are found' +time: 2024-03-05T13:59:33.741601-05:00 +custom: + Issue: "341" diff --git a/.changes/unreleased/FEATURES-20240305-140106.yaml b/.changes/unreleased/FEATURES-20240305-140106.yaml new file mode 100644 index 00000000..95e24909 --- /dev/null +++ b/.changes/unreleased/FEATURES-20240305-140106.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'validate: Added `NumberOfFilesCheck` which checks the number of provider + documentation files against the registry limit' +time: 2024-03-05T14:01:06.742843-05:00 +custom: + Issue: "341" diff --git a/.changes/unreleased/FEATURES-20240305-140234.yaml b/.changes/unreleased/FEATURES-20240305-140234.yaml new file mode 100644 index 00000000..1fbb4aa1 --- /dev/null +++ b/.changes/unreleased/FEATURES-20240305-140234.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'validate: Added `FileSizeCheck` which checks the provider documentation file + size against the registry limit' +time: 2024-03-05T14:02:34.112782-05:00 +custom: + Issue: "341" diff --git a/.changes/unreleased/FEATURES-20240305-140346.yaml b/.changes/unreleased/FEATURES-20240305-140346.yaml new file mode 100644 index 00000000..348fab9b --- /dev/null +++ b/.changes/unreleased/FEATURES-20240305-140346.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'validate: Added `FileExtensionCheck` which checks for valid provider documentation + file extensions' +time: 2024-03-05T14:03:46.816256-05:00 +custom: + Issue: "341" diff --git a/.changes/unreleased/FEATURES-20240305-140451.yaml b/.changes/unreleased/FEATURES-20240305-140451.yaml new file mode 100644 index 00000000..0ed3883a --- /dev/null +++ b/.changes/unreleased/FEATURES-20240305-140451.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'validate: Added `FrontMatterCheck` which checks the YAML frontmatter of provider + documentation for missing required fields or invalid fields' +time: 2024-03-05T14:04:51.781688-05:00 +custom: + Issue: "341" diff --git a/.changes/unreleased/FEATURES-20240305-140622.yaml b/.changes/unreleased/FEATURES-20240305-140622.yaml new file mode 100644 index 00000000..cbb637e7 --- /dev/null +++ b/.changes/unreleased/FEATURES-20240305-140622.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'validate: Added `FileMismatchCheck` which checks the names/number of provider + documentation files against the provider schema' +time: 2024-03-05T14:06:22.168518-05:00 +custom: + Issue: "341" diff --git a/README.md b/README.md index f7f8b40b..fc2f3895 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Available commands are: the generate command is run by default generate generates a plugin website from code, templates, and examples migrate migrates website files from either the legacy rendered website directory (`website/docs/r`) or the docs rendered website directory (`docs/resources`) to the tfplugindocs supported structure (`templates/`). - validate validates a plugin website for the current directory + validate validates a plugin website ``` @@ -81,6 +81,11 @@ Usage: tfplugindocs generate [] $ tfplugindocs validate --help Usage: tfplugindocs validate [] + + --provider-dir relative or absolute path to the root provider code directory; this will default to the current working directory if not set + --provider-name provider name, as used in Terraform configurations + --providers-schema path to the providers schema JSON file, which contains the output of the terraform providers schema -json command. Setting this flag will skip building the provider and calling Terraform CLI + --tf-version terraform binary version to download. If not provided, will look for a terraform binary in the local environment. If not found in the environment, will download the latest version of Terraform ``` `migrate` command: @@ -145,6 +150,22 @@ Otherwise, the provider developer can set an arbitrary description like this: // ... ``` +#### Validate subcommand + +The `validate` subcommand can be used to validate the provider website documentation against the [Terraform Registry's provider documentation guidelines](https://developer.hashicorp.com/terraform/registry/providers/docs) and provider documentation best practices. The current checks in the `validate` command are: + +| Check | Description | +|---------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `InvalidDirectoriesCheck` | Checks for valid subdirectory structure and throws an error if an invalid Terraform Provider documentation subdirectory is found. | +| `MixedDirectoriesCheck` | Throws an error if both legacy documentation (`/website/docs`) and registry documentation (`/docs`) are found. | +| `NumberOfFilesCheck` | Throws an error if the number of files in a directory is larger than the registry limit. | +| `FileSizeCheck` | Throws an error if the documentation file is above the registry storage limit. | +| `FileExtensionCheck` | Throws an error if the extension of the given file is not a valid registry documentation extension. | +| `FrontMatterCheck` | Checks the YAML frontmatter of documentation for missing required fields or invalid fields. | +| `FileMismatchCheck` | Throws an error if the names/number of resources/datasources/functions in the provider schema does not match the names/number of files in the corresponding documentation directory | + +All check errors are wrapped and returned as a single error message to stderr. + #### Migrate subcommand The `migrate` subcommand can be used to migrate website files from either the legacy rendered website directory (`website/docs/r`) or the docs diff --git a/cmd/tfplugindocs/main_test.go b/cmd/tfplugindocs/main_test.go index 703dd4cb..f3f2b27c 100644 --- a/cmd/tfplugindocs/main_test.go +++ b/cmd/tfplugindocs/main_test.go @@ -54,3 +54,11 @@ func Test_SchemaJson_MigrateAcceptanceTests(t *testing.T) { Dir: "testdata/scripts/schema-json/migrate", }) } + +func Test_SchemaJson_ValidateAcceptanceTests(t *testing.T) { + t.Parallel() + + testscript.Run(t, testscript.Params{ + Dir: "testdata/scripts/schema-json/validate", + }) +} diff --git a/cmd/tfplugindocs/testdata/scripts/schema-json/validate/framework_provider_error_file_mismatch.txtar b/cmd/tfplugindocs/testdata/scripts/schema-json/validate/framework_provider_error_file_mismatch.txtar new file mode 100644 index 00000000..8a0d2e19 --- /dev/null +++ b/cmd/tfplugindocs/testdata/scripts/schema-json/validate/framework_provider_error_file_mismatch.txtar @@ -0,0 +1,127 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +# Run of tfplugindocs validate command with a misnamed file +[!unix] skip +! exec tfplugindocs validate --provider-name=terraform-provider-scaffolding --providers-schema=schema.json +stderr 'Error executing command: validation errors found:' +stderr 'matching resource for documentation file \(resource2.md\) not found, file is extraneous or incorrectly named' +stderr 'missing documentation file for resource: scaffolding_example' + +-- docs/data-sources/example.md -- +--- +subcategory: "Example" +page_title: "Example: example_thing" +description: |- + Example description. +--- +# Data Fields + +Name: {{.Name}} +Type: {{.Type}} +-- docs/resources/resource2.md -- +--- +subcategory: "Example" +page_title: "Example: example_thing" +description: |- + Example description. +--- +# Data Fields + +Name: {{.Name}} +Type: {{.Type}} +-- schema.json -- +{ + "format_version": "1.0", + "provider_schemas": { + "registry.terraform.io/hashicorp/scaffolding": { + "provider": { + "version": 0, + "block": { + "attributes": { + "endpoint": { + "type": "string", + "description": "Example provider attribute", + "description_kind": "markdown", + "optional": true + } + }, + "description": "Example provider", + "description_kind": "markdown" + } + }, + "resource_schemas": { + "scaffolding_example": { + "version": 0, + "block": { + "attributes": { + "configurable_attribute": { + "type": "string", + "description": "Example configurable attribute", + "description_kind": "markdown", + "optional": true + }, + "defaulted": { + "type": "string", + "description": "Example configurable attribute with default value", + "description_kind": "markdown", + "optional": true, + "computed": true + }, + "id": { + "type": "string", + "description": "Example identifier", + "description_kind": "markdown", + "computed": true + } + }, + "description": "Example resource", + "description_kind": "markdown" + } + } + }, + "data_source_schemas": { + "scaffolding_example": { + "version": 0, + "block": { + "attributes": { + "configurable_attribute": { + "type": "string", + "description": "Example configurable attribute", + "description_kind": "markdown", + "optional": true + }, + "id": { + "type": "string", + "description": "Example identifier", + "description_kind": "markdown", + "computed": true + } + }, + "description": "Example data source", + "description_kind": "markdown" + } + } + }, + "functions": { + "example": { + "description": "Given a string value, returns the same value.", + "summary": "Echo a string", + "return_type": "string", + "parameters": [ + { + "name": "input", + "description": "Value to echo.", + "type": "string" + } + ], + "variadic_parameter": { + "name": "variadicInput", + "description": "Variadic input to echo.", + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/cmd/tfplugindocs/testdata/scripts/schema-json/validate/framework_provider_error_mixed_directories.txtar b/cmd/tfplugindocs/testdata/scripts/schema-json/validate/framework_provider_error_mixed_directories.txtar new file mode 100644 index 00000000..77f67ab6 --- /dev/null +++ b/cmd/tfplugindocs/testdata/scripts/schema-json/validate/framework_provider_error_mixed_directories.txtar @@ -0,0 +1,127 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +# Run of tfplugindocs validate command with mixed directory structure +[!unix] skip +! exec tfplugindocs validate --provider-name=terraform-provider-scaffolding --providers-schema=schema.json +stderr 'Error executing command: validation errors found:' +stderr 'mixed Terraform Provider documentation directory layouts found, must use only legacy or registry layout' + +-- website/docs/d/example.html.md -- +--- +subcategory: "Example" +layout: "example" +page_title: "Example: example_thing" +description: |- + Example description. +--- +# Data Fields + +Name: {{.Name}} +Type: {{.Type}} +-- docs/data-sources/example.md -- +--- +subcategory: "Example" +page_title: "Example: example_thing" +description: |- + Example description. +--- +# Data Fields + +Name: {{.Name}} +Type: {{.Type}} +-- schema.json -- +{ + "format_version": "1.0", + "provider_schemas": { + "registry.terraform.io/hashicorp/scaffolding": { + "provider": { + "version": 0, + "block": { + "attributes": { + "endpoint": { + "type": "string", + "description": "Example provider attribute", + "description_kind": "markdown", + "optional": true + } + }, + "description": "Example provider", + "description_kind": "markdown" + } + }, + "resource_schemas": { + "scaffolding_example": { + "version": 0, + "block": { + "attributes": { + "configurable_attribute": { + "type": "string", + "description": "Example configurable attribute", + "description_kind": "markdown", + "optional": true + }, + "defaulted": { + "type": "string", + "description": "Example configurable attribute with default value", + "description_kind": "markdown", + "optional": true, + "computed": true + }, + "id": { + "type": "string", + "description": "Example identifier", + "description_kind": "markdown", + "computed": true + } + }, + "description": "Example resource", + "description_kind": "markdown" + } + } + }, + "data_source_schemas": { + "scaffolding_example": { + "version": 0, + "block": { + "attributes": { + "configurable_attribute": { + "type": "string", + "description": "Example configurable attribute", + "description_kind": "markdown", + "optional": true + }, + "id": { + "type": "string", + "description": "Example identifier", + "description_kind": "markdown", + "computed": true + } + }, + "description": "Example data source", + "description_kind": "markdown" + } + } + }, + "functions": { + "example": { + "description": "Given a string value, returns the same value.", + "summary": "Echo a string", + "return_type": "string", + "parameters": [ + { + "name": "input", + "description": "Value to echo.", + "type": "string" + } + ], + "variadic_parameter": { + "name": "variadicInput", + "description": "Variadic input to echo.", + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/cmd/tfplugindocs/testdata/scripts/schema-json/validate/framework_provider_success_legacy_docs.txtar b/cmd/tfplugindocs/testdata/scripts/schema-json/validate/framework_provider_success_legacy_docs.txtar new file mode 100644 index 00000000..c8452040 --- /dev/null +++ b/cmd/tfplugindocs/testdata/scripts/schema-json/validate/framework_provider_success_legacy_docs.txtar @@ -0,0 +1,179 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +# Successful run of tfplugindocs validate command on a Framework provider with docs in the legacy directory structure (i.e. r/.md.tmpl) +[!unix] skip +exec tfplugindocs validate --provider-name=terraform-provider-scaffolding --providers-schema=schema.json +cmp stdout expected-output.txt + +-- expected-output.txt -- +exporting schema from JSON file +getting provider schema +running mixed directories check +running number of files check +detected legacy website directory, running checks +running invalid directories check on website/docs/d +running file checks on website/docs/d/example.html.md +running invalid directories check on website/docs/functions +running file checks on website/docs/functions/example.html.md +running invalid directories check on website/docs/guides +running file checks on website/docs/guides/example.html.md +running file checks on website/docs/index.html.md +running invalid directories check on website/docs/r +running file checks on website/docs/r/example.html.md +running file mismatch check +-- website/docs/guides/example.html.md -- +--- +subcategory: "Example" +layout: "example" +page_title: "Example Guide" +description: |- + Example description. +--- + +# Example Guide + +Example contents. + +-- website/docs/r/example.html.md -- +--- +subcategory: "Example" +layout: "example" +page_title: "Example: example_thing" +description: |- + Example description. +--- +# Data Fields + +Name: {{.Name}} +Type: {{.Type}} +-- website/docs/d/example.html.md -- +--- +subcategory: "Example" +layout: "example" +page_title: "Example: example_thing" +description: |- + Example description. +--- +# Data Fields + +Name: {{.Name}} +Type: {{.Type}} +-- website/docs/functions/example.html.md -- +--- +subcategory: "Example" +layout: "example" +page_title: "Example: example_thing" +description: |- + Example description. +--- +# Data Fields + +Name: {{.Name}} +Type: {{.Type}} +-- website/docs/index.html.md -- +--- +layout: "example" +page_title: "Example Provider" +description: |- + Example description. +--- +# Data Fields + +Name: {{.Name}} +Type: {{.Type}} +-- schema.json -- +{ + "format_version": "1.0", + "provider_schemas": { + "registry.terraform.io/hashicorp/scaffolding": { + "provider": { + "version": 0, + "block": { + "attributes": { + "endpoint": { + "type": "string", + "description": "Example provider attribute", + "description_kind": "markdown", + "optional": true + } + }, + "description": "Example provider", + "description_kind": "markdown" + } + }, + "resource_schemas": { + "scaffolding_example": { + "version": 0, + "block": { + "attributes": { + "configurable_attribute": { + "type": "string", + "description": "Example configurable attribute", + "description_kind": "markdown", + "optional": true + }, + "defaulted": { + "type": "string", + "description": "Example configurable attribute with default value", + "description_kind": "markdown", + "optional": true, + "computed": true + }, + "id": { + "type": "string", + "description": "Example identifier", + "description_kind": "markdown", + "computed": true + } + }, + "description": "Example resource", + "description_kind": "markdown" + } + } + }, + "data_source_schemas": { + "scaffolding_example": { + "version": 0, + "block": { + "attributes": { + "configurable_attribute": { + "type": "string", + "description": "Example configurable attribute", + "description_kind": "markdown", + "optional": true + }, + "id": { + "type": "string", + "description": "Example identifier", + "description_kind": "markdown", + "computed": true + } + }, + "description": "Example data source", + "description_kind": "markdown" + } + } + }, + "functions": { + "example": { + "description": "Given a string value, returns the same value.", + "summary": "Echo a string", + "return_type": "string", + "parameters": [ + { + "name": "input", + "description": "Value to echo.", + "type": "string" + } + ], + "variadic_parameter": { + "name": "variadicInput", + "description": "Variadic input to echo.", + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/cmd/tfplugindocs/testdata/scripts/schema-json/validate/framework_provider_success_registry_docs.txtar b/cmd/tfplugindocs/testdata/scripts/schema-json/validate/framework_provider_success_registry_docs.txtar new file mode 100644 index 00000000..197fceb1 --- /dev/null +++ b/cmd/tfplugindocs/testdata/scripts/schema-json/validate/framework_provider_success_registry_docs.txtar @@ -0,0 +1,174 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +# Successful run of tfplugindocs validate command on a Framework provider with docs in the registry directory structure (i.e. resource/.md.tmpl) +[!unix] skip +exec tfplugindocs validate --provider-name=terraform-provider-scaffolding --providers-schema=schema.json +cmpenv stdout expected-output.txt + +-- expected-output.txt -- +exporting schema from JSON file +getting provider schema +running mixed directories check +running number of files check +detected static docs directory, running checks +running invalid directories check on docs/data-sources +running file checks on docs/data-sources/example.md +running invalid directories check on docs/functions +running file checks on docs/functions/example.md +running invalid directories check on docs/guides +running file checks on docs/guides/example.md +running file checks on docs/index.md +running invalid directories check on docs/resources +running file checks on docs/resources/example.md +running file mismatch check +-- docs/guides/example.md -- +--- +subcategory: "Example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Example Guide + +Example contents. + +-- docs/resources/example.md -- +--- +subcategory: "Example" +page_title: "Example: example_thing" +description: |- + Example description. +--- +# Data Fields + +Name: {{.Name}} +Type: {{.Type}} +-- docs/data-sources/example.md -- +--- +subcategory: "Example" +page_title: "Example: example_thing" +description: |- + Example description. +--- +# Data Fields + +Name: {{.Name}} +Type: {{.Type}} +-- docs/functions/example.md -- +--- +subcategory: "Example" +page_title: "Example: example_thing" +description: |- + Example description. +--- +# Data Fields + +Name: {{.Name}} +Type: {{.Type}} +-- docs/index.md -- +--- +page_title: "Example Provider" +description: |- + Example description. +--- +# Data Fields + +Name: {{.Name}} +Type: {{.Type}} +-- schema.json -- +{ + "format_version": "1.0", + "provider_schemas": { + "registry.terraform.io/hashicorp/scaffolding": { + "provider": { + "version": 0, + "block": { + "attributes": { + "endpoint": { + "type": "string", + "description": "Example provider attribute", + "description_kind": "markdown", + "optional": true + } + }, + "description": "Example provider", + "description_kind": "markdown" + } + }, + "resource_schemas": { + "scaffolding_example": { + "version": 0, + "block": { + "attributes": { + "configurable_attribute": { + "type": "string", + "description": "Example configurable attribute", + "description_kind": "markdown", + "optional": true + }, + "defaulted": { + "type": "string", + "description": "Example configurable attribute with default value", + "description_kind": "markdown", + "optional": true, + "computed": true + }, + "id": { + "type": "string", + "description": "Example identifier", + "description_kind": "markdown", + "computed": true + } + }, + "description": "Example resource", + "description_kind": "markdown" + } + } + }, + "data_source_schemas": { + "scaffolding_example": { + "version": 0, + "block": { + "attributes": { + "configurable_attribute": { + "type": "string", + "description": "Example configurable attribute", + "description_kind": "markdown", + "optional": true + }, + "id": { + "type": "string", + "description": "Example identifier", + "description_kind": "markdown", + "computed": true + } + }, + "description": "Example data source", + "description_kind": "markdown" + } + } + }, + "functions": { + "example": { + "description": "Given a string value, returns the same value.", + "summary": "Echo a string", + "return_type": "string", + "parameters": [ + { + "name": "input", + "description": "Value to echo.", + "type": "string" + } + ], + "variadic_parameter": { + "name": "variadicInput", + "description": "Variadic input to echo.", + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/go.mod b/go.mod index cbe1336a..0115c442 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,10 @@ module github.com/hashicorp/terraform-plugin-docs -go 1.19 +go 1.21 require ( github.com/Kunde21/markdownfmt/v3 v3.1.0 + github.com/bmatcuk/doublestar/v4 v4.6.1 github.com/google/go-cmp v0.6.0 github.com/hashicorp/cli v1.1.6 github.com/hashicorp/go-version v1.6.0 @@ -15,11 +16,13 @@ require ( github.com/yuin/goldmark v1.7.0 github.com/yuin/goldmark-meta v1.1.0 github.com/zclconf/go-cty v1.14.4 + go.abhg.dev/goldmark/frontmatter v0.2.0 golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df golang.org/x/text v0.14.0 ) require ( + github.com/BurntSushi/toml v1.2.1 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.2.0 // indirect github.com/Masterminds/sprig/v3 v3.2.3 // indirect @@ -49,4 +52,5 @@ require ( golang.org/x/sys v0.16.0 // indirect golang.org/x/tools v0.13.0 // indirect gopkg.in/yaml.v2 v2.3.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 2a988dd1..6bfc0d97 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,7 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/Kunde21/markdownfmt/v3 v3.1.0 h1:KiZu9LKs+wFFBQKhrZJrFZwtLnCCWJahL+S+E/3VnM0= github.com/Kunde21/markdownfmt/v3 v3.1.0/go.mod h1:tPXN1RTyOzJwhfHoon9wUr4HGYmWgVxSQN6VBJDkrVc= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= @@ -8,6 +11,7 @@ github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYr github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/ProtonMail/go-crypto v1.1.0-alpha.0 h1:nHGfwXmFvJrSR9xu8qL7BkO4DqTHXE9N5vPhgY2I+j0= github.com/ProtonMail/go-crypto v1.1.0-alpha.0/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= @@ -16,20 +20,29 @@ github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= +github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4= +github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -65,9 +78,13 @@ github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -82,6 +99,7 @@ github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= @@ -89,10 +107,12 @@ github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSg github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ= +github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= @@ -100,8 +120,10 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA= github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= @@ -109,6 +131,8 @@ github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUei github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0= github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8= github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +go.abhg.dev/goldmark/frontmatter v0.2.0 h1:P8kPG0YkL12+aYk2yU3xHv4tcXzeVnN+gU0tJ5JnxRw= +go.abhg.dev/goldmark/frontmatter v0.2.0/go.mod h1:XqrEkZuM57djk7zrlRUB02x8I5J0px76YjkOzhB4YlU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= @@ -124,6 +148,7 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -154,6 +179,7 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T 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/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/check/directory.go b/internal/check/directory.go new file mode 100644 index 00000000..3aefa6a1 --- /dev/null +++ b/internal/check/directory.go @@ -0,0 +1,210 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package check + +import ( + "fmt" + "log" + "path/filepath" +) + +const ( + CdktfIndexDirectory = `cdktf` + + LegacyIndexDirectory = `website/docs` + LegacyDataSourcesDirectory = `d` + LegacyGuidesDirectory = `guides` + LegacyResourcesDirectory = `r` + LegacyFunctionsDirectory = `functions` + + RegistryIndexDirectory = `docs` + RegistryDataSourcesDirectory = `data-sources` + RegistryGuidesDirectory = `guides` + RegistryResourcesDirectory = `resources` + RegistryFunctionsDirectory = `functions` + + // Terraform Registry Storage Limits + // https://www.terraform.io/docs/registry/providers/docs.html#storage-limits + RegistryMaximumNumberOfFiles = 2000 + RegistryMaximumSizeOfFile = 500000 // 500KB + +) + +var ValidLegacyDirectories = []string{ + LegacyIndexDirectory, + LegacyIndexDirectory + "/" + LegacyDataSourcesDirectory, + LegacyIndexDirectory + "/" + LegacyGuidesDirectory, + LegacyIndexDirectory + "/" + LegacyResourcesDirectory, + LegacyIndexDirectory + "/" + LegacyFunctionsDirectory, +} + +var ValidRegistryDirectories = []string{ + RegistryIndexDirectory, + RegistryIndexDirectory + "/" + RegistryDataSourcesDirectory, + RegistryIndexDirectory + "/" + RegistryGuidesDirectory, + RegistryIndexDirectory + "/" + RegistryResourcesDirectory, + RegistryIndexDirectory + "/" + RegistryFunctionsDirectory, +} + +var ValidCdktfLanguages = []string{ + "csharp", + "go", + "java", + "python", + "typescript", +} + +var ValidLegacySubdirectories = []string{ + LegacyIndexDirectory, + LegacyDataSourcesDirectory, + LegacyGuidesDirectory, + LegacyResourcesDirectory, +} + +var ValidRegistrySubdirectories = []string{ + RegistryIndexDirectory, + RegistryDataSourcesDirectory, + RegistryGuidesDirectory, + RegistryResourcesDirectory, +} + +func InvalidDirectoriesCheck(dirPath string) error { + if IsValidRegistryDirectory(dirPath) { + return nil + } + + if IsValidLegacyDirectory(dirPath) { + return nil + } + + if IsValidCdktfDirectory(dirPath) { + return nil + } + + return fmt.Errorf("invalid Terraform Provider documentation directory found: %s", dirPath) + +} + +func MixedDirectoriesCheck(docFiles []string) error { + var legacyDirectoryFound bool + var registryDirectoryFound bool + err := fmt.Errorf("mixed Terraform Provider documentation directory layouts found, must use only legacy or registry layout") + + for _, file := range docFiles { + directory := filepath.Dir(file) + log.Printf("[DEBUG] Found directory: %s", directory) + + // Allow docs/ with other files + if IsValidRegistryDirectory(directory) && directory != RegistryIndexDirectory { + registryDirectoryFound = true + + if legacyDirectoryFound { + log.Printf("[DEBUG] Found mixed directories") + return err + } + } + + if IsValidLegacyDirectory(directory) { + legacyDirectoryFound = true + + if registryDirectoryFound { + log.Printf("[DEBUG] Found mixed directories") + return err + } + } + } + + return nil +} + +// NumberOfFilesCheck verifies that documentation is below the Terraform Registry storage limit. +// This check presumes that all provided directories are valid, e.g. that directory checking +// for invalid or mixed directory structures was previously completed. +func NumberOfFilesCheck(docFiles []string) error { + var numberOfFiles int + + directoryCounts := make(map[string]int) + for _, file := range docFiles { + directory := filepath.Dir(file) + + // Ignore CDKTF files. The file limit is per-language and presumably there is one CDKTF file per source HCL file. + if IsValidCdktfDirectory(directory) { + continue + } + + if directory == RegistryIndexDirectory || directory == filepath.FromSlash(LegacyIndexDirectory) { + continue + } + + directoryCounts[directory]++ + } + + for directory, count := range directoryCounts { + + log.Printf("[TRACE] Found %d documentation files in directory: %s", count, directory) + numberOfFiles = numberOfFiles + count + } + + log.Printf("[DEBUG] Found %d documentation files with limit of %d", numberOfFiles, RegistryMaximumNumberOfFiles) + if numberOfFiles >= RegistryMaximumNumberOfFiles { + return fmt.Errorf("exceeded maximum (%d) number of documentation files for Terraform Registry: %d", RegistryMaximumNumberOfFiles, numberOfFiles) + } + + return nil +} + +func IsValidLegacyDirectory(directory string) bool { + for _, validLegacyDirectory := range ValidLegacyDirectories { + if directory == filepath.FromSlash(validLegacyDirectory) { + return true + } + } + + return false +} + +func IsValidRegistryDirectory(directory string) bool { + for _, validRegistryDirectory := range ValidRegistryDirectories { + if directory == filepath.FromSlash(validRegistryDirectory) { + return true + } + } + + return false +} + +func IsValidCdktfDirectory(directory string) bool { + if directory == filepath.FromSlash(fmt.Sprintf("%s/%s", LegacyIndexDirectory, CdktfIndexDirectory)) { + return true + } + + if directory == filepath.FromSlash(fmt.Sprintf("%s/%s", RegistryIndexDirectory, CdktfIndexDirectory)) { + return true + } + + for _, validCdktfLanguage := range ValidCdktfLanguages { + + if directory == filepath.FromSlash(fmt.Sprintf("%s/%s/%s", LegacyIndexDirectory, CdktfIndexDirectory, validCdktfLanguage)) { + return true + } + + if directory == filepath.FromSlash(fmt.Sprintf("%s/%s/%s", RegistryIndexDirectory, CdktfIndexDirectory, validCdktfLanguage)) { + return true + } + + for _, validLegacySubdirectory := range ValidLegacySubdirectories { + if directory == filepath.FromSlash(fmt.Sprintf("%s/%s/%s/%s", LegacyIndexDirectory, CdktfIndexDirectory, validCdktfLanguage, validLegacySubdirectory)) { + return true + } + } + + for _, validRegistrySubdirectory := range ValidRegistrySubdirectories { + if directory == filepath.FromSlash(fmt.Sprintf("%s/%s/%s/%s", RegistryIndexDirectory, CdktfIndexDirectory, validCdktfLanguage, validRegistrySubdirectory)) { + return true + } + } + } + + return false +} diff --git a/internal/check/directory_test.go b/internal/check/directory_test.go new file mode 100644 index 00000000..dc0c5687 --- /dev/null +++ b/internal/check/directory_test.go @@ -0,0 +1,103 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package check + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/bmatcuk/doublestar/v4" +) + +var DocumentationGlobPattern = `{docs/index.md,docs/{,cdktf/}{data-sources,guides,resources,functions}/**/*,website/docs/**/*}` + +func TestNumberOfFilesCheck(t *testing.T) { + t.Parallel() + testCases := map[string]struct { + files []string + ExpectError bool + }{ + "under limit": { + files: testGenerateFiles(RegistryMaximumNumberOfFiles - 1), + }, + "at limit": { + files: testGenerateFiles(RegistryMaximumNumberOfFiles), + ExpectError: true, + }, + "over limit": { + files: testGenerateFiles(RegistryMaximumNumberOfFiles + 1), + ExpectError: true, + }, + } + + for name, testCase := range testCases { + name := name + testCase := testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + got := NumberOfFilesCheck(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 TestMixedDirectoriesCheck(t *testing.T) { + t.Parallel() + testCases := map[string]struct { + BasePath string + ExpectError bool + }{ + "valid mixed directories": { + BasePath: filepath.Join("testdata", "valid-mixed-directories"), + }, + "invalid mixed directories": { + BasePath: filepath.Join("testdata", "invalid-mixed-directories"), + ExpectError: true, + }, + } + + for name, testCase := range testCases { + name := name + testCase := testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + + providerFs := os.DirFS(testCase.BasePath) + + files, err := doublestar.Glob(providerFs, DocumentationGlobPattern) + if err != nil { + t.Fatalf("error finding documentation files: %s", err) + } + + got := MixedDirectoriesCheck(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 testGenerateFiles(numberOfFiles int) []string { + files := make([]string, numberOfFiles) + + for i := 0; i < numberOfFiles; i++ { + files[i] = fmt.Sprintf("thing%d.md", i) + } + + return files +} diff --git a/internal/check/file.go b/internal/check/file.go new file mode 100644 index 00000000..cb079b3a --- /dev/null +++ b/internal/check/file.go @@ -0,0 +1,39 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package check + +import ( + "fmt" + "log" + "os" + "path/filepath" +) + +type FileOptions struct { + BasePath string +} + +func (opts *FileOptions) FullPath(path string) string { + if opts.BasePath != "" { + return filepath.Join(opts.BasePath, path) + } + + return path +} + +// FileSizeCheck verifies that documentation file is below the Terraform Registry storage limit. +func FileSizeCheck(fullpath string) error { + fi, err := os.Stat(fullpath) + + if err != nil { + return err + } + + log.Printf("[DEBUG] File %s size: %d (limit: %d)", fullpath, fi.Size(), RegistryMaximumSizeOfFile) + if fi.Size() >= int64(RegistryMaximumSizeOfFile) { + return fmt.Errorf("exceeded maximum (%d) size of documentation file for Terraform Registry: %d", RegistryMaximumSizeOfFile, fi.Size()) + } + + return nil +} diff --git a/internal/check/file_extension.go b/internal/check/file_extension.go new file mode 100644 index 00000000..dd5f37b6 --- /dev/null +++ b/internal/check/file_extension.go @@ -0,0 +1,64 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package check + +import ( + "fmt" + "path/filepath" + "strings" +) + +const ( + FileExtensionHtmlMarkdown = `.html.markdown` + FileExtensionHtmlMd = `.html.md` + FileExtensionMarkdown = `.markdown` + FileExtensionMd = `.md` +) + +var ValidLegacyFileExtensions = []string{ + FileExtensionHtmlMarkdown, + FileExtensionHtmlMd, + FileExtensionMarkdown, + FileExtensionMd, +} + +var ValidRegistryFileExtensions = []string{ + FileExtensionMd, +} + +// FileExtensionCheck checks if the file extension of the given path is valid. +func FileExtensionCheck(path string, validExtensions []string) error { + if !FilePathEndsWithExtensionFrom(path, validExtensions) { + return fmt.Errorf("file does not end with a valid extension, valid extensions: %v", ValidLegacyFileExtensions) + } + + return nil +} + +func FilePathEndsWithExtensionFrom(path string, validExtensions []string) bool { + for _, validExtension := range validExtensions { + if strings.HasSuffix(path, validExtension) { + return true + } + } + + 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/internal/check/file_extension_test.go b/internal/check/file_extension_test.go new file mode 100644 index 00000000..15d756c4 --- /dev/null +++ b/internal/check/file_extension_test.go @@ -0,0 +1,51 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package check + +import ( + "testing" +) + +func TestTrimFileExtension(t *testing.T) { + t.Parallel() + testCases := map[string]struct { + Path string + Expect string + }{ + "empty path": { + Path: "", + Expect: "", + }, + "filename with single extension": { + Path: "file.md", + Expect: "file", + }, + "filename with multiple extensions": { + Path: "file.html.markdown", + Expect: "file", + }, + "full path with single extension": { + Path: "docs/resource/thing.md", + Expect: "thing", + }, + "full path with multiple extensions": { + Path: "website/docs/r/thing.html.markdown", + Expect: "thing", + }, + } + + for name, testCase := range testCases { + name := name + testCase := testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + got := TrimFileExtension(testCase.Path) + want := testCase.Expect + + if got != want { + t.Errorf("expected %s, got %s", want, got) + } + }) + } +} diff --git a/internal/check/file_mismatch.go b/internal/check/file_mismatch.go new file mode 100644 index 00000000..d65989fd --- /dev/null +++ b/internal/check/file_mismatch.go @@ -0,0 +1,284 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package check + +import ( + "errors" + "fmt" + "log" + "os" + "sort" + + tfjson "github.com/hashicorp/terraform-json" +) + +type FileMismatchOptions struct { + *FileOptions + + IgnoreFileMismatch []string + + IgnoreFileMissing []string + + ProviderShortName string + + DatasourceEntries []os.DirEntry + + ResourceEntries []os.DirEntry + + FunctionEntries []os.DirEntry + + Schema *tfjson.ProviderSchema +} + +type FileMismatchCheck struct { + Options *FileMismatchOptions +} + +func NewFileMismatchCheck(opts *FileMismatchOptions) *FileMismatchCheck { + check := &FileMismatchCheck{ + Options: opts, + } + + if check.Options == nil { + check.Options = &FileMismatchOptions{} + } + + if check.Options.FileOptions == nil { + check.Options.FileOptions = &FileOptions{} + } + + return check +} + +func (check *FileMismatchCheck) Run() error { + var result error + + if check.Options.Schema == nil { + log.Printf("[DEBUG] Skipping file mismatch checks due to missing provider schema") + return nil + } + + if check.Options.ResourceEntries != nil { + err := check.ResourceFileMismatchCheck(check.Options.ResourceEntries, "resource", check.Options.Schema.ResourceSchemas) + result = errors.Join(result, err) + } + + if check.Options.DatasourceEntries != nil { + err := check.ResourceFileMismatchCheck(check.Options.DatasourceEntries, "datasource", check.Options.Schema.DataSourceSchemas) + result = errors.Join(result, err) + } + + if check.Options.FunctionEntries != nil { + err := check.FunctionFileMismatchCheck(check.Options.FunctionEntries, check.Options.Schema.Functions) + result = errors.Join(result, err) + } + + return result +} + +// ResourceFileMismatchCheck checks for mismatched files, either missing or extraneous, against the resource/datasouce schema +func (check *FileMismatchCheck) ResourceFileMismatchCheck(files []os.DirEntry, resourceType string, schemas map[string]*tfjson.Schema) error { + if len(files) == 0 { + log.Printf("[DEBUG] Skipping %s file mismatch checks due to missing file list", resourceType) + return nil + } + + if len(schemas) == 0 { + log.Printf("[DEBUG] Skipping %s file mismatch checks due to missing schemas", resourceType) + return nil + } + + var extraFiles []string + var missingFiles []string + + for _, file := range files { + log.Printf("[DEBUG] Found file %s", file.Name()) + if fileHasResource(schemas, check.Options.ProviderShortName, file.Name()) { + continue + } + + if check.IgnoreFileMismatch(file.Name()) { + continue + } + + log.Printf("[DEBUG] Found extraneous file %s", file.Name()) + extraFiles = append(extraFiles, file.Name()) + } + + for _, resourceName := range resourceNames(schemas) { + log.Printf("[DEBUG] Found %s %s", resourceType, resourceName) + if resourceHasFile(files, check.Options.ProviderShortName, resourceName) { + continue + } + + if check.IgnoreFileMissing(resourceName) { + continue + } + + log.Printf("[DEBUG] Missing file for %s %s", resourceType, resourceName) + missingFiles = append(missingFiles, resourceName) + } + + var result 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 = errors.Join(result, err) + } + + for _, missingFile := range missingFiles { + err := fmt.Errorf("missing documentation file for %s: %s", resourceType, missingFile) + result = errors.Join(result, err) + } + + return result + +} + +// FunctionFileMismatchCheck checks for mismatched files, either missing or extraneous, against the function signature +func (check *FileMismatchCheck) FunctionFileMismatchCheck(files []os.DirEntry, functions map[string]*tfjson.FunctionSignature) error { + if len(files) == 0 { + log.Printf("[DEBUG] Skipping function file mismatch checks due to missing file list") + return nil + } + + if len(functions) == 0 { + log.Printf("[DEBUG] Skipping function file mismatch checks due to missing schemas") + return nil + } + + var extraFiles []string + var missingFiles []string + + for _, file := range files { + if fileHasFunction(functions, file.Name()) { + continue + } + + if check.IgnoreFileMismatch(file.Name()) { + continue + } + + extraFiles = append(extraFiles, file.Name()) + } + + for _, functionName := range functionNames(functions) { + if functionHasFile(files, functionName) { + continue + } + + if check.IgnoreFileMissing(functionName) { + continue + } + + missingFiles = append(missingFiles, functionName) + } + + var result error + + for _, extraFile := range extraFiles { + err := fmt.Errorf("matching function for documentation file (%s) not found, file is extraneous or incorrectly named", extraFile) + result = errors.Join(result, err) + } + + for _, missingFile := range missingFiles { + err := fmt.Errorf("missing documentation file for function: %s", missingFile) + result = errors.Join(result, err) + } + + return result + +} + +func (check *FileMismatchCheck) IgnoreFileMismatch(file string) bool { + for _, ignoreResourceName := range check.Options.IgnoreFileMismatch { + if ignoreResourceName == fileResourceName(check.Options.ProviderShortName, file) { + return true + } + } + + return false +} + +func (check *FileMismatchCheck) IgnoreFileMissing(resourceName string) bool { + for _, ignoreResourceName := range check.Options.IgnoreFileMissing { + if ignoreResourceName == resourceName { + return true + } + } + + return false +} + +func fileHasResource(schemaResources map[string]*tfjson.Schema, providerName, file string) bool { + if _, ok := schemaResources[fileResourceName(providerName, file)]; ok { + return true + } + + return false +} + +func fileHasFunction(functions map[string]*tfjson.FunctionSignature, file string) bool { + if _, ok := functions[TrimFileExtension(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 []os.DirEntry, providerName, resourceName string) bool { + var found bool + + for _, file := range files { + if fileResourceName(providerName, file.Name()) == resourceName { + found = true + break + } + } + + return found +} + +func functionHasFile(files []os.DirEntry, functionName string) bool { + var found bool + + for _, file := range files { + if TrimFileExtension(file.Name()) == functionName { + 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 +} + +func functionNames(functions map[string]*tfjson.FunctionSignature) []string { + names := make([]string, 0, len(functions)) + + for name := range functions { + names = append(names, name) + } + + sort.Strings(names) + + return names +} diff --git a/internal/check/file_mismatch_test.go b/internal/check/file_mismatch_test.go new file mode 100644 index 00000000..50ef8dfc --- /dev/null +++ b/internal/check/file_mismatch_test.go @@ -0,0 +1,435 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package check + +import ( + "path/filepath" + "reflect" + "testing" + "testing/fstest" + + tfjson "github.com/hashicorp/terraform-json" +) + +func TestFileHasResource(t *testing.T) { + t.Parallel() + testCases := map[string]struct { + File string + Resources map[string]*tfjson.Schema + Expect bool + }{ + "found": { + File: "resource1.md", + Resources: map[string]*tfjson.Schema{ + "test_resource1": {}, + "test_resource2": {}, + }, + Expect: true, + }, + "not found": { + File: "resource1.md", + Resources: map[string]*tfjson.Schema{ + "test_resource2": {}, + "test_resource3": {}, + }, + Expect: false, + }, + } + + for name, testCase := range testCases { + name := name + testCase := testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + + 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) { + t.Parallel() + testCases := map[string]struct { + File string + Expect string + }{ + "filename with single extension": { + File: "file.md", + Expect: "test_file", + }, + "filename with multiple extensions": { + File: "file.html.markdown", + Expect: "test_file", + }, + "full path with single extension": { + File: filepath.Join("docs", "resource", "thing.md"), + Expect: "test_thing", + }, + "full path with multiple extensions": { + File: filepath.Join("website", "docs", "r", "thing.html.markdown"), + Expect: "test_thing", + }, + } + + for name, testCase := range testCases { + name := name + testCase := testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + got := fileResourceName("test", testCase.File) + want := testCase.Expect + + if got != want { + t.Errorf("expected %s, got %s", want, got) + } + }) + } +} + +func TestFileMismatchCheck(t *testing.T) { + t.Parallel() + testCases := map[string]struct { + ResourceFiles fstest.MapFS + FunctionFiles fstest.MapFS + Options *FileMismatchOptions + ExpectError bool + }{ + "all found - resource": { + ResourceFiles: fstest.MapFS{ + "resource1.md": {}, + "resource2.md": {}, + }, + Options: &FileMismatchOptions{ + ProviderShortName: "test", + Schema: &tfjson.ProviderSchema{ + ResourceSchemas: map[string]*tfjson.Schema{ + "test_resource1": {}, + "test_resource2": {}, + }, + }, + }, + }, + "all found - function": { + FunctionFiles: fstest.MapFS{ + "function1.md": {}, + "function2.md": {}, + }, + Options: &FileMismatchOptions{ + ProviderShortName: "test", + Schema: &tfjson.ProviderSchema{ + Functions: map[string]*tfjson.FunctionSignature{ + "function1": {}, + "function2": {}, + }, + }, + }, + }, + "extra file - resource": { + ResourceFiles: fstest.MapFS{ + "resource1.md": {}, + "resource2.md": {}, + "resource3.md": {}, + }, + Options: &FileMismatchOptions{ + ProviderShortName: "test", + Schema: &tfjson.ProviderSchema{ + ResourceSchemas: map[string]*tfjson.Schema{ + "test_resource1": {}, + "test_resource2": {}, + }, + }, + }, + ExpectError: true, + }, + "extra file - function": { + FunctionFiles: fstest.MapFS{ + "function1.md": {}, + "function2.md": {}, + "function3.md": {}, + }, + Options: &FileMismatchOptions{ + ProviderShortName: "test", + Schema: &tfjson.ProviderSchema{ + Functions: map[string]*tfjson.FunctionSignature{ + "function1": {}, + "function2": {}, + }, + }, + }, + ExpectError: true, + }, + "ignore extra file - resource": { + ResourceFiles: fstest.MapFS{ + "resource1.md": {}, + "resource2.md": {}, + "resource3.md": {}, + }, + Options: &FileMismatchOptions{ + IgnoreFileMismatch: []string{"test_resource3"}, + ProviderShortName: "test", + Schema: &tfjson.ProviderSchema{ + ResourceSchemas: map[string]*tfjson.Schema{ + "test_resource1": {}, + "test_resource2": {}, + }, + }, + }, + }, + "ignore extra file - function": { + FunctionFiles: fstest.MapFS{ + "function1.md": {}, + "function2.md": {}, + "function3.md": {}, + }, + Options: &FileMismatchOptions{ + IgnoreFileMismatch: []string{"function3"}, + ProviderShortName: "test", + Schema: &tfjson.ProviderSchema{ + Functions: map[string]*tfjson.FunctionSignature{ + "function1": {}, + "function2": {}, + "function3": {}, + }, + }, + }, + }, + "missing file - resource": { + ResourceFiles: fstest.MapFS{ + "resource1.md": {}, + }, + Options: &FileMismatchOptions{ + ProviderShortName: "test", + Schema: &tfjson.ProviderSchema{ + ResourceSchemas: map[string]*tfjson.Schema{ + "test_resource1": {}, + "test_resource2": {}, + }, + }, + }, + ExpectError: true, + }, + "missing file - function": { + FunctionFiles: fstest.MapFS{ + "function1.md": {}, + }, + Options: &FileMismatchOptions{ + ProviderShortName: "test", + Schema: &tfjson.ProviderSchema{ + Functions: map[string]*tfjson.FunctionSignature{ + "function1": {}, + "function2": {}, + }, + }, + }, + ExpectError: true, + }, + "ignore missing file - resource": { + ResourceFiles: fstest.MapFS{ + "resource1.md": {}, + }, + Options: &FileMismatchOptions{ + IgnoreFileMissing: []string{"test_resource2"}, + ProviderShortName: "test", + Schema: &tfjson.ProviderSchema{ + ResourceSchemas: map[string]*tfjson.Schema{ + "test_resource1": {}, + "test_resource2": {}, + }, + }, + }, + }, + "ignore missing file - function": { + FunctionFiles: fstest.MapFS{ + "function1.md": {}, + }, + Options: &FileMismatchOptions{ + IgnoreFileMissing: []string{"function2"}, + ProviderShortName: "test", + Schema: &tfjson.ProviderSchema{ + Functions: map[string]*tfjson.FunctionSignature{ + "function1": {}, + "function2": {}, + }, + }, + }, + }, + "no files": { + Options: &FileMismatchOptions{ + ProviderShortName: "test", + Schema: &tfjson.ProviderSchema{ + ResourceSchemas: map[string]*tfjson.Schema{ + "test_resource1": {}, + "test_resource2": {}, + }, + Functions: map[string]*tfjson.FunctionSignature{ + "function1": {}, + "function2": {}, + }, + }, + }, + }, + "no schemas": { + ResourceFiles: fstest.MapFS{ + "resource1.md": {}, + }, + FunctionFiles: fstest.MapFS{ + "function1.md": {}, + }, + Options: &FileMismatchOptions{ + ProviderShortName: "test", + }, + }, + } + + for name, testCase := range testCases { + name := name + testCase := testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + + resourceFiles, _ := testCase.ResourceFiles.ReadDir(".") + functionFiles, _ := testCase.FunctionFiles.ReadDir(".") + testCase.Options.ResourceEntries = resourceFiles + testCase.Options.FunctionEntries = functionFiles + got := NewFileMismatchCheck(testCase.Options).Run() + + 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) { + t.Parallel() + testCases := map[string]struct { + FS fstest.MapFS + ResourceName string + Expect bool + }{ + "found": { + FS: fstest.MapFS{ + "resource1.md": {}, + "resource2.md": {}, + }, + ResourceName: "test_resource1", + Expect: true, + }, + "not found": { + FS: fstest.MapFS{ + "resource1.md": {}, + "resource2.md": {}, + }, + ResourceName: "test_resource3", + Expect: false, + }, + } + + for name, testCase := range testCases { + name := name + testCase := testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + + files, _ := testCase.FS.ReadDir(".") + + got := resourceHasFile(files, "test", testCase.ResourceName) + want := testCase.Expect + + if got != want { + t.Errorf("expected %t, got %t", want, got) + } + }) + } +} + +func TestFunctionHasFile(t *testing.T) { + t.Parallel() + testCases := map[string]struct { + FS fstest.MapFS + FunctionName string + Expect bool + }{ + "found": { + FS: fstest.MapFS{ + "function1.md": {}, + "function2.md": {}, + }, + FunctionName: "function1", + Expect: true, + }, + "not found": { + FS: fstest.MapFS{ + "function1.md": {}, + "function2.md": {}, + }, + FunctionName: "function3", + Expect: false, + }, + } + + for name, testCase := range testCases { + name := name + testCase := testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + + files, _ := testCase.FS.ReadDir(".") + + got := functionHasFile(files, testCase.FunctionName) + want := testCase.Expect + + if got != want { + t.Errorf("expected %t, got %t", want, got) + } + }) + } +} + +func TestResourceNames(t *testing.T) { + t.Parallel() + testCases := map[string]struct { + Resources map[string]*tfjson.Schema + Expect []string + }{ + "empty": { + Resources: map[string]*tfjson.Schema{}, + Expect: []string{}, + }, + "multiple": { + Resources: map[string]*tfjson.Schema{ + "test_resource1": {}, + "test_resource2": {}, + }, + Expect: []string{ + "test_resource1", + "test_resource2", + }, + }, + } + + for name, testCase := range testCases { + name := name + testCase := testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := resourceNames(testCase.Resources) + want := testCase.Expect + + if !reflect.DeepEqual(got, want) { + t.Errorf("expected %v, got %v", want, got) + } + }) + } +} diff --git a/internal/check/file_test.go b/internal/check/file_test.go new file mode 100644 index 00000000..c8ec7b8b --- /dev/null +++ b/internal/check/file_test.go @@ -0,0 +1,93 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package check + +import ( + "os" + "path/filepath" + "testing" +) + +func TestFileSizeCheck(t *testing.T) { + t.Parallel() + testCases := map[string]struct { + Size int64 + ExpectError bool + }{ + "under limit": { + Size: RegistryMaximumSizeOfFile - 1, + }, + "on limit": { + Size: RegistryMaximumSizeOfFile, + ExpectError: true, + }, + "over limit": { + Size: RegistryMaximumSizeOfFile + 1, + ExpectError: true, + }, + } + + for name, testCase := range testCases { + name := name + testCase := testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + + file, _ := os.CreateTemp(t.TempDir(), "TestFileSizeCheck") + + defer file.Close() + + if err := file.Truncate(testCase.Size); err != nil { + t.Fatalf("error writing temporary file: %s", err) + } + + got := FileSizeCheck(file.Name()) + + 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 TestFullPath(t *testing.T) { + t.Parallel() + testCases := map[string]struct { + FileOptions *FileOptions + Path string + Expect string + }{ + "without base path": { + FileOptions: &FileOptions{}, + Path: filepath.FromSlash("docs/resources/thing.md"), + Expect: filepath.FromSlash("docs/resources/thing.md"), + }, + "with base path": { + FileOptions: &FileOptions{ + BasePath: filepath.FromSlash("/full/path/to"), + }, + Path: filepath.FromSlash("docs/resources/thing.md"), + Expect: filepath.FromSlash("/full/path/to/docs/resources/thing.md"), + }, + } + + for name, testCase := range testCases { + name := name + testCase := testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.FileOptions.FullPath(testCase.Path) + want := testCase.Expect + + if got != want { + t.Errorf("expected %s, got %s", want, got) + } + }) + } +} diff --git a/internal/check/frontmatter.go b/internal/check/frontmatter.go new file mode 100644 index 00000000..65ac43aa --- /dev/null +++ b/internal/check/frontmatter.go @@ -0,0 +1,104 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package check + +import ( + "bytes" + "fmt" + + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/parser" + "go.abhg.dev/goldmark/frontmatter" +) + +type FrontMatterCheck struct { + Options *FrontMatterOptions +} + +// FrontMatterData represents the YAML frontmatter of Terraform Provider documentation. +type FrontMatterData struct { + Description *string `yaml:"description,omitempty"` + Layout *string `yaml:"layout,omitempty"` + PageTitle *string `yaml:"page_title,omitempty"` + SidebarCurrent *string `yaml:"sidebar_current,omitempty"` + Subcategory *string `yaml:"subcategory,omitempty"` +} + +// FrontMatterOptions represents configuration options for FrontMatter. +type FrontMatterOptions struct { + NoLayout bool + NoPageTitle bool + NoSidebarCurrent bool + NoSubcategory bool + RequireDescription bool + RequireLayout bool + RequirePageTitle bool +} + +func NewFrontMatterCheck(opts *FrontMatterOptions) *FrontMatterCheck { + check := &FrontMatterCheck{ + Options: opts, + } + + if check.Options == nil { + check.Options = &FrontMatterOptions{} + } + + return check +} + +func (check *FrontMatterCheck) Run(src []byte) error { + frontMatter := FrontMatterData{} + + md := goldmark.New( + goldmark.WithExtensions(&frontmatter.Extender{}), + ) + + ctx := parser.NewContext() + var buff bytes.Buffer + + err := md.Convert(src, &buff, parser.WithContext(ctx)) + if err != nil { + return err + } + d := frontmatter.Get(ctx) + if d == nil { + return fmt.Errorf("no frontmatter found") + } + + err = d.Decode(&frontMatter) + if err != nil { + return fmt.Errorf("error parsing YAML frontmatter: %w", err) + } + + if check.Options.NoLayout && frontMatter.Layout != nil { + return fmt.Errorf("YAML frontmatter should not contain layout") + } + + if check.Options.NoPageTitle && frontMatter.PageTitle != nil { + return fmt.Errorf("YAML frontmatter should not contain page_title") + } + + if check.Options.NoSidebarCurrent && frontMatter.SidebarCurrent != nil { + return fmt.Errorf("YAML frontmatter should not contain sidebar_current") + } + + if check.Options.NoSubcategory && frontMatter.Subcategory != nil { + return fmt.Errorf("YAML frontmatter should not contain subcategory") + } + + if check.Options.RequireDescription && frontMatter.Description == nil { + return fmt.Errorf("YAML frontmatter missing required description") + } + + if check.Options.RequireLayout && frontMatter.Layout == nil { + return fmt.Errorf("YAML frontmatter missing required layout") + } + + if check.Options.RequirePageTitle && frontMatter.PageTitle == nil { + return fmt.Errorf("YAML frontmatter missing required page_title") + } + + return nil +} diff --git a/internal/check/frontmatter_test.go b/internal/check/frontmatter_test.go new file mode 100644 index 00000000..9189b0de --- /dev/null +++ b/internal/check/frontmatter_test.go @@ -0,0 +1,160 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package check + +import ( + "testing" +) + +func TestFrontMatterCheck(t *testing.T) { + t.Parallel() + testCases := map[string]struct { + Source string + Options *FrontMatterOptions + ExpectError bool + }{ + "empty source": { + Source: ``, + ExpectError: true, + }, + "valid YAML with default options": { + Source: ` +--- +description: |- + Example description +layout: "example" +page_title: Example Page Title +subcategory: Example Subcategory +--- +`, + }, + "valid YAML section and Markdown with default options": { + Source: ` +--- +description: |- + Example description +layout: "example" +page_title: Example Page Title +subcategory: Example Subcategory +--- + +# Markdown here we go! +`, + }, + "invalid YAML": { + Source: ` +description: |- + Example description +Extraneous newline +`, + ExpectError: true, + }, + "no layout option": { + Source: ` +description: |- + Example description +layout: "example" +page_title: Example Page Title +subcategory: Example Subcategory +`, + Options: &FrontMatterOptions{ + NoLayout: true, + }, + ExpectError: true, + }, + "no page_title option": { + Source: ` +description: |- + Example description +layout: "example" +page_title: Example Page Title +subcategory: Example Subcategory +`, + Options: &FrontMatterOptions{ + NoPageTitle: true, + }, + ExpectError: true, + }, + "no sidebar_current option": { + Source: ` +description: |- + Example description +layout: "example" +page_title: Example Page Title +sidebar_current: "example_resource" +subcategory: Example Subcategory +`, + Options: &FrontMatterOptions{ + NoSidebarCurrent: true, + }, + ExpectError: true, + }, + "no subcategory option": { + Source: ` +description: |- + Example description +layout: "example" +page_title: Example Page Title +subcategory: Example Subcategory +`, + Options: &FrontMatterOptions{ + NoSubcategory: true, + }, + ExpectError: true, + }, + "require description option": { + Source: ` +layout: "example" +page_title: Example Page Title +subcategory: Example Subcategory +`, + Options: &FrontMatterOptions{ + RequireDescription: true, + }, + ExpectError: true, + }, + "require layout option": { + Source: ` +description: |- + Example description +page_title: Example Page Title +subcategory: Example Subcategory +`, + Options: &FrontMatterOptions{ + RequireLayout: true, + }, + ExpectError: true, + }, + "require page_title option": { + Source: ` +description: |- + Example description +layout: "example" +subcategory: Example Subcategory +`, + Options: &FrontMatterOptions{ + RequirePageTitle: true, + }, + ExpectError: true, + }, + } + + for name, testCase := range testCases { + name := name + testCase := testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := NewFrontMatterCheck(testCase.Options).Run([]byte(testCase.Source)) + + 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) + } + }) + } +} diff --git a/internal/check/provider_file.go b/internal/check/provider_file.go new file mode 100644 index 00000000..5358b669 --- /dev/null +++ b/internal/check/provider_file.go @@ -0,0 +1,67 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package check + +import ( + "fmt" + "log" + "os" +) + +type ProviderFileOptions struct { + *FileOptions + + FrontMatter *FrontMatterOptions + ValidExtensions []string +} + +type ProviderFileCheck struct { + Options *ProviderFileOptions +} + +func NewProviderFileCheck(opts *ProviderFileOptions) *ProviderFileCheck { + check := &ProviderFileCheck{ + Options: opts, + } + + if check.Options == nil { + check.Options = &ProviderFileOptions{} + } + + if check.Options.FileOptions == nil { + check.Options.FileOptions = &FileOptions{} + } + + if check.Options.FrontMatter == nil { + check.Options.FrontMatter = &FrontMatterOptions{} + } + + return check +} + +func (check *ProviderFileCheck) Run(path string) error { + fullpath := check.Options.FullPath(path) + + log.Printf("[DEBUG] Checking file: %s", fullpath) + + if err := FileExtensionCheck(path, check.Options.ValidExtensions); err != nil { + return fmt.Errorf("%s: error checking file extension: %w", path, err) + } + + if err := FileSizeCheck(fullpath); err != nil { + return fmt.Errorf("%s: error checking file size: %w", path, err) + } + + content, err := os.ReadFile(fullpath) + + if err != nil { + return fmt.Errorf("%s: error reading file: %w", path, err) + } + + if err := NewFrontMatterCheck(check.Options.FrontMatter).Run(content); err != nil { + return fmt.Errorf("%s: error checking file frontmatter: %w", path, err) + } + + return nil +} diff --git a/internal/check/testdata/invalid-mixed-directories/docs/resources/thing.md b/internal/check/testdata/invalid-mixed-directories/docs/resources/thing.md new file mode 100644 index 00000000..59bb04cc --- /dev/null +++ b/internal/check/testdata/invalid-mixed-directories/docs/resources/thing.md @@ -0,0 +1,27 @@ +--- +subcategory: "Example" +layout: "example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Resource: example_thing + +Byline. + +## Example Usage + +```terraform +resource "example_thing" "example" { + name = "example" +} +``` + +## Argument Reference + +* `name` - (Required) Name of thing. + +## Attribute Reference + +* `id` - Name of thing. diff --git a/internal/check/testdata/invalid-mixed-directories/website/docs/r/thing.html.markdown b/internal/check/testdata/invalid-mixed-directories/website/docs/r/thing.html.markdown new file mode 100644 index 00000000..e69de29b diff --git a/internal/check/testdata/valid-mixed-directories/docs/CONTRIBUTING.md b/internal/check/testdata/valid-mixed-directories/docs/CONTRIBUTING.md new file mode 100644 index 00000000..423305c4 --- /dev/null +++ b/internal/check/testdata/valid-mixed-directories/docs/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# Contributing Guide + +This file has contents and no YAML frontmatter, because it is not a Terraform Provider documentation file and that is okay. diff --git a/internal/check/testdata/valid-mixed-directories/docs/nonregistrydocs/valid.md b/internal/check/testdata/valid-mixed-directories/docs/nonregistrydocs/valid.md new file mode 100644 index 00000000..a7f1fce5 --- /dev/null +++ b/internal/check/testdata/valid-mixed-directories/docs/nonregistrydocs/valid.md @@ -0,0 +1,3 @@ +# Valid + +Files in `/docs`, but outside Registry documentation directories, should be ignored. diff --git a/internal/check/testdata/valid-mixed-directories/website/docs/r/thing.html.markdown b/internal/check/testdata/valid-mixed-directories/website/docs/r/thing.html.markdown new file mode 100644 index 00000000..59bb04cc --- /dev/null +++ b/internal/check/testdata/valid-mixed-directories/website/docs/r/thing.html.markdown @@ -0,0 +1,27 @@ +--- +subcategory: "Example" +layout: "example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Resource: example_thing + +Byline. + +## Example Usage + +```terraform +resource "example_thing" "example" { + name = "example" +} +``` + +## Argument Reference + +* `name` - (Required) Name of thing. + +## Attribute Reference + +* `id` - Name of thing. diff --git a/internal/cmd/validate.go b/internal/cmd/validate.go index c4a406cf..8999c4e8 100644 --- a/internal/cmd/validate.go +++ b/internal/cmd/validate.go @@ -4,6 +4,7 @@ package cmd import ( + "errors" "flag" "fmt" "strings" @@ -13,10 +14,15 @@ import ( type validateCmd struct { commonCmd + + flagProviderName string + flagProviderDir string + flagProvidersSchema string + tfVersion string } func (cmd *validateCmd) Synopsis() string { - return "validates a plugin website for the current directory" + return "validates a plugin website" } func (cmd *validateCmd) Help() string { @@ -59,6 +65,10 @@ func (cmd *validateCmd) Help() string { func (cmd *validateCmd) Flags() *flag.FlagSet { fs := flag.NewFlagSet("validate", flag.ExitOnError) + fs.StringVar(&cmd.flagProviderName, "provider-name", "", "provider name, as used in Terraform configurations") + fs.StringVar(&cmd.flagProviderDir, "provider-dir", "", "relative or absolute path to the root provider code directory; this will default to the current working directory if not set") + fs.StringVar(&cmd.flagProvidersSchema, "providers-schema", "", "path to the providers schema JSON file, which contains the output of the terraform providers schema -json command. Setting this flag will skip building the provider and calling Terraform CLI") + fs.StringVar(&cmd.tfVersion, "tf-version", "", "terraform binary version to download. If not provided, will look for a terraform binary in the local environment. If not found in the environment, will download the latest version of Terraform") return fs } @@ -74,9 +84,14 @@ func (cmd *validateCmd) Run(args []string) int { } func (cmd *validateCmd) runInternal() error { - err := provider.Validate(cmd.ui) + err := provider.Validate(cmd.ui, + cmd.flagProviderDir, + cmd.flagProviderName, + cmd.flagProvidersSchema, + cmd.tfVersion, + ) if err != nil { - return fmt.Errorf("unable to validate website: %w", err) + return errors.Join(errors.New("validation errors found: "), err) } return nil diff --git a/internal/provider/generate.go b/internal/provider/generate.go index d0c53a54..d0c3c965 100644 --- a/internal/provider/generate.go +++ b/internal/provider/generate.go @@ -8,7 +8,6 @@ import ( "fmt" "os" "os/exec" - "path" "path/filepath" "runtime" "strings" @@ -455,7 +454,7 @@ func (g *generator) renderStaticWebsite(providerSchema *tfjson.ProviderSchema) e // Remove subdirectories managed by tfplugindocs if file.IsDir() && slices.Contains(managedWebsiteSubDirectories, file.Name()) { g.infof("removing directory: %q", file.Name()) - err = os.RemoveAll(path.Join(g.ProviderDocsDir(), file.Name())) + err = os.RemoveAll(filepath.Join(g.ProviderDocsDir(), file.Name())) if err != nil { return fmt.Errorf("unable to remove directory %q from rendered website directory: %w", file.Name(), err) } @@ -465,7 +464,7 @@ func (g *generator) renderStaticWebsite(providerSchema *tfjson.ProviderSchema) e // Remove files managed by tfplugindocs if !file.IsDir() && slices.Contains(managedWebsiteFiles, file.Name()) { g.infof("removing file: %q", file.Name()) - err = os.RemoveAll(path.Join(g.ProviderDocsDir(), file.Name())) + err = os.RemoveAll(filepath.Join(g.ProviderDocsDir(), file.Name())) if err != nil { return fmt.Errorf("unable to remove file %q from rendered website directory: %w", file.Name(), err) } diff --git a/internal/provider/logger.go b/internal/provider/logger.go new file mode 100644 index 00000000..366812bc --- /dev/null +++ b/internal/provider/logger.go @@ -0,0 +1,27 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "fmt" + + "github.com/hashicorp/cli" +) + +type Logger struct { + ui cli.Ui +} + +func NewLogger(ui cli.Ui) *Logger { + return &Logger{ui} +} + +func (l *Logger) infof(format string, args ...interface{}) { + l.ui.Info(fmt.Sprintf(format, args...)) +} + +//nolint:unused +func (l *Logger) warnf(format string, args ...interface{}) { + l.ui.Warn(fmt.Sprintf(format, args...)) +} diff --git a/internal/provider/schema.go b/internal/provider/schema.go new file mode 100644 index 00000000..3338fe98 --- /dev/null +++ b/internal/provider/schema.go @@ -0,0 +1,136 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + + "github.com/hashicorp/go-version" + install "github.com/hashicorp/hc-install" + "github.com/hashicorp/hc-install/checkpoint" + "github.com/hashicorp/hc-install/fs" + "github.com/hashicorp/hc-install/product" + "github.com/hashicorp/hc-install/releases" + "github.com/hashicorp/hc-install/src" + "github.com/hashicorp/terraform-exec/tfexec" + tfjson "github.com/hashicorp/terraform-json" +) + +func TerraformProviderSchemaFromTerraform(ctx context.Context, providerName, providerDir, tfVersion string, l *Logger) (*tfjson.ProviderSchema, error) { + var err error + + shortName := providerShortName(providerName) + + tmpDir, err := os.MkdirTemp("", "tfws") + if err != nil { + return nil, fmt.Errorf("unable to create temporary provider install directory %q: %w", tmpDir, err) + } + defer os.RemoveAll(tmpDir) + + l.infof("compiling provider %q", shortName) + providerPath := fmt.Sprintf("plugins/registry.terraform.io/hashicorp/%s/0.0.1/%s_%s", shortName, runtime.GOOS, runtime.GOARCH) + outFile := filepath.Join(tmpDir, providerPath, fmt.Sprintf("terraform-provider-%s", shortName)) + switch runtime.GOOS { + case "windows": + outFile = outFile + ".exe" + } + buildCmd := exec.Command("go", "build", "-o", outFile) + buildCmd.Dir = providerDir + // TODO: constrain env here to make it a little safer? + _, err = runCmd(buildCmd) + if err != nil { + return nil, fmt.Errorf("unable to execute go build command: %w", err) + } + + err = writeFile(filepath.Join(tmpDir, "provider.tf"), fmt.Sprintf(` +provider %[1]q { +} +`, shortName)) + if err != nil { + return nil, fmt.Errorf("unable to write provider.tf file: %w", err) + } + + i := install.NewInstaller() + var sources []src.Source + if tfVersion != "" { + l.infof("downloading Terraform CLI binary version from releases.hashicorp.com: %s", tfVersion) + sources = []src.Source{ + &releases.ExactVersion{ + Product: product.Terraform, + Version: version.Must(version.NewVersion(tfVersion)), + InstallDir: tmpDir, + }, + } + } else { + l.infof("using Terraform CLI binary from PATH if available, otherwise downloading latest Terraform CLI binary") + sources = []src.Source{ + &fs.AnyVersion{ + Product: &product.Terraform, + }, + &checkpoint.LatestVersion{ + InstallDir: tmpDir, + Product: product.Terraform, + }, + } + } + + tfBin, err := i.Ensure(context.Background(), sources) + if err != nil { + return nil, fmt.Errorf("unable to download Terraform binary: %w", err) + } + + tf, err := tfexec.NewTerraform(tmpDir, tfBin) + if err != nil { + return nil, fmt.Errorf("unable to create new terraform exec instance: %w", err) + } + + l.infof("running terraform init") + err = tf.Init(ctx, tfexec.Get(false), tfexec.PluginDir("./plugins")) + if err != nil { + return nil, fmt.Errorf("unable to run terraform init on provider: %w", err) + } + + l.infof("getting provider schema") + schemas, err := tf.ProvidersSchema(ctx) + if err != nil { + return nil, fmt.Errorf("unable to retrieve provider schema from terraform exec: %w", err) + } + + if ps, ok := schemas.Schemas[shortName]; ok { + return ps, nil + } + + if ps, ok := schemas.Schemas["registry.terraform.io/hashicorp/"+shortName]; ok { + return ps, nil + } + + return nil, fmt.Errorf("unable to find schema in JSON for provider %q", shortName) +} + +func TerraformProviderSchemaFromFile(providerName, providersSchemaPath string, l *Logger) (*tfjson.ProviderSchema, error) { + var err error + + shortName := providerShortName(providerName) + + l.infof("getting provider schema") + schemas, err := extractSchemaFromFile(providersSchemaPath) + if err != nil { + return nil, fmt.Errorf("unable to retrieve provider schema from JSON file: %w", err) + } + + if ps, ok := schemas.Schemas[shortName]; ok { + return ps, nil + } + + if ps, ok := schemas.Schemas["registry.terraform.io/hashicorp/"+shortName]; ok { + return ps, nil + } + + return nil, fmt.Errorf("unable to find schema in JSON for provider %q", shortName) +} diff --git a/internal/provider/testdata/invalid-legacy-directories/website/docs/r/invalid/thing.html.markdown b/internal/provider/testdata/invalid-legacy-directories/website/docs/r/invalid/thing.html.markdown new file mode 100644 index 00000000..59bb04cc --- /dev/null +++ b/internal/provider/testdata/invalid-legacy-directories/website/docs/r/invalid/thing.html.markdown @@ -0,0 +1,27 @@ +--- +subcategory: "Example" +layout: "example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Resource: example_thing + +Byline. + +## Example Usage + +```terraform +resource "example_thing" "example" { + name = "example" +} +``` + +## Argument Reference + +* `name` - (Required) Name of thing. + +## Attribute Reference + +* `id` - Name of thing. diff --git a/internal/provider/testdata/invalid-legacy-files/data_source_invalid_extension.txt b/internal/provider/testdata/invalid-legacy-files/data_source_invalid_extension.txt new file mode 100644 index 00000000..caee79a4 --- /dev/null +++ b/internal/provider/testdata/invalid-legacy-files/data_source_invalid_extension.txt @@ -0,0 +1,27 @@ +--- +subcategory: "Example" +layout: "example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Data Source: example_thing + +Byline. + +## Example Usage + +```terraform +data "example_thing" "example" { + name = "example" +} +``` + +## Argument Reference + +* `name` - (Required) Name of thing. + +## Attribute Reference + +* `id` - Name of thing. diff --git a/internal/provider/testdata/invalid-legacy-files/data_source_invalid_frontmatter.html.markdown b/internal/provider/testdata/invalid-legacy-files/data_source_invalid_frontmatter.html.markdown new file mode 100644 index 00000000..1cb8750a --- /dev/null +++ b/internal/provider/testdata/invalid-legacy-files/data_source_invalid_frontmatter.html.markdown @@ -0,0 +1,27 @@ +--- +subcategory: "Example" +layout: "example" +page_title: "Example: example_thing" +description: |- +Missing indentation. +--- + +# Data Source: example_thing + +Byline. + +## Example Usage + +```terraform +data "example_thing" "example" { + name = "example" +} +``` + +## Argument Reference + +* `name` - (Required) Name of thing. + +## Attribute Reference + +* `id` - Name of thing. diff --git a/internal/provider/testdata/invalid-legacy-files/data_source_with_sidebar_current.html.markdown b/internal/provider/testdata/invalid-legacy-files/data_source_with_sidebar_current.html.markdown new file mode 100644 index 00000000..510d48ae --- /dev/null +++ b/internal/provider/testdata/invalid-legacy-files/data_source_with_sidebar_current.html.markdown @@ -0,0 +1,28 @@ +--- +subcategory: "Example" +sidebar_current: "example_thing" +layout: "example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Data Source: example_thing + +Byline. + +## Example Usage + +```terraform +data "example_thing" "example" { + name = "example" +} +``` + +## Argument Reference + +* `name` - (Required) Name of thing. + +## Attribute Reference + +* `id` - Name of thing. diff --git a/internal/provider/testdata/invalid-legacy-files/data_source_without_layout.html.markdown b/internal/provider/testdata/invalid-legacy-files/data_source_without_layout.html.markdown new file mode 100644 index 00000000..688fa598 --- /dev/null +++ b/internal/provider/testdata/invalid-legacy-files/data_source_without_layout.html.markdown @@ -0,0 +1,26 @@ +--- +subcategory: "Example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Data Source: example_thing + +Byline. + +## Example Usage + +```terraform +data "example_thing" "example" { + name = "example" +} +``` + +## Argument Reference + +* `name` - (Required) Name of thing. + +## Attribute Reference + +* `id` - Name of thing. diff --git a/internal/provider/testdata/invalid-legacy-files/guide_invalid_extension.txt b/internal/provider/testdata/invalid-legacy-files/guide_invalid_extension.txt new file mode 100644 index 00000000..b02bcc8e --- /dev/null +++ b/internal/provider/testdata/invalid-legacy-files/guide_invalid_extension.txt @@ -0,0 +1,11 @@ +--- +subcategory: "Example" +layout: "example" +page_title: "Example Guide" +description: |- + Example description. +--- + +# Example Guide + +Example contents. diff --git a/internal/provider/testdata/invalid-legacy-files/guide_invalid_frontmatter.html.markdown b/internal/provider/testdata/invalid-legacy-files/guide_invalid_frontmatter.html.markdown new file mode 100644 index 00000000..33997fc6 --- /dev/null +++ b/internal/provider/testdata/invalid-legacy-files/guide_invalid_frontmatter.html.markdown @@ -0,0 +1,11 @@ +--- +subcategory: "Example" +layout: "example" +page_title: "Example Guide" +description: |- +Missing indentation. +--- + +# Example Guide + +Example contents. diff --git a/internal/provider/testdata/invalid-legacy-files/guide_with_sidebar_current.html.markdown b/internal/provider/testdata/invalid-legacy-files/guide_with_sidebar_current.html.markdown new file mode 100644 index 00000000..5a101a3c --- /dev/null +++ b/internal/provider/testdata/invalid-legacy-files/guide_with_sidebar_current.html.markdown @@ -0,0 +1,12 @@ +--- +subcategory: "Example" +sidebar_current: "example" +layout: "example" +page_title: "Example Guide" +description: |- + Example description. +--- + +# Example Guide + +Example contents. diff --git a/internal/provider/testdata/invalid-legacy-files/guide_without_layout.html.markdown b/internal/provider/testdata/invalid-legacy-files/guide_without_layout.html.markdown new file mode 100644 index 00000000..61331ef0 --- /dev/null +++ b/internal/provider/testdata/invalid-legacy-files/guide_without_layout.html.markdown @@ -0,0 +1,10 @@ +--- +subcategory: "Example" +page_title: "Example Guide" +description: |- + Example description. +--- + +# Example Guide + +Example contents. diff --git a/internal/provider/testdata/invalid-legacy-files/index_invalid_extension.txt b/internal/provider/testdata/invalid-legacy-files/index_invalid_extension.txt new file mode 100644 index 00000000..c4ab1478 --- /dev/null +++ b/internal/provider/testdata/invalid-legacy-files/index_invalid_extension.txt @@ -0,0 +1,10 @@ +--- +layout: "example" +page_title: "Example Provider" +description: |- + Example description. +--- + +# Example Provider + +Example contents. diff --git a/internal/provider/testdata/invalid-legacy-files/index_invalid_frontmatter.html.markdown b/internal/provider/testdata/invalid-legacy-files/index_invalid_frontmatter.html.markdown new file mode 100644 index 00000000..c4903d5a --- /dev/null +++ b/internal/provider/testdata/invalid-legacy-files/index_invalid_frontmatter.html.markdown @@ -0,0 +1,10 @@ +--- +layout: "example" +page_title: "Example Provider" +description: |- +Missing indentation. +--- + +# Example Provider + +Example contents. diff --git a/internal/provider/testdata/invalid-legacy-files/index_with_sidebar_current.html.markdown b/internal/provider/testdata/invalid-legacy-files/index_with_sidebar_current.html.markdown new file mode 100644 index 00000000..fe33d3f2 --- /dev/null +++ b/internal/provider/testdata/invalid-legacy-files/index_with_sidebar_current.html.markdown @@ -0,0 +1,11 @@ +--- +sidebar_current: "example" +layout: "example" +page_title: "Example Provider" +description: |- + Example description. +--- + +# Example Provider + +Example contents. diff --git a/internal/provider/testdata/invalid-legacy-files/index_with_subcategory.html.markdown b/internal/provider/testdata/invalid-legacy-files/index_with_subcategory.html.markdown new file mode 100644 index 00000000..9766b6df --- /dev/null +++ b/internal/provider/testdata/invalid-legacy-files/index_with_subcategory.html.markdown @@ -0,0 +1,10 @@ +--- +subcategory: "Example" +page_title: "Example Provider" +description: |- + Example description. +--- + +# Example Provider + +Example contents. diff --git a/internal/provider/testdata/invalid-legacy-files/index_without_layout.html.markdown b/internal/provider/testdata/invalid-legacy-files/index_without_layout.html.markdown new file mode 100644 index 00000000..9daab155 --- /dev/null +++ b/internal/provider/testdata/invalid-legacy-files/index_without_layout.html.markdown @@ -0,0 +1,9 @@ +--- +page_title: "Example Provider" +description: |- + Example description. +--- + +# Example Provider + +Example contents. diff --git a/internal/provider/testdata/invalid-legacy-files/resource_invalid_extension.txt b/internal/provider/testdata/invalid-legacy-files/resource_invalid_extension.txt new file mode 100644 index 00000000..59bb04cc --- /dev/null +++ b/internal/provider/testdata/invalid-legacy-files/resource_invalid_extension.txt @@ -0,0 +1,27 @@ +--- +subcategory: "Example" +layout: "example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Resource: example_thing + +Byline. + +## Example Usage + +```terraform +resource "example_thing" "example" { + name = "example" +} +``` + +## Argument Reference + +* `name` - (Required) Name of thing. + +## Attribute Reference + +* `id` - Name of thing. diff --git a/internal/provider/testdata/invalid-legacy-files/resource_invalid_frontmatter.html.markdown b/internal/provider/testdata/invalid-legacy-files/resource_invalid_frontmatter.html.markdown new file mode 100644 index 00000000..0cd796a3 --- /dev/null +++ b/internal/provider/testdata/invalid-legacy-files/resource_invalid_frontmatter.html.markdown @@ -0,0 +1,27 @@ +--- +subcategory: "Example" +layout: "example" +page_title: "Example: example_thing" +description: |- +Missing indentation. +--- + +# Resource: example_thing + +Byline. + +## Example Usage + +```terraform +resource "example_thing" "example" { + name = "example" +} +``` + +## Argument Reference + +* `name` - (Required) Name of thing. + +## Attribute Reference + +* `id` - Name of thing. diff --git a/internal/provider/testdata/invalid-legacy-files/resource_with_sidebar_current.html.markdown b/internal/provider/testdata/invalid-legacy-files/resource_with_sidebar_current.html.markdown new file mode 100644 index 00000000..32ec4684 --- /dev/null +++ b/internal/provider/testdata/invalid-legacy-files/resource_with_sidebar_current.html.markdown @@ -0,0 +1,28 @@ +--- +subcategory: "Example" +sidebar_current: "example_thing" +layout: "example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Resource: example_thing + +Byline. + +## Example Usage + +```terraform +resource "example_thing" "example" { + name = "example" +} +``` + +## Argument Reference + +* `name` - (Required) Name of thing. + +## Attribute Reference + +* `id` - Name of thing. diff --git a/internal/provider/testdata/invalid-legacy-files/resource_without_layout.html.markdown b/internal/provider/testdata/invalid-legacy-files/resource_without_layout.html.markdown new file mode 100644 index 00000000..aa156f35 --- /dev/null +++ b/internal/provider/testdata/invalid-legacy-files/resource_without_layout.html.markdown @@ -0,0 +1,26 @@ +--- +subcategory: "Example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Resource: example_thing + +Byline. + +## Example Usage + +```terraform +resource "example_thing" "example" { + name = "example" +} +``` + +## Argument Reference + +* `name` - (Required) Name of thing. + +## Attribute Reference + +* `id` - Name of thing. diff --git a/internal/provider/testdata/invalid-registry-directories/docs/resources/invalid/thing.md b/internal/provider/testdata/invalid-registry-directories/docs/resources/invalid/thing.md new file mode 100644 index 00000000..aa156f35 --- /dev/null +++ b/internal/provider/testdata/invalid-registry-directories/docs/resources/invalid/thing.md @@ -0,0 +1,26 @@ +--- +subcategory: "Example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Resource: example_thing + +Byline. + +## Example Usage + +```terraform +resource "example_thing" "example" { + name = "example" +} +``` + +## Argument Reference + +* `name` - (Required) Name of thing. + +## Attribute Reference + +* `id` - Name of thing. diff --git a/internal/provider/testdata/invalid-registry-files/data_source_invalid_extension.markdown b/internal/provider/testdata/invalid-registry-files/data_source_invalid_extension.markdown new file mode 100644 index 00000000..688fa598 --- /dev/null +++ b/internal/provider/testdata/invalid-registry-files/data_source_invalid_extension.markdown @@ -0,0 +1,26 @@ +--- +subcategory: "Example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Data Source: example_thing + +Byline. + +## Example Usage + +```terraform +data "example_thing" "example" { + name = "example" +} +``` + +## Argument Reference + +* `name` - (Required) Name of thing. + +## Attribute Reference + +* `id` - Name of thing. diff --git a/internal/provider/testdata/invalid-registry-files/data_source_invalid_frontmatter.md b/internal/provider/testdata/invalid-registry-files/data_source_invalid_frontmatter.md new file mode 100644 index 00000000..224dcf7e --- /dev/null +++ b/internal/provider/testdata/invalid-registry-files/data_source_invalid_frontmatter.md @@ -0,0 +1,26 @@ +--- +subcategory: "Example" +page_title: "Example: example_thing" +description: |- +Missing indentation. +--- + +# Data Source: example_thing + +Byline. + +## Example Usage + +```terraform +data "example_thing" "example" { + name = "example" +} +``` + +## Argument Reference + +* `name` - (Required) Name of thing. + +## Attribute Reference + +* `id` - Name of thing. diff --git a/internal/provider/testdata/invalid-registry-files/data_source_with_layout.md b/internal/provider/testdata/invalid-registry-files/data_source_with_layout.md new file mode 100644 index 00000000..caee79a4 --- /dev/null +++ b/internal/provider/testdata/invalid-registry-files/data_source_with_layout.md @@ -0,0 +1,27 @@ +--- +subcategory: "Example" +layout: "example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Data Source: example_thing + +Byline. + +## Example Usage + +```terraform +data "example_thing" "example" { + name = "example" +} +``` + +## Argument Reference + +* `name` - (Required) Name of thing. + +## Attribute Reference + +* `id` - Name of thing. diff --git a/internal/provider/testdata/invalid-registry-files/data_source_with_sidebar_current.md b/internal/provider/testdata/invalid-registry-files/data_source_with_sidebar_current.md new file mode 100644 index 00000000..afc1af9e --- /dev/null +++ b/internal/provider/testdata/invalid-registry-files/data_source_with_sidebar_current.md @@ -0,0 +1,27 @@ +--- +subcategory: "Example" +sidebar_current: "example_thing" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Data Source: example_thing + +Byline. + +## Example Usage + +```terraform +data "example_thing" "example" { + name = "example" +} +``` + +## Argument Reference + +* `name` - (Required) Name of thing. + +## Attribute Reference + +* `id` - Name of thing. diff --git a/internal/provider/testdata/invalid-registry-files/guide_invalid_extension.markdown b/internal/provider/testdata/invalid-registry-files/guide_invalid_extension.markdown new file mode 100644 index 00000000..9acc401d --- /dev/null +++ b/internal/provider/testdata/invalid-registry-files/guide_invalid_extension.markdown @@ -0,0 +1,10 @@ +--- +subcategory: "Example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Example Guide + +Example contents. diff --git a/internal/provider/testdata/invalid-registry-files/guide_invalid_frontmatter.md b/internal/provider/testdata/invalid-registry-files/guide_invalid_frontmatter.md new file mode 100644 index 00000000..1dbe9583 --- /dev/null +++ b/internal/provider/testdata/invalid-registry-files/guide_invalid_frontmatter.md @@ -0,0 +1,10 @@ +--- +subcategory: "Example" +page_title: "Example: example_thing" +description: |- +Missing indentation. +--- + +# Example Guide + +Example contents. diff --git a/internal/provider/testdata/invalid-registry-files/guide_with_layout.md b/internal/provider/testdata/invalid-registry-files/guide_with_layout.md new file mode 100644 index 00000000..ff7efb92 --- /dev/null +++ b/internal/provider/testdata/invalid-registry-files/guide_with_layout.md @@ -0,0 +1,11 @@ +--- +subcategory: "Example" +layout: "example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Example Guide + +Example contents. diff --git a/internal/provider/testdata/invalid-registry-files/guide_with_sidebar_current.md b/internal/provider/testdata/invalid-registry-files/guide_with_sidebar_current.md new file mode 100644 index 00000000..1954289d --- /dev/null +++ b/internal/provider/testdata/invalid-registry-files/guide_with_sidebar_current.md @@ -0,0 +1,11 @@ +--- +subcategory: "Example" +sidebar_current: "example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Example Guide + +Example contents. diff --git a/internal/provider/testdata/invalid-registry-files/index_invalid_extension.markdown b/internal/provider/testdata/invalid-registry-files/index_invalid_extension.markdown new file mode 100644 index 00000000..9daab155 --- /dev/null +++ b/internal/provider/testdata/invalid-registry-files/index_invalid_extension.markdown @@ -0,0 +1,9 @@ +--- +page_title: "Example Provider" +description: |- + Example description. +--- + +# Example Provider + +Example contents. diff --git a/internal/provider/testdata/invalid-registry-files/index_invalid_frontmatter.md b/internal/provider/testdata/invalid-registry-files/index_invalid_frontmatter.md new file mode 100644 index 00000000..f19cf719 --- /dev/null +++ b/internal/provider/testdata/invalid-registry-files/index_invalid_frontmatter.md @@ -0,0 +1,9 @@ +--- +page_title: "Example Provider" +description: |- +Missing indentation. +--- + +# Example Provider + +Example contents. diff --git a/internal/provider/testdata/invalid-registry-files/index_with_layout.md b/internal/provider/testdata/invalid-registry-files/index_with_layout.md new file mode 100644 index 00000000..c4ab1478 --- /dev/null +++ b/internal/provider/testdata/invalid-registry-files/index_with_layout.md @@ -0,0 +1,10 @@ +--- +layout: "example" +page_title: "Example Provider" +description: |- + Example description. +--- + +# Example Provider + +Example contents. diff --git a/internal/provider/testdata/invalid-registry-files/index_with_sidebar_current.md b/internal/provider/testdata/invalid-registry-files/index_with_sidebar_current.md new file mode 100644 index 00000000..cd0a42c5 --- /dev/null +++ b/internal/provider/testdata/invalid-registry-files/index_with_sidebar_current.md @@ -0,0 +1,10 @@ +--- +sidebar_current: "example" +page_title: "Example Provider" +description: |- + Example description. +--- + +# Example Provider + +Example contents. diff --git a/internal/provider/testdata/invalid-registry-files/index_with_subcategory.md b/internal/provider/testdata/invalid-registry-files/index_with_subcategory.md new file mode 100644 index 00000000..eb4751c9 --- /dev/null +++ b/internal/provider/testdata/invalid-registry-files/index_with_subcategory.md @@ -0,0 +1,10 @@ +--- +subcategory: "example" +page_title: "Example Provider" +description: |- + Example description. +--- + +# Example Provider + +Example contents. diff --git a/internal/provider/testdata/invalid-registry-files/resource_invalid_extension.markdown b/internal/provider/testdata/invalid-registry-files/resource_invalid_extension.markdown new file mode 100644 index 00000000..aa156f35 --- /dev/null +++ b/internal/provider/testdata/invalid-registry-files/resource_invalid_extension.markdown @@ -0,0 +1,26 @@ +--- +subcategory: "Example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Resource: example_thing + +Byline. + +## Example Usage + +```terraform +resource "example_thing" "example" { + name = "example" +} +``` + +## Argument Reference + +* `name` - (Required) Name of thing. + +## Attribute Reference + +* `id` - Name of thing. diff --git a/internal/provider/testdata/invalid-registry-files/resource_invalid_frontmatter.md b/internal/provider/testdata/invalid-registry-files/resource_invalid_frontmatter.md new file mode 100644 index 00000000..39a2823b --- /dev/null +++ b/internal/provider/testdata/invalid-registry-files/resource_invalid_frontmatter.md @@ -0,0 +1,26 @@ +--- +subcategory: "Example" +page_title: "Example: example_thing" +description: |- +Missing indentation. +--- + +# Resource: example_thing + +Byline. + +## Example Usage + +```terraform +resource "example_thing" "example" { + name = "example" +} +``` + +## Argument Reference + +* `name` - (Required) Name of thing. + +## Attribute Reference + +* `id` - Name of thing. diff --git a/internal/provider/testdata/invalid-registry-files/resource_with_layout.md b/internal/provider/testdata/invalid-registry-files/resource_with_layout.md new file mode 100644 index 00000000..59bb04cc --- /dev/null +++ b/internal/provider/testdata/invalid-registry-files/resource_with_layout.md @@ -0,0 +1,27 @@ +--- +subcategory: "Example" +layout: "example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Resource: example_thing + +Byline. + +## Example Usage + +```terraform +resource "example_thing" "example" { + name = "example" +} +``` + +## Argument Reference + +* `name` - (Required) Name of thing. + +## Attribute Reference + +* `id` - Name of thing. diff --git a/internal/provider/testdata/invalid-registry-files/resource_with_sidebar_current.md b/internal/provider/testdata/invalid-registry-files/resource_with_sidebar_current.md new file mode 100644 index 00000000..91e6dcbb --- /dev/null +++ b/internal/provider/testdata/invalid-registry-files/resource_with_sidebar_current.md @@ -0,0 +1,27 @@ +--- +subcategory: "Example" +sidebar_current: "example_thing" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Resource: example_thing + +Byline. + +## Example Usage + +```terraform +resource "example_thing" "example" { + name = "example" +} +``` + +## Argument Reference + +* `name` - (Required) Name of thing. + +## Attribute Reference + +* `id` - Name of thing. diff --git a/internal/provider/testdata/valid-legacy-directories-with-cdktf/website/docs/cdktf/typescript/d/thing.html.markdown b/internal/provider/testdata/valid-legacy-directories-with-cdktf/website/docs/cdktf/typescript/d/thing.html.markdown new file mode 100644 index 00000000..32e9de67 --- /dev/null +++ b/internal/provider/testdata/valid-legacy-directories-with-cdktf/website/docs/cdktf/typescript/d/thing.html.markdown @@ -0,0 +1,37 @@ +--- +subcategory: "Example" +layout: "example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Data Source: example_thing + +Byline. + +## Example Usage + +```ts +import { Construct } from "construct"; +import { TerraformStack } from "cdktf"; +import { DataExample } from "./.gen/providers/example/data_example_thing"; + +class MyStack extends TerraformStack { + constructs(scope: Construct, name: string) { + super(scope, name); + + new DataExampleThing(this, "example", { + name: "example", + }); + } +} +``` + +## Argument Reference + +- `name` - (Required) Name of thing. + +## Attribute Reference + +- `id` - Name of thing. diff --git a/internal/provider/testdata/valid-legacy-directories-with-cdktf/website/docs/cdktf/typescript/r/thing.html.markdown b/internal/provider/testdata/valid-legacy-directories-with-cdktf/website/docs/cdktf/typescript/r/thing.html.markdown new file mode 100644 index 00000000..962ae86b --- /dev/null +++ b/internal/provider/testdata/valid-legacy-directories-with-cdktf/website/docs/cdktf/typescript/r/thing.html.markdown @@ -0,0 +1,37 @@ +--- +subcategory: "Example" +layout: "example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Resource: example_thing + +Byline. + +## Example Usage + +```ts +import { Construct } from "construct"; +import { TerraformStack } from "cdktf"; +import { Thing } from "./.gen/providers/example/thing"; + +class MyStack extends TerraformStack { + constructs(scope: Construct, name: string) { + super(scope, name); + + new Thing(this, "example", { + name: "example", + }); + } +} +``` + +## Argument Reference + +- `name` - (Required) Name of thing. + +## Attribute Reference + +- `id` - Name of thing. diff --git a/internal/provider/testdata/valid-legacy-directories-with-cdktf/website/docs/d/thing.html.markdown b/internal/provider/testdata/valid-legacy-directories-with-cdktf/website/docs/d/thing.html.markdown new file mode 100644 index 00000000..caee79a4 --- /dev/null +++ b/internal/provider/testdata/valid-legacy-directories-with-cdktf/website/docs/d/thing.html.markdown @@ -0,0 +1,27 @@ +--- +subcategory: "Example" +layout: "example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Data Source: example_thing + +Byline. + +## Example Usage + +```terraform +data "example_thing" "example" { + name = "example" +} +``` + +## Argument Reference + +* `name` - (Required) Name of thing. + +## Attribute Reference + +* `id` - Name of thing. diff --git a/internal/provider/testdata/valid-legacy-directories-with-cdktf/website/docs/r/thing.html.markdown b/internal/provider/testdata/valid-legacy-directories-with-cdktf/website/docs/r/thing.html.markdown new file mode 100644 index 00000000..59bb04cc --- /dev/null +++ b/internal/provider/testdata/valid-legacy-directories-with-cdktf/website/docs/r/thing.html.markdown @@ -0,0 +1,27 @@ +--- +subcategory: "Example" +layout: "example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Resource: example_thing + +Byline. + +## Example Usage + +```terraform +resource "example_thing" "example" { + name = "example" +} +``` + +## Argument Reference + +* `name` - (Required) Name of thing. + +## Attribute Reference + +* `id` - Name of thing. diff --git a/internal/provider/testdata/valid-legacy-directories/website/docs/d/thing.html.markdown b/internal/provider/testdata/valid-legacy-directories/website/docs/d/thing.html.markdown new file mode 100644 index 00000000..caee79a4 --- /dev/null +++ b/internal/provider/testdata/valid-legacy-directories/website/docs/d/thing.html.markdown @@ -0,0 +1,27 @@ +--- +subcategory: "Example" +layout: "example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Data Source: example_thing + +Byline. + +## Example Usage + +```terraform +data "example_thing" "example" { + name = "example" +} +``` + +## Argument Reference + +* `name` - (Required) Name of thing. + +## Attribute Reference + +* `id` - Name of thing. diff --git a/internal/provider/testdata/valid-legacy-directories/website/docs/r/thing.html.markdown b/internal/provider/testdata/valid-legacy-directories/website/docs/r/thing.html.markdown new file mode 100644 index 00000000..59bb04cc --- /dev/null +++ b/internal/provider/testdata/valid-legacy-directories/website/docs/r/thing.html.markdown @@ -0,0 +1,27 @@ +--- +subcategory: "Example" +layout: "example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Resource: example_thing + +Byline. + +## Example Usage + +```terraform +resource "example_thing" "example" { + name = "example" +} +``` + +## Argument Reference + +* `name` - (Required) Name of thing. + +## Attribute Reference + +* `id` - Name of thing. diff --git a/internal/provider/testdata/valid-legacy-files/2.0-guide.html.markdown b/internal/provider/testdata/valid-legacy-files/2.0-guide.html.markdown new file mode 100644 index 00000000..b02bcc8e --- /dev/null +++ b/internal/provider/testdata/valid-legacy-files/2.0-guide.html.markdown @@ -0,0 +1,11 @@ +--- +subcategory: "Example" +layout: "example" +page_title: "Example Guide" +description: |- + Example description. +--- + +# Example Guide + +Example contents. diff --git a/internal/provider/testdata/valid-legacy-files/data_source.html.markdown b/internal/provider/testdata/valid-legacy-files/data_source.html.markdown new file mode 100644 index 00000000..caee79a4 --- /dev/null +++ b/internal/provider/testdata/valid-legacy-files/data_source.html.markdown @@ -0,0 +1,27 @@ +--- +subcategory: "Example" +layout: "example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Data Source: example_thing + +Byline. + +## Example Usage + +```terraform +data "example_thing" "example" { + name = "example" +} +``` + +## Argument Reference + +* `name` - (Required) Name of thing. + +## Attribute Reference + +* `id` - Name of thing. diff --git a/internal/provider/testdata/valid-legacy-files/guide.html.markdown b/internal/provider/testdata/valid-legacy-files/guide.html.markdown new file mode 100644 index 00000000..b02bcc8e --- /dev/null +++ b/internal/provider/testdata/valid-legacy-files/guide.html.markdown @@ -0,0 +1,11 @@ +--- +subcategory: "Example" +layout: "example" +page_title: "Example Guide" +description: |- + Example description. +--- + +# Example Guide + +Example contents. diff --git a/internal/provider/testdata/valid-legacy-files/index.html.markdown b/internal/provider/testdata/valid-legacy-files/index.html.markdown new file mode 100644 index 00000000..c4ab1478 --- /dev/null +++ b/internal/provider/testdata/valid-legacy-files/index.html.markdown @@ -0,0 +1,10 @@ +--- +layout: "example" +page_title: "Example Provider" +description: |- + Example description. +--- + +# Example Provider + +Example contents. diff --git a/internal/provider/testdata/valid-legacy-files/resource.html.markdown b/internal/provider/testdata/valid-legacy-files/resource.html.markdown new file mode 100644 index 00000000..59bb04cc --- /dev/null +++ b/internal/provider/testdata/valid-legacy-files/resource.html.markdown @@ -0,0 +1,27 @@ +--- +subcategory: "Example" +layout: "example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Resource: example_thing + +Byline. + +## Example Usage + +```terraform +resource "example_thing" "example" { + name = "example" +} +``` + +## Argument Reference + +* `name` - (Required) Name of thing. + +## Attribute Reference + +* `id` - Name of thing. diff --git a/internal/provider/testdata/valid-registry-directories-with-cdktf/docs/CONTRIBUTING.md b/internal/provider/testdata/valid-registry-directories-with-cdktf/docs/CONTRIBUTING.md new file mode 100644 index 00000000..423305c4 --- /dev/null +++ b/internal/provider/testdata/valid-registry-directories-with-cdktf/docs/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# Contributing Guide + +This file has contents and no YAML frontmatter, because it is not a Terraform Provider documentation file and that is okay. diff --git a/internal/provider/testdata/valid-registry-directories-with-cdktf/docs/cdktf/typescript/CONTRIBUTING.md b/internal/provider/testdata/valid-registry-directories-with-cdktf/docs/cdktf/typescript/CONTRIBUTING.md new file mode 100644 index 00000000..423305c4 --- /dev/null +++ b/internal/provider/testdata/valid-registry-directories-with-cdktf/docs/cdktf/typescript/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# Contributing Guide + +This file has contents and no YAML frontmatter, because it is not a Terraform Provider documentation file and that is okay. diff --git a/internal/provider/testdata/valid-registry-directories-with-cdktf/docs/cdktf/typescript/data-sources/thing.md b/internal/provider/testdata/valid-registry-directories-with-cdktf/docs/cdktf/typescript/data-sources/thing.md new file mode 100644 index 00000000..5c415dba --- /dev/null +++ b/internal/provider/testdata/valid-registry-directories-with-cdktf/docs/cdktf/typescript/data-sources/thing.md @@ -0,0 +1,36 @@ +--- +subcategory: "Example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Data Source: example_thing + +Byline. + +## Example Usage + +```ts +import { Construct } from "construct"; +import { TerraformStack } from "cdktf"; +import { DataExample } from "./.gen/providers/example/data_example_thing"; + +class MyStack extends TerraformStack { + constructs(scope: Construct, name: string) { + super(scope, name); + + new DataExampleThing(this, "example", { + name: "example", + }); + } +} +``` + +## Argument Reference + +- `name` - (Required) Name of thing. + +## Attribute Reference + +- `id` - Name of thing. diff --git a/internal/provider/testdata/valid-registry-directories-with-cdktf/docs/cdktf/typescript/index.md b/internal/provider/testdata/valid-registry-directories-with-cdktf/docs/cdktf/typescript/index.md new file mode 100644 index 00000000..9daab155 --- /dev/null +++ b/internal/provider/testdata/valid-registry-directories-with-cdktf/docs/cdktf/typescript/index.md @@ -0,0 +1,9 @@ +--- +page_title: "Example Provider" +description: |- + Example description. +--- + +# Example Provider + +Example contents. diff --git a/internal/provider/testdata/valid-registry-directories-with-cdktf/docs/cdktf/typescript/nonregistrydocs/valid.md b/internal/provider/testdata/valid-registry-directories-with-cdktf/docs/cdktf/typescript/nonregistrydocs/valid.md new file mode 100644 index 00000000..a7f1fce5 --- /dev/null +++ b/internal/provider/testdata/valid-registry-directories-with-cdktf/docs/cdktf/typescript/nonregistrydocs/valid.md @@ -0,0 +1,3 @@ +# Valid + +Files in `/docs`, but outside Registry documentation directories, should be ignored. diff --git a/internal/provider/testdata/valid-registry-directories-with-cdktf/docs/cdktf/typescript/resources/thing.md b/internal/provider/testdata/valid-registry-directories-with-cdktf/docs/cdktf/typescript/resources/thing.md new file mode 100644 index 00000000..1f46f48b --- /dev/null +++ b/internal/provider/testdata/valid-registry-directories-with-cdktf/docs/cdktf/typescript/resources/thing.md @@ -0,0 +1,36 @@ +--- +subcategory: "Example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Resource: example_thing + +Byline. + +## Example Usage + +```ts +import { Construct } from "construct"; +import { TerraformStack } from "cdktf"; +import { Thing } from "./.gen/providers/example/thing"; + +class MyStack extends TerraformStack { + constructs(scope: Construct, name: string) { + super(scope, name); + + new Thing(this, "example", { + name: "example", + }); + } +} +``` + +## Argument Reference + +- `name` - (Required) Name of thing. + +## Attribute Reference + +- `id` - Name of thing. diff --git a/internal/provider/testdata/valid-registry-directories-with-cdktf/docs/data-sources/thing.md b/internal/provider/testdata/valid-registry-directories-with-cdktf/docs/data-sources/thing.md new file mode 100644 index 00000000..688fa598 --- /dev/null +++ b/internal/provider/testdata/valid-registry-directories-with-cdktf/docs/data-sources/thing.md @@ -0,0 +1,26 @@ +--- +subcategory: "Example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Data Source: example_thing + +Byline. + +## Example Usage + +```terraform +data "example_thing" "example" { + name = "example" +} +``` + +## Argument Reference + +* `name` - (Required) Name of thing. + +## Attribute Reference + +* `id` - Name of thing. diff --git a/internal/provider/testdata/valid-registry-directories-with-cdktf/docs/index.md b/internal/provider/testdata/valid-registry-directories-with-cdktf/docs/index.md new file mode 100644 index 00000000..9daab155 --- /dev/null +++ b/internal/provider/testdata/valid-registry-directories-with-cdktf/docs/index.md @@ -0,0 +1,9 @@ +--- +page_title: "Example Provider" +description: |- + Example description. +--- + +# Example Provider + +Example contents. diff --git a/internal/provider/testdata/valid-registry-directories-with-cdktf/docs/nonregistrydocs/valid.md b/internal/provider/testdata/valid-registry-directories-with-cdktf/docs/nonregistrydocs/valid.md new file mode 100644 index 00000000..a7f1fce5 --- /dev/null +++ b/internal/provider/testdata/valid-registry-directories-with-cdktf/docs/nonregistrydocs/valid.md @@ -0,0 +1,3 @@ +# Valid + +Files in `/docs`, but outside Registry documentation directories, should be ignored. diff --git a/internal/provider/testdata/valid-registry-directories-with-cdktf/docs/resources/thing.md b/internal/provider/testdata/valid-registry-directories-with-cdktf/docs/resources/thing.md new file mode 100644 index 00000000..aa156f35 --- /dev/null +++ b/internal/provider/testdata/valid-registry-directories-with-cdktf/docs/resources/thing.md @@ -0,0 +1,26 @@ +--- +subcategory: "Example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Resource: example_thing + +Byline. + +## Example Usage + +```terraform +resource "example_thing" "example" { + name = "example" +} +``` + +## Argument Reference + +* `name` - (Required) Name of thing. + +## Attribute Reference + +* `id` - Name of thing. diff --git a/internal/provider/testdata/valid-registry-directories/docs/CONTRIBUTING.md b/internal/provider/testdata/valid-registry-directories/docs/CONTRIBUTING.md new file mode 100644 index 00000000..423305c4 --- /dev/null +++ b/internal/provider/testdata/valid-registry-directories/docs/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# Contributing Guide + +This file has contents and no YAML frontmatter, because it is not a Terraform Provider documentation file and that is okay. diff --git a/internal/provider/testdata/valid-registry-directories/docs/data-sources/thing.md b/internal/provider/testdata/valid-registry-directories/docs/data-sources/thing.md new file mode 100644 index 00000000..688fa598 --- /dev/null +++ b/internal/provider/testdata/valid-registry-directories/docs/data-sources/thing.md @@ -0,0 +1,26 @@ +--- +subcategory: "Example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Data Source: example_thing + +Byline. + +## Example Usage + +```terraform +data "example_thing" "example" { + name = "example" +} +``` + +## Argument Reference + +* `name` - (Required) Name of thing. + +## Attribute Reference + +* `id` - Name of thing. diff --git a/internal/provider/testdata/valid-registry-directories/docs/index.md b/internal/provider/testdata/valid-registry-directories/docs/index.md new file mode 100644 index 00000000..9daab155 --- /dev/null +++ b/internal/provider/testdata/valid-registry-directories/docs/index.md @@ -0,0 +1,9 @@ +--- +page_title: "Example Provider" +description: |- + Example description. +--- + +# Example Provider + +Example contents. diff --git a/internal/provider/testdata/valid-registry-directories/docs/nonregistrydocs/valid.md b/internal/provider/testdata/valid-registry-directories/docs/nonregistrydocs/valid.md new file mode 100644 index 00000000..a7f1fce5 --- /dev/null +++ b/internal/provider/testdata/valid-registry-directories/docs/nonregistrydocs/valid.md @@ -0,0 +1,3 @@ +# Valid + +Files in `/docs`, but outside Registry documentation directories, should be ignored. diff --git a/internal/provider/testdata/valid-registry-directories/docs/resources/thing.md b/internal/provider/testdata/valid-registry-directories/docs/resources/thing.md new file mode 100644 index 00000000..aa156f35 --- /dev/null +++ b/internal/provider/testdata/valid-registry-directories/docs/resources/thing.md @@ -0,0 +1,26 @@ +--- +subcategory: "Example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Resource: example_thing + +Byline. + +## Example Usage + +```terraform +resource "example_thing" "example" { + name = "example" +} +``` + +## Argument Reference + +* `name` - (Required) Name of thing. + +## Attribute Reference + +* `id` - Name of thing. diff --git a/internal/provider/testdata/valid-registry-files/2.0-guide.md b/internal/provider/testdata/valid-registry-files/2.0-guide.md new file mode 100644 index 00000000..9acc401d --- /dev/null +++ b/internal/provider/testdata/valid-registry-files/2.0-guide.md @@ -0,0 +1,10 @@ +--- +subcategory: "Example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Example Guide + +Example contents. diff --git a/internal/provider/testdata/valid-registry-files/data_source.md b/internal/provider/testdata/valid-registry-files/data_source.md new file mode 100644 index 00000000..688fa598 --- /dev/null +++ b/internal/provider/testdata/valid-registry-files/data_source.md @@ -0,0 +1,26 @@ +--- +subcategory: "Example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Data Source: example_thing + +Byline. + +## Example Usage + +```terraform +data "example_thing" "example" { + name = "example" +} +``` + +## Argument Reference + +* `name` - (Required) Name of thing. + +## Attribute Reference + +* `id` - Name of thing. diff --git a/internal/provider/testdata/valid-registry-files/guide.md b/internal/provider/testdata/valid-registry-files/guide.md new file mode 100644 index 00000000..9acc401d --- /dev/null +++ b/internal/provider/testdata/valid-registry-files/guide.md @@ -0,0 +1,10 @@ +--- +subcategory: "Example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Example Guide + +Example contents. diff --git a/internal/provider/testdata/valid-registry-files/index.md b/internal/provider/testdata/valid-registry-files/index.md new file mode 100644 index 00000000..9daab155 --- /dev/null +++ b/internal/provider/testdata/valid-registry-files/index.md @@ -0,0 +1,9 @@ +--- +page_title: "Example Provider" +description: |- + Example description. +--- + +# Example Provider + +Example contents. diff --git a/internal/provider/testdata/valid-registry-files/resource.md b/internal/provider/testdata/valid-registry-files/resource.md new file mode 100644 index 00000000..aa156f35 --- /dev/null +++ b/internal/provider/testdata/valid-registry-files/resource.md @@ -0,0 +1,26 @@ +--- +subcategory: "Example" +page_title: "Example: example_thing" +description: |- + Example description. +--- + +# Resource: example_thing + +Byline. + +## Example Usage + +```terraform +resource "example_thing" "example" { + name = "example" +} +``` + +## Argument Reference + +* `name` - (Required) Name of thing. + +## Attribute Reference + +* `id` - Name of thing. diff --git a/internal/provider/util.go b/internal/provider/util.go index 9bc50528..7a3ec336 100644 --- a/internal/provider/util.go +++ b/internal/provider/util.go @@ -103,6 +103,7 @@ func writeFile(path string, data string) error { return nil } +//nolint:unparam func runCmd(cmd *exec.Cmd) ([]byte, error) { output, err := cmd.CombinedOutput() if err != nil { diff --git a/internal/provider/validate.go b/internal/provider/validate.go index c93ff3ec..6732091e 100644 --- a/internal/provider/validate.go +++ b/internal/provider/validate.go @@ -4,269 +4,363 @@ package provider import ( + "context" + "errors" "fmt" + "log" "os" "path/filepath" - "strings" + "github.com/bmatcuk/doublestar/v4" "github.com/hashicorp/cli" + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-docs/internal/check" ) -func Validate(ui cli.Ui) error { - dirExists := func(name string) bool { - if _, err := os.Stat(name); err != nil { - return false - } +const ( + FileExtensionHtmlMarkdown = `.html.markdown` + FileExtensionHtmlMd = `.html.md` + FileExtensionMarkdown = `.markdown` + FileExtensionMd = `.md` - return true - } + DocumentationGlobPattern = `{docs/index.md,docs/{,cdktf/}{data-sources,guides,resources,functions}/**/*,website/docs/**/*}` + DocumentationDirGlobPattern = `{docs/{,cdktf/}{data-sources,guides,resources,functions}{,/*},website/docs/**/*}` +) - switch { - default: - ui.Warn("no website detected, exiting") - case dirExists("templates"): - ui.Info("detected templates directory, running checks...") - err := validateTemplates(ui, "templates") - if err != nil { - return err - } - if dirExists("examples") { - ui.Info("detected examples directory for templates, running checks...") - err = validateExamples(ui, "examples") - if err != nil { - return err - } - } - return err - case dirExists("docs"): - ui.Info("detected static docs directory, running checks") - return validateStaticDocs(ui, "docs") - case dirExists("website"): - ui.Info("detected legacy website directory, running checks") - return validateLegacyWebsite(ui, "website") - } +var ValidLegacyFileExtensions = []string{ + FileExtensionHtmlMarkdown, + FileExtensionHtmlMd, + FileExtensionMarkdown, + FileExtensionMd, +} - return nil +var ValidRegistryFileExtensions = []string{ + FileExtensionMd, } -func validateExamples(ui cli.Ui, dir string) error { - return nil +var LegacyFrontMatterOptions = &check.FrontMatterOptions{ + NoSidebarCurrent: true, + RequireDescription: true, + RequireLayout: true, + RequirePageTitle: true, } -func validateTemplates(ui cli.Ui, dir string) error { - checks := []check{ - checkAllowedFiles( - "index.md", - "index.md.tmpl", - ), - checkAllowedDirs( - "data-sources", - "guides", - "functions", - "resources", - ), - checkBlockedExtensions( - ".html.md.tmpl", - ), - checkAllowedExtensions( - ".md", - ".md.tmpl", - ), - } - issues := []issue{} - for _, c := range checks { - checkIssues, err := c(dir) +var LegacyIndexFrontMatterOptions = &check.FrontMatterOptions{ + NoSidebarCurrent: true, + NoSubcategory: true, + RequireDescription: true, + RequireLayout: true, + RequirePageTitle: true, +} + +var LegacyGuideFrontMatterOptions = &check.FrontMatterOptions{ + NoSidebarCurrent: true, + RequireDescription: true, + RequireLayout: true, + RequirePageTitle: true, +} + +var RegistryFrontMatterOptions = &check.FrontMatterOptions{ + NoLayout: true, + NoSidebarCurrent: true, +} + +var RegistryIndexFrontMatterOptions = &check.FrontMatterOptions{ + NoLayout: true, + NoSidebarCurrent: true, + NoSubcategory: true, +} + +var RegistryGuideFrontMatterOptions = &check.FrontMatterOptions{ + NoLayout: true, + NoSidebarCurrent: true, + RequirePageTitle: true, +} + +type validator struct { + providerName string + providerDir string + providersSchemaPath string + + tfVersion string + providerSchema *tfjson.ProviderSchema + + logger *Logger +} + +func Validate(ui cli.Ui, providerDir, providerName, providersSchemaPath, tfversion string) error { + // Ensure provider directory is resolved absolute path + if providerDir == "" { + wd, err := os.Getwd() + if err != nil { - return err + return fmt.Errorf("error getting working directory: %w", err) + } + + providerDir = wd + } else { + absProviderDir, err := filepath.Abs(providerDir) + + if err != nil { + return fmt.Errorf("error getting absolute path with provider directory %q: %w", providerDir, err) } - issues = append(issues, checkIssues...) + + providerDir = absProviderDir } - for _, issue := range issues { - ui.Warn(fmt.Sprintf("%s: %s", issue.file, issue.message)) + + // Verify provider directory + providerDirFileInfo, err := os.Stat(providerDir) + + if err != nil { + return fmt.Errorf("error getting information for provider directory %q: %w", providerDir, err) } - if len(issues) > 0 { - return fmt.Errorf("invalid templates directory") + + if !providerDirFileInfo.IsDir() { + return fmt.Errorf("expected %q to be a directory", providerDir) } - return nil -} -func validateStaticDocs(ui cli.Ui, dir string) error { - checks := []check{ - checkAllowedFiles( - "index.md", - ), - checkAllowedDirs( - "data-sources", - "guides", - "functions", - "resources", - "cdktf", - ), - checkBlockedExtensions( - ".html.md.tmpl", - ".html.md", - ".md.tmpl", - ), - checkAllowedExtensions( - ".md", - ), + v := &validator{ + providerName: providerName, + providerDir: providerDir, + providersSchemaPath: providersSchemaPath, + tfVersion: tfversion, + + logger: NewLogger(ui), } - issues := []issue{} - for _, c := range checks { - checkIssues, err := c(dir) + + ctx := context.Background() + + return v.validate(ctx) +} + +func (v *validator) validate(ctx context.Context) error { + var result error + + var err error + + if v.providersSchemaPath == "" { + v.logger.infof("exporting schema from Terraform") + v.providerSchema, err = TerraformProviderSchemaFromTerraform(ctx, v.providerName, v.providerDir, v.tfVersion, v.logger) if err != nil { - return err + return fmt.Errorf("error exporting provider schema from Terraform: %w", err) + } + } else { + v.logger.infof("exporting schema from JSON file") + v.providerSchema, err = TerraformProviderSchemaFromFile(v.providerName, v.providersSchemaPath, v.logger) + if err != nil { + return fmt.Errorf("error exporting provider schema from JSON file: %w", err) } - issues = append(issues, checkIssues...) } - for _, issue := range issues { - ui.Warn(fmt.Sprintf("%s: %s", issue.file, issue.message)) + + providerFs := os.DirFS(v.providerDir) + + files, globErr := doublestar.Glob(providerFs, DocumentationGlobPattern) + if globErr != nil { + return fmt.Errorf("error finding documentation files: %w", err) } - if len(issues) > 0 { - return fmt.Errorf("invalid templates directory") + + log.Printf("[DEBUG] Found documentation files %v", files) + + v.logger.infof("running mixed directories check") + err = check.MixedDirectoriesCheck(files) + result = errors.Join(result, err) + + v.logger.infof("running number of files check") + err = check.NumberOfFilesCheck(files) + result = errors.Join(result, err) + + if dirExists(filepath.Join(v.providerDir, "docs")) { + v.logger.infof("detected static docs directory, running checks") + err = v.validateStaticDocs(filepath.Join(v.providerDir, "docs")) + result = errors.Join(result, err) + + } + if dirExists(filepath.Join(v.providerDir, filepath.Join("website", "docs"))) { + v.logger.infof("detected legacy website directory, running checks") + err = v.validateLegacyWebsite(filepath.Join(v.providerDir, "website/docs")) + result = errors.Join(result, err) } - return nil -} -func validateLegacyWebsite(ui cli.Ui, dir string) error { - panic("not implemented") + return result } -type issue struct { - file string - message string -} +func (v *validator) validateStaticDocs(dir string) error { -type check func(dir string) ([]issue, error) + var result error -func checkBlockedExtensions(exts ...string) check { - return func(dir string) ([]issue, error) { - issues := []issue{} - err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if info.IsDir() { - return nil - } - for _, ext := range exts { - if strings.HasSuffix(path, ext) { - _, file := filepath.Split(path) - issues = append(issues, issue{ - file: path, - message: fmt.Sprintf("the extension for %q is not supported", file), - }) - break - } - } - return nil - }) + options := &check.ProviderFileOptions{ + FrontMatter: RegistryFrontMatterOptions, + ValidExtensions: ValidRegistryFileExtensions, + } + + var files []string + + err := filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { if err != nil { - return nil, err + return fmt.Errorf("error walking directory %q: %w", dir, err) } - return issues, nil - } -} -func checkAllowedExtensions(exts ...string) check { - return func(dir string) ([]issue, error) { - issues := []issue{} - err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + rel, err := filepath.Rel(v.providerDir, path) + if err != nil { + return err + } + if d.IsDir() { + match, err := doublestar.PathMatch(filepath.FromSlash(DocumentationDirGlobPattern), rel) if err != nil { return err } - if info.IsDir() { - return nil - } - valid := false - for _, ext := range exts { - if strings.HasSuffix(path, ext) { - valid = true - break - } - } - if !valid { - _, file := filepath.Split(path) - issues = append(issues, issue{ - file: path, - message: fmt.Sprintf("the extension for %q is not expected", file), - }) + if !match { + return nil // skip valid non-documentation directories } + + v.logger.infof("running invalid directories check on %s", rel) + result = errors.Join(result, check.InvalidDirectoriesCheck(rel)) return nil - }) + } + match, err := doublestar.PathMatch(filepath.FromSlash(DocumentationGlobPattern), rel) if err != nil { - return nil, err + return err } - return issues, nil + if !match { + return nil // skip valid non-documentation files + } + + // Configure FrontMatterOptions based on file type + if d.Name() == "index.md" { + options.FrontMatter = RegistryIndexFrontMatterOptions + } else if _, relErr := filepath.Rel(rel, "guides"); relErr != nil { + options.FrontMatter = RegistryGuideFrontMatterOptions + } else { + options.FrontMatter = RegistryFrontMatterOptions + } + v.logger.infof("running file checks on %s", rel) + result = errors.Join(result, check.NewProviderFileCheck(options).Run(path)) + + files = append(files, path) + return nil + }) + if err != nil { + return fmt.Errorf("error walking directory %q: %w", dir, err) + } + + mismatchOpt := &check.FileMismatchOptions{ + ProviderShortName: providerShortName(v.providerName), + Schema: v.providerSchema, } -} -func checkAllowedDirs(dirs ...string) check { - allowedDirs := map[string]bool{} - for _, d := range dirs { - allowedDirs[d] = true + if dirExists(filepath.Join(dir, "data-sources")) { + dataSourceFiles, _ := os.ReadDir(filepath.Join(dir, "data-sources")) + mismatchOpt.DatasourceEntries = dataSourceFiles + } + if dirExists(filepath.Join(dir, "resources")) { + resourceFiles, _ := os.ReadDir(filepath.Join(dir, "resources")) + mismatchOpt.ResourceEntries = resourceFiles } + if dirExists(filepath.Join(dir, "functions")) { + functionFiles, _ := os.ReadDir(filepath.Join(dir, "functions")) + mismatchOpt.FunctionEntries = functionFiles + } + + v.logger.infof("running file mismatch check") + if err := check.NewFileMismatchCheck(mismatchOpt).Run(); err != nil { + result = errors.Join(result, err) + } + + return result +} + +func (v *validator) validateLegacyWebsite(dir string) error { + + var result error - return func(dir string) ([]issue, error) { - issues := []issue{} + options := &check.ProviderFileOptions{ + FrontMatter: LegacyFrontMatterOptions, + ValidExtensions: ValidLegacyFileExtensions, + } - f, err := os.Open(dir) + var files []string + err := filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { if err != nil { - return nil, err + return fmt.Errorf("error walking directory %q: %w", dir, err) } - infos, err := f.Readdir(-1) + + rel, err := filepath.Rel(v.providerDir, path) if err != nil { - return nil, err + return err } - - for _, fi := range infos { - if !fi.IsDir() { - continue + if d.IsDir() { + match, err := doublestar.PathMatch(filepath.FromSlash(DocumentationDirGlobPattern), rel) + if err != nil { + return err } - - if !allowedDirs[fi.Name()] { - issues = append(issues, issue{ - file: filepath.Join(dir, fi.Name()), - message: fmt.Sprintf("directory %q is not allowed", fi.Name()), - }) + if !match { + return nil // skip valid non-documentation directories } + + v.logger.infof("running invalid directories check on %s", rel) + result = errors.Join(result, check.InvalidDirectoriesCheck(rel)) + return nil } - return issues, nil - } -} + match, err := doublestar.PathMatch(filepath.FromSlash(DocumentationGlobPattern), rel) + if err != nil { + return err + } + if !match { + return nil // skip non-documentation files + } -func checkAllowedFiles(dirs ...string) check { - allowedFiles := map[string]bool{} - for _, d := range dirs { - allowedFiles[d] = true + // Configure FrontMatterOptions based on file type + if d.Name() == "index.md" { + options.FrontMatter = LegacyIndexFrontMatterOptions + } else if _, relErr := filepath.Rel(rel, "guides"); relErr != nil { + options.FrontMatter = LegacyGuideFrontMatterOptions + } else { + options.FrontMatter = LegacyFrontMatterOptions + } + v.logger.infof("running file checks on %s", rel) + result = errors.Join(result, check.NewProviderFileCheck(options).Run(path)) + + files = append(files, path) + return nil + }) + if err != nil { + return fmt.Errorf("error walking directory %q: %w", dir, err) } - return func(dir string) ([]issue, error) { - issues := []issue{} + mismatchOpt := &check.FileMismatchOptions{ + ProviderShortName: providerShortName(v.providerName), + Schema: v.providerSchema, + } - f, err := os.Open(dir) - if err != nil { - return nil, err - } - infos, err := f.Readdir(-1) - if err != nil { - return nil, err - } + if dirExists(filepath.Join(dir, "d")) { + dataSourceFiles, _ := os.ReadDir(filepath.Join(dir, "d")) + mismatchOpt.DatasourceEntries = dataSourceFiles + } + if dirExists(filepath.Join(dir, "r")) { + resourceFiles, _ := os.ReadDir(filepath.Join(dir, "r")) + mismatchOpt.ResourceEntries = resourceFiles + } + if dirExists(filepath.Join(dir, "functions")) { + functionFiles, _ := os.ReadDir(filepath.Join(dir, "functions")) + mismatchOpt.FunctionEntries = functionFiles + } - for _, fi := range infos { - if fi.IsDir() { - continue - } + v.logger.infof("running file mismatch check") + if err := check.NewFileMismatchCheck(mismatchOpt).Run(); err != nil { + result = errors.Join(result, err) + } - if !allowedFiles[fi.Name()] { - issues = append(issues, issue{ - file: filepath.Join(dir, fi.Name()), - message: fmt.Sprintf("file %q is not allowed", fi.Name()), - }) - } - } + return result +} - return issues, nil +func dirExists(name string) bool { + if file, err := os.Stat(name); err != nil { + return false + } else if !file.IsDir() { + return false } + + return true } diff --git a/internal/provider/validate_test.go b/internal/provider/validate_test.go new file mode 100644 index 00000000..4f087b9c --- /dev/null +++ b/internal/provider/validate_test.go @@ -0,0 +1,168 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "path/filepath" + "testing" + + "github.com/bmatcuk/doublestar/v4" + "github.com/hashicorp/cli" +) + +func TestValidateStaticDocs(t *testing.T) { + t.Parallel() + testCases := map[string]struct { + BasePath string + ExpectError bool + ExpectedError string + }{ + "valid registry directories": { + BasePath: filepath.Join("testdata", "valid-registry-directories"), + }, + + "valid registry directories with cdktf docs": { + BasePath: filepath.Join("testdata", "valid-registry-directories-with-cdktf"), + }, + "invalid registry directories": { + BasePath: filepath.Join("testdata", "invalid-registry-directories"), + ExpectError: true, + ExpectedError: "invalid Terraform Provider documentation directory found: " + filepath.Join("docs", "resources", "invalid"), + }, + } + + for name, testCase := range testCases { + name := name + testCase := testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + + v := &validator{ + providerDir: testCase.BasePath, + providerName: "terraform-provider-test", + + logger: NewLogger(cli.NewMockUi()), + } + + got := v.validateStaticDocs(filepath.Join(v.providerDir, "docs")) + + 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) + } + + if got != nil && got.Error() != testCase.ExpectedError { + t.Errorf("expected error: %s, got error: %s", testCase.ExpectedError, got) + } + }) + } +} + +func TestValidateLegacyWebsite(t *testing.T) { + t.Parallel() + testCases := map[string]struct { + BasePath string + ExpectError bool + ExpectedError string + }{ + "valid legacy directories": { + BasePath: filepath.Join("testdata", "valid-legacy-directories"), + }, + "valid legacy directories with cdktf docs": { + BasePath: filepath.Join("testdata", "valid-legacy-directories-with-cdktf"), + }, + "invalid legacy directories": { + BasePath: filepath.Join("testdata", "invalid-legacy-directories"), + ExpectError: true, + ExpectedError: "invalid Terraform Provider documentation directory found: " + filepath.Join("website", "docs", "r", "invalid"), + }, + } + + for name, testCase := range testCases { + name := name + testCase := testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + + v := &validator{ + providerDir: testCase.BasePath, + providerName: "terraform-provider-test", + + logger: NewLogger(cli.NewMockUi()), + } + + got := v.validateLegacyWebsite(filepath.Join(v.providerDir, "website")) + + 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) + } + + if got != nil && got.Error() != testCase.ExpectedError { + t.Errorf("expected error: %s, got error: %s", testCase.ExpectedError, got) + } + }) + } +} + +func TestDocumentationDirGlobPattern(t *testing.T) { + t.Parallel() + testCases := map[string]struct { + ExpectMatch bool + }{ + "docs/data-sources": { + ExpectMatch: true, + }, + "docs/guides": { + ExpectMatch: true, + }, + "docs/resources": { + ExpectMatch: true, + }, + "website/docs/r": { + ExpectMatch: true, + }, + "website/docs/r/invalid": { + ExpectMatch: true, + }, + "website/docs/d": { + ExpectMatch: true, + }, + "website/docs/invalid": { + ExpectMatch: true, + }, + "docs/resources/invalid": { + ExpectMatch: true, + }, + "docs/index.md": { + ExpectMatch: false, + }, + "docs/invalid": { + ExpectMatch: false, + }, + } + + for name, testCase := range testCases { + name := name + testCase := testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + + match, err := doublestar.Match(DocumentationDirGlobPattern, name) + if err != nil { + t.Fatalf("error matching pattern: %q", err) + } + + if match != testCase.ExpectMatch { + t.Errorf("expected match: %t, got match: %t", testCase.ExpectMatch, match) + } + }) + } +} diff --git a/schemamd/behaviors_test.go b/schemamd/behaviors_test.go index cdc93af2..48e03336 100644 --- a/schemamd/behaviors_test.go +++ b/schemamd/behaviors_test.go @@ -13,7 +13,6 @@ import ( func TestChildAttributeIsRequired(t *testing.T) { t.Parallel() - for _, c := range []struct { name string att *tfjson.SchemaAttribute diff --git a/tools/go.mod b/tools/go.mod index 4a070c7f..7c992ca2 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -1,6 +1,6 @@ module tools -go 1.19 +go 1.21 require github.com/hashicorp/copywrite v0.18.0