diff --git a/.changes/unreleased/BUG FIXES-20241118-125001.yaml b/.changes/unreleased/BUG FIXES-20241118-125001.yaml new file mode 100644 index 00000000..52155315 --- /dev/null +++ b/.changes/unreleased/BUG FIXES-20241118-125001.yaml @@ -0,0 +1,6 @@ +kind: BUG FIXES +body: 'validate: Fixed a bug that caused false positive validation errors for resource + types that have the same name as the provider.' +time: 2024-11-18T12:50:01.114497-05:00 +custom: + Issue: "419" diff --git a/.changes/unreleased/BUG FIXES-20241118-125100.yaml b/.changes/unreleased/BUG FIXES-20241118-125100.yaml new file mode 100644 index 00000000..0e1f7342 --- /dev/null +++ b/.changes/unreleased/BUG FIXES-20241118-125100.yaml @@ -0,0 +1,7 @@ +kind: BUG FIXES +body: 'generate: Fixed a bug that caused all generated resource documentation to have + the same content when the provider has a resource type with the same name as the + provider.' +time: 2024-11-18T12:51:00.736136-05:00 +custom: + Issue: "419" diff --git a/.changes/unreleased/BUG FIXES-20241118-125204.yaml b/.changes/unreleased/BUG FIXES-20241118-125204.yaml new file mode 100644 index 00000000..17cefc0d --- /dev/null +++ b/.changes/unreleased/BUG FIXES-20241118-125204.yaml @@ -0,0 +1,6 @@ +kind: BUG FIXES +body: 'generate: Fixed a bug that would return an error when a static file exists + in both `templates` and `docs`, which will now be ignored.' +time: 2024-11-18T12:52:04.748022-05:00 +custom: + Issue: "421" diff --git a/.gitignore b/.gitignore index 19e2604d..588539de 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,6 @@ tfproviderdocsgen # JetBrains IDEs files .idea/ *.iws + +# VSCode files +.vscode \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 8ef33d91..00000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Launch", - "type": "go", - "request": "launch", - "mode": "auto", - "program": "${workspaceFolder}/cmd/tfplugindocs/", - "env": {}, - "args": [], - // Set this to a directory with the provider source code you - // with which you want to test generation. - "cwd": "${workspaceFolder}/../../hashicorp/terraform-provider-null" - } - ] -} \ No newline at end of file diff --git a/README.md b/README.md index dc1edecc..b1e6d166 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,11 @@ When you run `tfplugindocs`, by default from the root directory of a provider co * Generate function template files, if missing (Requires Terraform v1.8.0+) * Generate ephemeral resource template files, if missing (Requires Terraform v1.10.0+) * Copy all non-template files to the output website directory + +> [!NOTE] +> +> Non-template files that already exist in the output website directory will not be overwritten. + * Process all the remaining templates to generate files for the output website directory For inspiration, you can look at the templates and output of the diff --git a/internal/check/file_mismatch.go b/internal/check/file_mismatch.go index c53827d7..97d78d70 100644 --- a/internal/check/file_mismatch.go +++ b/internal/check/file_mismatch.go @@ -84,7 +84,7 @@ func (check *FileMismatchCheck) Run() error { return result } -// ResourceFileMismatchCheck checks for mismatched files, either missing or extraneous, against the resource/datasouce schema +// ResourceFileMismatchCheck checks for mismatched files, either missing or extraneous, against the resource/datasource 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) @@ -200,7 +200,11 @@ func (check *FileMismatchCheck) FunctionFileMismatchCheck(files []os.DirEntry, f func (check *FileMismatchCheck) IgnoreFileMismatch(file string) bool { for _, ignoreResourceName := range check.Options.IgnoreFileMismatch { - if ignoreResourceName == fileResourceName(check.Options.ProviderShortName, file) { + if ignoreResourceName == fileResourceNameWithProvider(check.Options.ProviderShortName, file) { + return true + } else if ignoreResourceName == TrimFileExtension(file) { + // While uncommon, it is valid for a resource type to be named the same as the provider itself. + // https://github.com/hashicorp/terraform-plugin-docs/issues/419 return true } } @@ -219,7 +223,13 @@ func (check *FileMismatchCheck) IgnoreFileMissing(resourceName string) bool { } func fileHasResource(schemaResources map[string]*tfjson.Schema, providerName, file string) bool { - if _, ok := schemaResources[fileResourceName(providerName, file)]; ok { + if _, ok := schemaResources[fileResourceNameWithProvider(providerName, file)]; ok { + return true + } + + // While uncommon, it is valid for a resource type to be named the same as the provider itself. + // https://github.com/hashicorp/terraform-plugin-docs/issues/419 + if _, ok := schemaResources[TrimFileExtension(file)]; ok { return true } @@ -234,7 +244,7 @@ func fileHasFunction(functions map[string]*tfjson.FunctionSignature, file string return false } -func fileResourceName(providerName, fileName string) string { +func fileResourceNameWithProvider(providerName, fileName string) string { resourceSuffix := TrimFileExtension(fileName) return fmt.Sprintf("%s_%s", providerName, resourceSuffix) @@ -244,7 +254,12 @@ func resourceHasFile(files []os.DirEntry, providerName, resourceName string) boo var found bool for _, file := range files { - if fileResourceName(providerName, file.Name()) == resourceName { + if fileResourceNameWithProvider(providerName, file.Name()) == resourceName { + found = true + break + } else if TrimFileExtension(file.Name()) == resourceName { + // While uncommon, it is valid for a resource type to be named the same as the provider itself. + // https://github.com/hashicorp/terraform-plugin-docs/issues/419 found = true break } diff --git a/internal/check/file_mismatch_test.go b/internal/check/file_mismatch_test.go index 50ef8dfc..67f7103d 100644 --- a/internal/check/file_mismatch_test.go +++ b/internal/check/file_mismatch_test.go @@ -82,7 +82,7 @@ func TestFileResourceName(t *testing.T) { testCase := testCase t.Run(name, func(t *testing.T) { t.Parallel() - got := fileResourceName("test", testCase.File) + got := fileResourceNameWithProvider("test", testCase.File) want := testCase.Expect if got != want { @@ -115,6 +115,19 @@ func TestFileMismatchCheck(t *testing.T) { }, }, }, + "all found - resource with no suffix": { + ResourceFiles: fstest.MapFS{ + "test.md": {}, + }, + Options: &FileMismatchOptions{ + ProviderShortName: "test", + Schema: &tfjson.ProviderSchema{ + ResourceSchemas: map[string]*tfjson.Schema{ + "test": {}, + }, + }, + }, + }, "all found - function": { FunctionFiles: fstest.MapFS{ "function1.md": {}, @@ -181,6 +194,23 @@ func TestFileMismatchCheck(t *testing.T) { }, }, }, + "ignore extra file - resource with no suffix": { + ResourceFiles: fstest.MapFS{ + "resource1.md": {}, + "resource2.md": {}, + "test.md": {}, + }, + Options: &FileMismatchOptions{ + IgnoreFileMismatch: []string{"test"}, + ProviderShortName: "test", + Schema: &tfjson.ProviderSchema{ + ResourceSchemas: map[string]*tfjson.Schema{ + "test_resource1": {}, + "test_resource2": {}, + }, + }, + }, + }, "ignore extra file - function": { FunctionFiles: fstest.MapFS{ "function1.md": {}, @@ -214,6 +244,21 @@ func TestFileMismatchCheck(t *testing.T) { }, ExpectError: true, }, + "missing file - resource with no suffix": { + ResourceFiles: fstest.MapFS{ + "resource1.md": {}, + }, + Options: &FileMismatchOptions{ + ProviderShortName: "test", + Schema: &tfjson.ProviderSchema{ + ResourceSchemas: map[string]*tfjson.Schema{ + "test_resource1": {}, + "test": {}, + }, + }, + }, + ExpectError: true, + }, "missing file - function": { FunctionFiles: fstest.MapFS{ "function1.md": {}, diff --git a/internal/provider/util.go b/internal/provider/util.go index 7a3ec336..9041cf01 100644 --- a/internal/provider/util.go +++ b/internal/provider/util.go @@ -4,6 +4,7 @@ package provider import ( + "errors" "fmt" "io" "log" @@ -45,6 +46,10 @@ func copyFile(srcPath, dstPath string, mode os.FileMode) error { // If the destination file already exists, we shouldn't blow it away dstFile, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, mode) if err != nil { + // If the file already exists, we can skip it without returning an error. + if errors.Is(err, os.ErrExist) { + return nil + } return err } defer dstFile.Close() @@ -71,16 +76,15 @@ func removeAllExt(file string) string { // has either the providerShortName or the providerShortName concatenated with the // templateFileName (stripped of file extension. func resourceSchema(schemas map[string]*tfjson.Schema, providerShortName, templateFileName string) (*tfjson.Schema, string) { - if schema, ok := schemas[providerShortName]; ok { - return schema, providerShortName - } - resName := providerShortName + "_" + removeAllExt(templateFileName) - if schema, ok := schemas[resName]; ok { return schema, resName } + if schema, ok := schemas[providerShortName]; ok { + return schema, providerShortName + } + return nil, resName }