From da27bbd3ca51850f839945cf8f3be905b737d5d0 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Wed, 8 Nov 2023 16:31:49 -0500 Subject: [PATCH 01/12] Add acceptance tests for `generate` command --- cmd/build/version.go | 18 ++++++++++++++++++ cmd/tfplugindocs/main_test.go | 3 --- 2 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 cmd/build/version.go diff --git a/cmd/build/version.go b/cmd/build/version.go new file mode 100644 index 00000000..01ded843 --- /dev/null +++ b/cmd/build/version.go @@ -0,0 +1,18 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package build + +var ( + // These vars will be set by goreleaser. + version string = `dev` + commit string = `` +) + +func GetVersion() string { + version := "tfplugindocs" + " Version " + version + if commit != "" { + version += " from commit " + commit + } + return version +} diff --git a/cmd/tfplugindocs/main_test.go b/cmd/tfplugindocs/main_test.go index cd542aae..5c0ec4e5 100644 --- a/cmd/tfplugindocs/main_test.go +++ b/cmd/tfplugindocs/main_test.go @@ -1,6 +1,3 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - package main import ( From 0eb31fc8d9a85ab8109e78040f8527afbaaf7a5b Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Wed, 8 Nov 2023 17:34:15 -0500 Subject: [PATCH 02/12] Add copyright headers --- cmd/tfplugindocs/main_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/tfplugindocs/main_test.go b/cmd/tfplugindocs/main_test.go index 5c0ec4e5..cd542aae 100644 --- a/cmd/tfplugindocs/main_test.go +++ b/cmd/tfplugindocs/main_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package main import ( From 14c597d811cf9eba5a8a87fca1a5a753de6bcf73 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Fri, 10 Nov 2023 15:04:41 -0500 Subject: [PATCH 03/12] Move `build` package and update goreleaser `idflags` --- cmd/build/version.go | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 cmd/build/version.go diff --git a/cmd/build/version.go b/cmd/build/version.go deleted file mode 100644 index 01ded843..00000000 --- a/cmd/build/version.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package build - -var ( - // These vars will be set by goreleaser. - version string = `dev` - commit string = `` -) - -func GetVersion() string { - version := "tfplugindocs" + " Version " + version - if commit != "" { - version += " from commit " + commit - } - return version -} From 7752585f7803b8dedd1e8ed7a90cb672ea439419 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 14 Nov 2023 13:59:14 -0500 Subject: [PATCH 04/12] Add context to errors --- internal/provider/generate.go | 45 ++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/internal/provider/generate.go b/internal/provider/generate.go index d6aafe25..cd1e0c64 100644 --- a/internal/provider/generate.go +++ b/internal/provider/generate.go @@ -171,20 +171,20 @@ func (g *generator) Generate(ctx context.Context) error { case g.websiteTmpDir == "": g.websiteTmpDir, err = os.MkdirTemp("", "tfws") if err != nil { - return err + return fmt.Errorf("error creating temporary website directory: %w", err) } defer os.RemoveAll(g.websiteTmpDir) default: g.infof("cleaning tmp dir %q", g.websiteTmpDir) err = os.RemoveAll(g.websiteTmpDir) if err != nil { - return err + return fmt.Errorf("error removing temporary website directory %q: %w", g.websiteTmpDir, err) } g.infof("creating tmp dir %q", g.websiteTmpDir) err = os.MkdirAll(g.websiteTmpDir, 0755) if err != nil { - return err + return fmt.Errorf("error creating temporary website directory %q: %w", g.websiteTmpDir, err) } } @@ -193,7 +193,7 @@ func (g *generator) Generate(ctx context.Context) error { case os.IsNotExist(err): // do nothing, no template dir case err != nil: - return err + return fmt.Errorf("error getting information for provider templates directory %q: %w", g.ProviderTemplatesDir(), err) default: if !templatesDirInfo.IsDir() { return fmt.Errorf("template path is not a directory: %s", g.ProviderTemplatesDir()) @@ -202,26 +202,26 @@ func (g *generator) Generate(ctx context.Context) error { g.infof("copying any existing content to tmp dir") err = cp(g.ProviderTemplatesDir(), g.TempTemplatesDir()) if err != nil { - return err + return fmt.Errorf("error copying exiting content to temporary directory %q: %w", g.TempTemplatesDir(), err) } } g.infof("exporting schema from Terraform") providerSchema, err := g.terraformProviderSchema(ctx, providerName) if err != nil { - return err + return fmt.Errorf("error exporting provider schema from Terraform: %w", err) } g.infof("rendering missing docs") err = g.renderMissingDocs(providerName, providerSchema) if err != nil { - return err + return fmt.Errorf("error rendering missing docs: %w", err) } g.infof("rendering static website") err = g.renderStaticWebsite(providerName, providerSchema) if err != nil { - return err + return fmt.Errorf("error rendering static website: %w", err) } return nil @@ -430,7 +430,7 @@ func (g *generator) renderStaticWebsite(providerName string, providerSchema *tfj g.infof("cleaning rendered website dir") dirEntry, err := os.ReadDir(g.ProviderDocsDir()) if err != nil && !os.IsNotExist(err) { - return err + return fmt.Errorf("unable to read rendered website directory %q: %w", g.ProviderDocsDir(), err) } for _, file := range dirEntry { @@ -440,7 +440,7 @@ func (g *generator) renderStaticWebsite(providerName string, providerSchema *tfj g.infof("removing directory: %q", file.Name()) err = os.RemoveAll(path.Join(g.ProviderDocsDir(), file.Name())) if err != nil { - return err + return fmt.Errorf("unable to remove directory %q from rendered website directory: %w", file.Name(), err) } continue } @@ -450,7 +450,7 @@ func (g *generator) renderStaticWebsite(providerName string, providerSchema *tfj g.infof("removing file: %q", file.Name()) err = os.RemoveAll(path.Join(g.ProviderDocsDir(), file.Name())) if err != nil { - return err + return fmt.Errorf("unable to remove file %q from rendered website directory: %w", file.Name(), err) } continue } @@ -468,7 +468,8 @@ func (g *generator) renderStaticWebsite(providerName string, providerSchema *tfj rel, err := filepath.Rel(filepath.Join(g.TempTemplatesDir()), path) if err != nil { - return err + return fmt.Errorf("unable to retrieve the relative path of basepath %q and targetpath %q: %w", + filepath.Join(g.TempTemplatesDir()), path, err) } relDir, relFile := filepath.Split(rel) @@ -482,7 +483,7 @@ func (g *generator) renderStaticWebsite(providerName string, providerSchema *tfj renderedPath := filepath.Join(g.ProviderDocsDir(), rel) err = os.MkdirAll(filepath.Dir(renderedPath), 0755) if err != nil { - return err + return fmt.Errorf("unable to create rendered website subdirectory %q: %w", renderedPath, err) } ext := filepath.Ext(path) @@ -500,7 +501,7 @@ func (g *generator) renderStaticWebsite(providerName string, providerSchema *tfj out, err := os.Create(renderedPath) if err != nil { - return err + return fmt.Errorf("unable to create file %q: %w", renderedPath, err) } defer out.Close() @@ -565,7 +566,7 @@ func (g *generator) renderStaticWebsite(providerName string, providerSchema *tfj return nil }) if err != nil { - return err + return fmt.Errorf("unable to render templated website to static markdown: %w", err) } return nil @@ -578,7 +579,7 @@ func (g *generator) terraformProviderSchema(ctx context.Context, providerName st tmpDir, err := os.MkdirTemp("", "tfws") if err != nil { - return nil, err + return nil, fmt.Errorf("unable to create temporary provider install directory %q: %w", tmpDir, err) } defer os.RemoveAll(tmpDir) @@ -599,7 +600,7 @@ func (g *generator) terraformProviderSchema(ctx context.Context, providerName st // TODO: constrain env here to make it a little safer? _, err = runCmd(buildCmd) if err != nil { - return nil, err + return nil, fmt.Errorf("unable to execute go build command: %w", err) } err = writeFile(filepath.Join(tmpDir, "provider.tf"), fmt.Sprintf(` @@ -607,7 +608,7 @@ provider %[1]q { } `, shortName)) if err != nil { - return nil, err + return nil, fmt.Errorf("unable to write provider.tf file: %w", err) } i := install.NewInstaller() @@ -636,24 +637,24 @@ provider %[1]q { tfBin, err := i.Ensure(context.Background(), sources) if err != nil { - return nil, err + return nil, fmt.Errorf("unable to download Terraform binary: %w", err) } tf, err := tfexec.NewTerraform(tmpDir, tfBin) if err != nil { - return nil, err + return nil, fmt.Errorf("unable to create new terraform exec instance: %w", err) } g.infof("running terraform init") err = tf.Init(ctx, tfexec.Get(false), tfexec.PluginDir("./plugins")) if err != nil { - return nil, err + return nil, fmt.Errorf("unable to run terraform init on provider: %w", err) } g.infof("getting provider schema") schemas, err := tf.ProvidersSchema(ctx) if err != nil { - return nil, err + return nil, fmt.Errorf("unable to retrieve provider schema from terraform exec: %w", err) } if ps, ok := schemas.Schemas[shortName]; ok { From 544d22213b0fe0c8134eb61d9d646ae7c41947f6 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Wed, 15 Nov 2023 17:28:20 -0500 Subject: [PATCH 05/12] Refactor to use `g.providerName` instead of passing the provider name as a parameter. --- internal/provider/generate.go | 108 ++++++++++++++++------------------ 1 file changed, 51 insertions(+), 57 deletions(-) diff --git a/internal/provider/generate.go b/internal/provider/generate.go index cd1e0c64..f376d9f6 100644 --- a/internal/provider/generate.go +++ b/internal/provider/generate.go @@ -156,16 +156,15 @@ func Generate(ui cli.Ui, providerDir, providerName, renderedProviderName, render func (g *generator) Generate(ctx context.Context) error { var err error - providerName := g.providerName if g.providerName == "" { - providerName = filepath.Base(g.providerDir) + g.providerName = filepath.Base(g.providerDir) } if g.renderedProviderName == "" { - g.renderedProviderName = providerName + g.renderedProviderName = g.providerName } - g.infof("rendering website for provider %q (as %q)", providerName, g.renderedProviderName) + g.infof("rendering website for provider %q (as %q)", g.providerName, g.renderedProviderName) switch { case g.websiteTmpDir == "": @@ -207,19 +206,19 @@ func (g *generator) Generate(ctx context.Context) error { } g.infof("exporting schema from Terraform") - providerSchema, err := g.terraformProviderSchema(ctx, providerName) + providerSchema, err := g.terraformProviderSchema(ctx) if err != nil { return fmt.Errorf("error exporting provider schema from Terraform: %w", err) } g.infof("rendering missing docs") - err = g.renderMissingDocs(providerName, providerSchema) + err = g.renderMissingDocs(providerSchema) if err != nil { return fmt.Errorf("error rendering missing docs: %w", err) } g.infof("rendering static website") - err = g.renderStaticWebsite(providerName, providerSchema) + err = g.renderStaticWebsite(providerSchema) if err != nil { return fmt.Errorf("error rendering static website: %w", err) } @@ -229,55 +228,55 @@ func (g *generator) Generate(ctx context.Context) error { // ProviderDocsDir returns the absolute path to the joined provider and // given website documentation directory, which defaults to "docs". -func (g generator) ProviderDocsDir() string { +func (g *generator) ProviderDocsDir() string { return filepath.Join(g.providerDir, g.renderedWebsiteDir) } // ProviderExamplesDir returns the absolute path to the joined provider and // given examples directory, which defaults to "examples". -func (g generator) ProviderExamplesDir() string { +func (g *generator) ProviderExamplesDir() string { return filepath.Join(g.providerDir, g.examplesDir) } // ProviderTemplatesDir returns the absolute path to the joined provider and // given templates directory, which defaults to "templates". -func (g generator) ProviderTemplatesDir() string { +func (g *generator) ProviderTemplatesDir() string { return filepath.Join(g.providerDir, g.templatesDir) } // TempTemplatesDir returns the absolute path to the joined temporary and -// hardcoded "templates" sub-directory, which is where provider templates are +// hardcoded "templates" subdirectory, which is where provider templates are // copied. -func (g generator) TempTemplatesDir() string { +func (g *generator) TempTemplatesDir() string { return filepath.Join(g.websiteTmpDir, "templates") } -func (g *generator) renderMissingResourceDoc(providerName, name, typeName string, schema *tfjson.Schema, websiteFileTemplate resourceFileTemplate, fallbackWebsiteFileTemplate resourceFileTemplate, websiteStaticCandidateTemplates []resourceFileTemplate, examplesFileTemplate resourceFileTemplate, examplesImportTemplate *resourceFileTemplate) error { - tmplPath, err := websiteFileTemplate.Render(g.providerDir, name, providerName) +func (g *generator) renderMissingResourceDoc(resourceName, typeName string, schema *tfjson.Schema, websiteFileTemplate resourceFileTemplate, fallbackWebsiteFileTemplate resourceFileTemplate, websiteStaticCandidateTemplates []resourceFileTemplate, examplesFileTemplate resourceFileTemplate, examplesImportTemplate *resourceFileTemplate) error { + tmplPath, err := websiteFileTemplate.Render(g.providerDir, resourceName, g.providerName) if err != nil { - return fmt.Errorf("unable to render path for resource %q: %w", name, err) + return fmt.Errorf("unable to render path for resource %q: %w", resourceName, err) } tmplPath = filepath.Join(g.TempTemplatesDir(), tmplPath) if fileExists(tmplPath) { - g.infof("resource %q template exists, skipping", name) + g.infof("resource %q template exists, skipping", resourceName) return nil } for _, candidate := range websiteStaticCandidateTemplates { - candidatePath, err := candidate.Render(g.providerDir, name, providerName) + candidatePath, err := candidate.Render(g.providerDir, resourceName, g.providerName) if err != nil { - return fmt.Errorf("unable to render path for resource %q: %w", name, err) + return fmt.Errorf("unable to render path for resource %q: %w", resourceName, err) } candidatePath = filepath.Join(g.TempTemplatesDir(), candidatePath) if fileExists(candidatePath) { - g.infof("resource %q static file exists, skipping", name) + g.infof("resource %q static file exists, skipping", resourceName) return nil } } - examplePath, err := examplesFileTemplate.Render(g.providerDir, name, providerName) + examplePath, err := examplesFileTemplate.Render(g.providerDir, resourceName, g.providerName) if err != nil { - return fmt.Errorf("unable to render example file path for %q: %w", name, err) + return fmt.Errorf("unable to render example file path for %q: %w", resourceName, err) } if examplePath != "" { examplePath = filepath.Join(g.ProviderExamplesDir(), examplePath) @@ -288,9 +287,9 @@ func (g *generator) renderMissingResourceDoc(providerName, name, typeName string importPath := "" if examplesImportTemplate != nil { - importPath, err = examplesImportTemplate.Render(g.providerDir, name, providerName) + importPath, err = examplesImportTemplate.Render(g.providerDir, resourceName, g.providerName) if err != nil { - return fmt.Errorf("unable to render example import file path for %q: %w", name, err) + return fmt.Errorf("unable to render example import file path for %q: %w", resourceName, err) } if importPath != "" { importPath = filepath.Join(g.ProviderExamplesDir(), importPath) @@ -302,13 +301,13 @@ func (g *generator) renderMissingResourceDoc(providerName, name, typeName string targetResourceTemplate := defaultResourceTemplate - fallbackTmplPath, err := fallbackWebsiteFileTemplate.Render(g.providerDir, name, providerName) + fallbackTmplPath, err := fallbackWebsiteFileTemplate.Render(g.providerDir, resourceName, g.providerName) if err != nil { - return fmt.Errorf("unable to render path for resource %q: %w", name, err) + return fmt.Errorf("unable to render path for resource %q: %w", resourceName, err) } fallbackTmplPath = filepath.Join(g.TempTemplatesDir(), fallbackTmplPath) if fileExists(fallbackTmplPath) { - g.infof("resource %q fallback template exists", name) + g.infof("resource %q fallback template exists", resourceName) tmplData, err := os.ReadFile(fallbackTmplPath) if err != nil { return fmt.Errorf("unable to read file %q: %w", fallbackTmplPath, err) @@ -316,10 +315,10 @@ func (g *generator) renderMissingResourceDoc(providerName, name, typeName string targetResourceTemplate = resourceTemplate(tmplData) } - g.infof("generating template for %q", name) - md, err := targetResourceTemplate.Render(g.providerDir, name, providerName, g.renderedProviderName, typeName, examplePath, importPath, schema) + g.infof("generating template for %q", resourceName) + md, err := targetResourceTemplate.Render(g.providerDir, resourceName, g.providerName, g.renderedProviderName, typeName, examplePath, importPath, schema) if err != nil { - return fmt.Errorf("unable to render template for %q: %w", name, err) + return fmt.Errorf("unable to render template for %q: %w", resourceName, err) } err = writeFile(tmplPath, md) @@ -330,32 +329,32 @@ func (g *generator) renderMissingResourceDoc(providerName, name, typeName string return nil } -func (g *generator) renderMissingProviderDoc(providerName string, schema *tfjson.Schema, websiteFileTemplate providerFileTemplate, websiteStaticCandidateTemplates []providerFileTemplate, examplesFileTemplate providerFileTemplate) error { - tmplPath, err := websiteFileTemplate.Render(g.providerDir, providerName) +func (g *generator) renderMissingProviderDoc(schema *tfjson.Schema, websiteFileTemplate providerFileTemplate, websiteStaticCandidateTemplates []providerFileTemplate, examplesFileTemplate providerFileTemplate) error { + tmplPath, err := websiteFileTemplate.Render(g.providerDir, g.providerName) if err != nil { - return fmt.Errorf("unable to render path for provider %q: %w", providerName, err) + return fmt.Errorf("unable to render path for provider %q: %w", g.providerName, err) } tmplPath = filepath.Join(g.TempTemplatesDir(), tmplPath) if fileExists(tmplPath) { - g.infof("provider %q template exists, skipping", providerName) + g.infof("provider %q template exists, skipping", g.providerName) return nil } for _, candidate := range websiteStaticCandidateTemplates { - candidatePath, err := candidate.Render(g.providerDir, providerName) + candidatePath, err := candidate.Render(g.providerDir, g.providerName) if err != nil { - return fmt.Errorf("unable to render path for provider %q: %w", providerName, err) + return fmt.Errorf("unable to render path for provider %q: %w", g.providerName, err) } candidatePath = filepath.Join(g.TempTemplatesDir(), candidatePath) if fileExists(candidatePath) { - g.infof("provider %q static file exists, skipping", providerName) + g.infof("provider %q static file exists, skipping", g.providerName) return nil } } - examplePath, err := examplesFileTemplate.Render(g.providerDir, providerName) + examplePath, err := examplesFileTemplate.Render(g.providerDir, g.providerName) if err != nil { - return fmt.Errorf("unable to render example file path for %q: %w", providerName, err) + return fmt.Errorf("unable to render example file path for %q: %w", g.providerName, err) } if examplePath != "" { examplePath = filepath.Join(g.ProviderExamplesDir(), examplePath) @@ -364,10 +363,10 @@ func (g *generator) renderMissingProviderDoc(providerName string, schema *tfjson examplePath = "" } - g.infof("generating template for %q", providerName) - md, err := defaultProviderTemplate.Render(g.providerDir, providerName, g.renderedProviderName, examplePath, schema) + g.infof("generating template for %q", g.providerName) + md, err := defaultProviderTemplate.Render(g.providerDir, g.providerName, g.renderedProviderName, examplePath, schema) if err != nil { - return fmt.Errorf("unable to render template for %q: %w", providerName, err) + return fmt.Errorf("unable to render template for %q: %w", g.providerName, err) } err = writeFile(tmplPath, md) @@ -378,14 +377,14 @@ func (g *generator) renderMissingProviderDoc(providerName string, schema *tfjson return nil } -func (g *generator) renderMissingDocs(providerName string, providerSchema *tfjson.ProviderSchema) error { +func (g *generator) renderMissingDocs(providerSchema *tfjson.ProviderSchema) error { g.infof("generating missing resource content") for name, schema := range providerSchema.ResourceSchemas { if g.ignoreDeprecated && schema.Block.Deprecated { continue } - err := g.renderMissingResourceDoc(providerName, name, "Resource", schema, + err := g.renderMissingResourceDoc(name, "Resource", schema, websiteResourceFileTemplate, websiteResourceFallbackFileTemplate, websiteResourceFileStatic, @@ -402,7 +401,7 @@ func (g *generator) renderMissingDocs(providerName string, providerSchema *tfjso continue } - err := g.renderMissingResourceDoc(providerName, name, "Data Source", schema, + err := g.renderMissingResourceDoc(name, "Data Source", schema, websiteDataSourceFileTemplate, websiteDataSourceFallbackFileTemplate, websiteDataSourceFileStatic, @@ -414,7 +413,7 @@ func (g *generator) renderMissingDocs(providerName string, providerSchema *tfjso } g.infof("generating missing provider content") - err := g.renderMissingProviderDoc(providerName, providerSchema.ConfigSchema, + err := g.renderMissingProviderDoc(providerSchema.ConfigSchema, websiteProviderFileTemplate, websiteProviderFileStatic, examplesProviderFileTemplate, @@ -426,7 +425,7 @@ func (g *generator) renderMissingDocs(providerName string, providerSchema *tfjso return nil } -func (g *generator) renderStaticWebsite(providerName string, providerSchema *tfjson.ProviderSchema) error { +func (g *generator) renderStaticWebsite(providerSchema *tfjson.ProviderSchema) error { g.infof("cleaning rendered website dir") dirEntry, err := os.ReadDir(g.ProviderDocsDir()) if err != nil && !os.IsNotExist(err) { @@ -456,7 +455,7 @@ func (g *generator) renderStaticWebsite(providerName string, providerSchema *tfj } } - shortName := providerShortName(providerName) + shortName := providerShortName(g.providerName) g.infof("rendering templated website to static markdown") @@ -513,7 +512,7 @@ func (g *generator) renderStaticWebsite(providerName string, providerSchema *tfj if resSchema != nil { tmpl := resourceTemplate(tmplData) - render, err := tmpl.Render(g.providerDir, resName, providerName, g.renderedProviderName, "Data Source", exampleFilePath, "", resSchema) + render, err := tmpl.Render(g.providerDir, resName, g.providerName, g.renderedProviderName, "Data Source", exampleFilePath, "", resSchema) if err != nil { return fmt.Errorf("unable to render data source template %q: %w", rel, err) } @@ -531,7 +530,7 @@ func (g *generator) renderStaticWebsite(providerName string, providerSchema *tfj if resSchema != nil { tmpl := resourceTemplate(tmplData) - render, err := tmpl.Render(g.providerDir, resName, providerName, g.renderedProviderName, "Resource", exampleFilePath, importFilePath, resSchema) + render, err := tmpl.Render(g.providerDir, resName, g.providerName, g.renderedProviderName, "Resource", exampleFilePath, importFilePath, resSchema) if err != nil { return fmt.Errorf("unable to render resource template %q: %w", rel, err) } @@ -546,7 +545,7 @@ func (g *generator) renderStaticWebsite(providerName string, providerSchema *tfj if relFile == "index.md.tmpl" { tmpl := providerTemplate(tmplData) exampleFilePath := filepath.Join(g.ProviderExamplesDir(), "provider", "provider.tf") - render, err := tmpl.Render(g.providerDir, providerName, g.renderedProviderName, exampleFilePath, providerSchema.ConfigSchema) + render, err := tmpl.Render(g.providerDir, g.providerName, g.renderedProviderName, exampleFilePath, providerSchema.ConfigSchema) if err != nil { return fmt.Errorf("unable to render provider template %q: %w", rel, err) } @@ -572,10 +571,10 @@ func (g *generator) renderStaticWebsite(providerName string, providerSchema *tfj return nil } -func (g *generator) terraformProviderSchema(ctx context.Context, providerName string) (*tfjson.ProviderSchema, error) { +func (g *generator) terraformProviderSchema(ctx context.Context) (*tfjson.ProviderSchema, error) { var err error - shortName := providerShortName(providerName) + shortName := providerShortName(g.providerName) tmpDir, err := os.MkdirTemp("", "tfws") if err != nil { @@ -583,11 +582,6 @@ func (g *generator) terraformProviderSchema(ctx context.Context, providerName st } defer os.RemoveAll(tmpDir) - // tmpDir := "/tmp/tftmp" - // os.RemoveAll(tmpDir) - // os.MkdirAll(tmpDir, 0755) - // fmt.Printf("[DEBUG] tmpdir %q\n", tmpDir) - g.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)) From 05a435933cc80d11e36d5dce032919bb094c33d0 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Thu, 16 Nov 2023 18:41:33 -0500 Subject: [PATCH 06/12] Add support for `--providers-schema` flag to pass in a providers schema JSON file. --- internal/cmd/generate.go | 3 + internal/provider/generate.go | 46 +++++++++++-- internal/provider/generate_test.go | 42 ++++++++++++ internal/provider/testdata/schema.json | 89 ++++++++++++++++++++++++++ internal/provider/util.go | 18 ++++++ internal/provider/util_test.go | 29 +++++++++ 6 files changed, 221 insertions(+), 6 deletions(-) create mode 100644 internal/provider/generate_test.go create mode 100644 internal/provider/testdata/schema.json diff --git a/internal/cmd/generate.go b/internal/cmd/generate.go index 29b25ed5..58dc56c7 100644 --- a/internal/cmd/generate.go +++ b/internal/cmd/generate.go @@ -20,6 +20,7 @@ type generateCmd struct { flagRenderedProviderName string flagProviderDir string + flagProvidersSchema string flagRenderedWebsiteDir string flagExamplesDir string flagWebsiteTmpDir string @@ -73,6 +74,7 @@ func (cmd *generateCmd) Flags() *flag.FlagSet { fs := flag.NewFlagSet("generate", 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 when running the command outside the root provider code directory") + 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 schema generation via Terraform") fs.StringVar(&cmd.flagRenderedProviderName, "rendered-provider-name", "", "provider name, as generated in documentation (ex. page titles, ...)") fs.StringVar(&cmd.flagRenderedWebsiteDir, "rendered-website-dir", "docs", "output directory based on provider-dir") fs.StringVar(&cmd.flagExamplesDir, "examples-dir", "examples", "examples directory based on provider-dir") @@ -99,6 +101,7 @@ func (cmd *generateCmd) runInternal() error { cmd.ui, cmd.flagProviderDir, cmd.flagProviderName, + cmd.flagProvidersSchema, cmd.flagRenderedProviderName, cmd.flagRenderedWebsiteDir, cmd.flagExamplesDir, diff --git a/internal/provider/generate.go b/internal/provider/generate.go index f376d9f6..a303a57a 100644 --- a/internal/provider/generate.go +++ b/internal/provider/generate.go @@ -85,6 +85,7 @@ type generator struct { providerDir string providerName string + providersSchemaPath string renderedProviderName string renderedWebsiteDir string examplesDir string @@ -102,7 +103,7 @@ func (g *generator) warnf(format string, a ...interface{}) { g.ui.Warn(fmt.Sprintf(format, a...)) } -func Generate(ui cli.Ui, providerDir, providerName, renderedProviderName, renderedWebsiteDir, examplesDir, websiteTmpDir, templatesDir, tfVersion string, ignoreDeprecated bool) error { +func Generate(ui cli.Ui, providerDir, providerName, providersSchemaPath, renderedProviderName, renderedWebsiteDir, examplesDir, websiteTmpDir, templatesDir, tfVersion string, ignoreDeprecated bool) error { // Ensure provider directory is resolved absolute path if providerDir == "" { wd, err := os.Getwd() @@ -139,6 +140,7 @@ func Generate(ui cli.Ui, providerDir, providerName, renderedProviderName, render providerDir: providerDir, providerName: providerName, + providersSchemaPath: providersSchemaPath, renderedProviderName: renderedProviderName, renderedWebsiteDir: renderedWebsiteDir, examplesDir: examplesDir, @@ -205,10 +207,20 @@ func (g *generator) Generate(ctx context.Context) error { } } - g.infof("exporting schema from Terraform") - providerSchema, err := g.terraformProviderSchema(ctx) - if err != nil { - return fmt.Errorf("error exporting provider schema from Terraform: %w", err) + var providerSchema *tfjson.ProviderSchema + + if g.providersSchemaPath == "" { + g.infof("exporting schema from Terraform") + providerSchema, err = g.terraformProviderSchemaFromTerraform(ctx) + if err != nil { + return fmt.Errorf("error exporting provider schema from Terraform: %w", err) + } + } else { + g.infof("exporting schema from JSON file") + providerSchema, err = g.terraformProviderSchemaFromFile() + if err != nil { + return fmt.Errorf("error exporting provider schema from JSON file: %w", err) + } } g.infof("rendering missing docs") @@ -571,7 +583,7 @@ func (g *generator) renderStaticWebsite(providerSchema *tfjson.ProviderSchema) e return nil } -func (g *generator) terraformProviderSchema(ctx context.Context) (*tfjson.ProviderSchema, error) { +func (g *generator) terraformProviderSchemaFromTerraform(ctx context.Context) (*tfjson.ProviderSchema, error) { var err error shortName := providerShortName(g.providerName) @@ -661,3 +673,25 @@ provider %[1]q { return nil, fmt.Errorf("unable to find schema in JSON for provider %q", shortName) } + +func (g *generator) terraformProviderSchemaFromFile() (*tfjson.ProviderSchema, error) { + var err error + + shortName := providerShortName(g.providerName) + + g.infof("getting provider schema") + schemas, err := extractSchemaFromFile(g.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/generate_test.go b/internal/provider/generate_test.go new file mode 100644 index 00000000..c2883dc8 --- /dev/null +++ b/internal/provider/generate_test.go @@ -0,0 +1,42 @@ +package provider + +import ( + "testing" + + "github.com/mitchellh/cli" +) + +func TestGenerator_terraformProviderSchemaFromFile(t *testing.T) { + t.Parallel() + + g := &generator{ + ignoreDeprecated: true, + tfVersion: "1.0.0", + + providerDir: "testdata/test-provider-dir", + providerName: "terraform-provider-null", + providersSchemaPath: "testdata/schema.json", + ui: cli.NewMockUi(), + } + + providerSchema, err := g.terraformProviderSchemaFromFile() + if err != nil { + t.Fatalf("error retrieving schema: %q", err) + } + + if providerSchema == nil { + t.Fatalf("provider schema not found") + } + if providerSchema.ResourceSchemas["null_resource"] == nil { + t.Fatalf("null_resource not found") + } + if providerSchema.DataSourceSchemas["null_data_source"] == nil { + t.Fatalf("null_data_source not found") + } + if providerSchema.ResourceSchemas["null_resource"].Block.Attributes["id"] == nil { + t.Fatalf("null_resoruce id attribute not found") + } + if providerSchema.DataSourceSchemas["null_data_source"].Block.Attributes["id"] == nil { + t.Fatalf("null_data_source id attribute not found") + } +} diff --git a/internal/provider/testdata/schema.json b/internal/provider/testdata/schema.json new file mode 100644 index 00000000..868bae22 --- /dev/null +++ b/internal/provider/testdata/schema.json @@ -0,0 +1,89 @@ +{ + "format_version": "1.0", + "provider_schemas": { + "registry.terraform.io/hashicorp/null": { + "provider": { + "version": 0, + "block": { + "description_kind": "plain" + } + }, + "resource_schemas": { + "null_resource": { + "version": 0, + "block": { + "attributes": { + "id": { + "type": "string", + "description": "This is set to a random value at create time.", + "description_kind": "plain", + "computed": true + }, + "triggers": { + "type": [ + "map", + "string" + ], + "description": "A map of arbitrary strings that, when changed, will force the null resource to be replaced, re-running any associated provisioners.", + "description_kind": "plain", + "optional": true + } + }, + "description": "The `null_resource` resource implements the standard resource lifecycle but takes no further action.\n\nThe `triggers` argument allows specifying an arbitrary set of values that, when changed, will cause the resource to be replaced.", + "description_kind": "plain" + } + } + }, + "data_source_schemas": { + "null_data_source": { + "version": 0, + "block": { + "attributes": { + "has_computed_default": { + "type": "string", + "description": "If set, its literal value will be stored and returned. If not, its value defaults to `\"default\"`. This argument exists primarily for testing and has little practical use.", + "description_kind": "plain", + "optional": true, + "computed": true + }, + "id": { + "type": "string", + "description": "This attribute is only present for some legacy compatibility issues and should not be used. It will be removed in a future version.", + "description_kind": "plain", + "deprecated": true, + "computed": true + }, + "inputs": { + "type": [ + "map", + "string" + ], + "description": "A map of arbitrary strings that is copied into the `outputs` attribute, and accessible directly for interpolation.", + "description_kind": "plain", + "optional": true + }, + "outputs": { + "type": [ + "map", + "string" + ], + "description": "After the data source is \"read\", a copy of the `inputs` map.", + "description_kind": "plain", + "computed": true + }, + "random": { + "type": "string", + "description": "A random value. This is primarily for testing and has little practical use; prefer the [hashicorp/random provider](https://registry.terraform.io/providers/hashicorp/random) for more practical random number use-cases.", + "description_kind": "plain", + "computed": true + } + }, + "description": "The `null_data_source` data source implements the standard data source lifecycle but does not\ninteract with any external APIs.\n\nHistorically, the `null_data_source` was typically used to construct intermediate values to re-use elsewhere in configuration. The\nsame can now be achieved using [locals](https://www.terraform.io/docs/language/values/locals.html).\n", + "description_kind": "plain", + "deprecated": true + } + } + } + } + } +} \ No newline at end of file diff --git a/internal/provider/util.go b/internal/provider/util.go index 06aee6b1..ce9acf76 100644 --- a/internal/provider/util.go +++ b/internal/provider/util.go @@ -136,3 +136,21 @@ func fileExists(filename string) bool { } return !info.IsDir() } + +func extractSchemaFromFile(path string) (*tfjson.ProviderSchemas, error) { + schemajson, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("unable to read file %q: %w", path, err) + } + + schemas := &tfjson.ProviderSchemas{ + FormatVersion: "", + Schemas: nil, + } + err = schemas.UnmarshalJSON(schemajson) + if err != nil { + return nil, err + } + + return schemas, nil +} diff --git a/internal/provider/util_test.go b/internal/provider/util_test.go index d25bfdc9..95e59b84 100644 --- a/internal/provider/util_test.go +++ b/internal/provider/util_test.go @@ -85,3 +85,32 @@ func Test_resourceSchema(t *testing.T) { }) } } + +func Test_extractSchemaFromFile(t *testing.T) { + t.Parallel() + + filepath := "testdata/schema.json" + schema, err := extractSchemaFromFile(filepath) + if err != nil { + t.Errorf("received error %v:", err) + } + + providerSchema := schema.Schemas["registry.terraform.io/hashicorp/null"] + if providerSchema == nil { + t.Fatalf("null provider not found") + } + + if providerSchema.ResourceSchemas["null_resource"] == nil { + t.Fatalf("null_resource not found") + } + if providerSchema.DataSourceSchemas["null_data_source"] == nil { + t.Fatalf("null_data_source not found") + } + if providerSchema.ResourceSchemas["null_resource"].Block.Attributes["id"] == nil { + t.Fatalf("null_resoruce id attribute not found") + } + if providerSchema.DataSourceSchemas["null_data_source"].Block.Attributes["id"] == nil { + t.Fatalf("null_data_source id attribute not found") + } + +} From f28c96add300386601e071a2c187724860bf28b2 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Thu, 16 Nov 2023 18:42:45 -0500 Subject: [PATCH 07/12] Add schema json acceptance tests and move provider build tests into a separate folder. --- cmd/tfplugindocs/main_test.go | 14 +- ...k_provider_success_generic_templates.txtar | 0 ...ork_provider_success_named_templates.txtar | 0 ...mework_provider_success_no_templates.txtar | 0 .../generate/null_provider_success.txtar | 0 .../generate/null_provider_success.txtar | 921 ++++++++++++++++++ 6 files changed, 932 insertions(+), 3 deletions(-) rename cmd/tfplugindocs/testdata/scripts/{ => provider-build}/generate/framework_provider_success_generic_templates.txtar (100%) rename cmd/tfplugindocs/testdata/scripts/{ => provider-build}/generate/framework_provider_success_named_templates.txtar (100%) rename cmd/tfplugindocs/testdata/scripts/{ => provider-build}/generate/framework_provider_success_no_templates.txtar (100%) rename cmd/tfplugindocs/testdata/scripts/{ => provider-build}/generate/null_provider_success.txtar (100%) create mode 100644 cmd/tfplugindocs/testdata/scripts/schema-json/generate/null_provider_success.txtar diff --git a/cmd/tfplugindocs/main_test.go b/cmd/tfplugindocs/main_test.go index cd542aae..b8799966 100644 --- a/cmd/tfplugindocs/main_test.go +++ b/cmd/tfplugindocs/main_test.go @@ -18,10 +18,10 @@ func TestMain(m *testing.M) { })) } -func Test_GenerateAcceptanceTests(t *testing.T) { +func Test_ProviderBuild_GenerateAcceptanceTests(t *testing.T) { t.Parallel() if os.Getenv("ACCTEST") == "" { - t.Skip("ACCTEST env var not set; skipping acceptance tests.") + t.Skip("ACCTEST env var not set; skipping provider build acceptance tests.") } // Setting a custom temp dir instead of relying on os.TempDir() // because Terraform providers fail to start up when $TMPDIR @@ -34,7 +34,15 @@ func Test_GenerateAcceptanceTests(t *testing.T) { defer os.RemoveAll(tmpDir) testscript.Run(t, testscript.Params{ - Dir: "testdata/scripts/generate", + Dir: "testdata/scripts/provider-build/generate", WorkdirRoot: tmpDir, }) } + +func Test_SchemaJson_GenerateAcceptanceTests(t *testing.T) { + t.Parallel() + + testscript.Run(t, testscript.Params{ + Dir: "testdata/scripts/schema-json/generate", + }) +} diff --git a/cmd/tfplugindocs/testdata/scripts/generate/framework_provider_success_generic_templates.txtar b/cmd/tfplugindocs/testdata/scripts/provider-build/generate/framework_provider_success_generic_templates.txtar similarity index 100% rename from cmd/tfplugindocs/testdata/scripts/generate/framework_provider_success_generic_templates.txtar rename to cmd/tfplugindocs/testdata/scripts/provider-build/generate/framework_provider_success_generic_templates.txtar diff --git a/cmd/tfplugindocs/testdata/scripts/generate/framework_provider_success_named_templates.txtar b/cmd/tfplugindocs/testdata/scripts/provider-build/generate/framework_provider_success_named_templates.txtar similarity index 100% rename from cmd/tfplugindocs/testdata/scripts/generate/framework_provider_success_named_templates.txtar rename to cmd/tfplugindocs/testdata/scripts/provider-build/generate/framework_provider_success_named_templates.txtar diff --git a/cmd/tfplugindocs/testdata/scripts/generate/framework_provider_success_no_templates.txtar b/cmd/tfplugindocs/testdata/scripts/provider-build/generate/framework_provider_success_no_templates.txtar similarity index 100% rename from cmd/tfplugindocs/testdata/scripts/generate/framework_provider_success_no_templates.txtar rename to cmd/tfplugindocs/testdata/scripts/provider-build/generate/framework_provider_success_no_templates.txtar diff --git a/cmd/tfplugindocs/testdata/scripts/generate/null_provider_success.txtar b/cmd/tfplugindocs/testdata/scripts/provider-build/generate/null_provider_success.txtar similarity index 100% rename from cmd/tfplugindocs/testdata/scripts/generate/null_provider_success.txtar rename to cmd/tfplugindocs/testdata/scripts/provider-build/generate/null_provider_success.txtar diff --git a/cmd/tfplugindocs/testdata/scripts/schema-json/generate/null_provider_success.txtar b/cmd/tfplugindocs/testdata/scripts/schema-json/generate/null_provider_success.txtar new file mode 100644 index 00000000..9ffa5d4b --- /dev/null +++ b/cmd/tfplugindocs/testdata/scripts/schema-json/generate/null_provider_success.txtar @@ -0,0 +1,921 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +# Successful run of tfplugindocs on a stripped-down version of the Null provider with pre-exiting templates, examples, +# docs, and non-tfplugindocs managed docs (docs/cdktf subdirectory). +[!unix] skip +exec tfplugindocs --provider-name=terraform-provider-null --providers-schema=schema.json +cmp stdout expected-output.txt + +-- expected-output.txt -- +rendering website for provider "terraform-provider-null" (as "terraform-provider-null") +copying any existing content to tmp dir +exporting schema from JSON file +getting provider schema +rendering missing docs +generating missing resource content +resource "null_resource" fallback template exists +generating template for "null_resource" +generating missing data source content +resource "null_data_source" fallback template exists +generating template for "null_data_source" +generating missing provider content +provider "terraform-provider-null" template exists, skipping +rendering static website +cleaning rendered website dir +removing directory: "data-sources" +removing file: "index.md" +removing directory: "resources" +rendering templated website to static markdown +rendering "data-sources/data_source.md.tmpl" +rendering "index.md.tmpl" +rendering "resources/resource.md.tmpl" +-- templates/data-sources.md.tmpl -- +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Name}} + +{{ .Description | trimspace }} + +{{ if .HasExample -}} +## Example Usage + +{{ printf "{{tffile %q}}" .ExampleFile }} +{{- end }} + +{{ .SchemaMarkdown | trimspace }} + +{{ if .HasImport -}} +## Import + +Import is supported using the following syntax: + +{{ printf "{{codefile \"shell\" %q}}" .ImportFile }} +{{- end }} +-- templates/index.md.tmpl -- +--- +page_title: "Provider: Null" +description: |- + The null provider provides no-op constructs that can be useful helpers in tricky cases. +--- + +# Null Provider + +The `null` provider is a rather-unusual provider that has constructs that +intentionally do nothing. This may sound strange, and indeed these constructs +do not need to be used in most cases, but they can be useful in various +situations to help orchestrate tricky behavior or work around limitations. + +The documentation of each feature of this provider, accessible via the +navigation, gives examples of situations where these constructs may prove +useful. + +Usage of the `null` provider can make a Terraform configuration harder to +understand. While it can be useful in certain cases, it should be applied with +care and other solutions preferred when available. + +{{ .SchemaMarkdown | trimspace }} +-- templates/resources.md.tmpl -- +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Name}} + +{{ .Description | trimspace }} + +{{ if .HasExample -}} +## Example Usage + +{{ printf "{{tffile %q}}" .ExampleFile }} +{{- end }} + +{{ .SchemaMarkdown | trimspace }} + +{{ if .HasImport -}} +## Import + +Import is supported using the following syntax: + +{{ printf "{{codefile \"shell\" %q}}" .ImportFile }} +{{- end }} + +-- docs/cdktf/python/data-sources/data_source.md -- +--- + + +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "null_data_source Data Source - terraform-provider-null" +subcategory: "" +description: |- + The null_data_source data source implements the standard data source lifecycle but does not + interact with any external APIs. + Historically, the null_data_source was typically used to construct intermediate values to re-use elsewhere in configuration. The + same can now be achieved using locals https://developer.hashicorp.com/terraform/language/values/locals or the terraform_data resource type https://developer.hashicorp.com/terraform/language/resources/terraform-data in Terraform 1.4 and later. +--- + +# null_data_source + +The `null_data_source` data source implements the standard data source lifecycle but does not +interact with any external APIs. + +Historically, the `null_data_source` was typically used to construct intermediate values to re-use elsewhere in configuration. The +same can now be achieved using [locals](https://developer.hashicorp.com/terraform/language/values/locals) or the [terraform_data resource type](https://developer.hashicorp.com/terraform/language/resources/terraform-data) in Terraform 1.4 and later. + +## Example Usage + +```python +# DO NOT EDIT. Code generated by 'cdktf convert' - Please report bugs at https://cdk.tf/bug +from constructs import Construct +from cdktf import Token, TerraformCount, Fn, TerraformOutput, TerraformStack +# +# Provider bindings are generated by running `cdktf get`. +# See https://cdk.tf/provider-generation for more details. +# +from imports.aws.elb import Elb +from imports.aws.instance import Instance +from imports.null.data_null_data_source import DataNullDataSource +class MyConvertedCode(TerraformStack): + def __init__(self, scope, name): + super().__init__(scope, name) + # In most cases loops should be handled in the programming language context and + # not inside of the Terraform context. If you are looping over something external, e.g. a variable or a file input + # you should consider using a for loop. If you are looping over something only known to Terraform, e.g. a result of a data source + # you need to keep this like it is. + blue_count = TerraformCount.of(Token.as_number("3")) + blue = Instance(self, "blue", + ami="ami-0dcc1e21636832c5d", + instance_type="m5.large", + count=blue_count + ) + # In most cases loops should be handled in the programming language context and + # not inside of the Terraform context. If you are looping over something external, e.g. a variable or a file input + # you should consider using a for loop. If you are looping over something only known to Terraform, e.g. a result of a data source + # you need to keep this like it is. + green_count = TerraformCount.of(Token.as_number("3")) + green = Instance(self, "green", + ami="ami-0dcc1e21636832c5d", + instance_type="m5.large", + count=green_count + ) + values = DataNullDataSource(self, "values", + inputs={ + "all_server_ids": Token.as_string( + Fn.concat([ + Fn.lookup_nested(green, ["*", "id"]), + Fn.lookup_nested(blue, ["*", "id"]) + ])), + "all_server_ips": Token.as_string( + Fn.concat([ + Fn.lookup_nested(green, ["*", "private_ip"]), + Fn.lookup_nested(blue, ["*", "private_ip"]) + ])) + } + ) + TerraformOutput(self, "all_server_ids", + value=Fn.lookup_nested(values.outputs, ["\"all_server_ids\""]) + ) + TerraformOutput(self, "all_server_ips", + value=Fn.lookup_nested(values.outputs, ["\"all_server_ips\""]) + ) + Elb(self, "main", + instances=Token.as_list( + Fn.lookup_nested(values.outputs, ["\"all_server_ids\""])), + listener=[ElbListener( + instance_port=8000, + instance_protocol="http", + lb_port=80, + lb_protocol="http" + ) + ] + ) +``` + + +## Schema + +### Optional + +- `has_computed_default` (String) If set, its literal value will be stored and returned. If not, its value defaults to `"default"`. This argument exists primarily for testing and has little practical use. +- `inputs` (Map of String) A map of arbitrary strings that is copied into the `outputs` attribute, and accessible directly for interpolation. + +### Read-Only + +- `id` (String, Deprecated) This attribute is only present for some legacy compatibility issues and should not be used. It will be removed in a future version. +- `outputs` (Map of String) After the data source is "read", a copy of the `inputs` map. +- `random` (String) A random value. This is primarily for testing and has little practical use; prefer the [hashicorp/random provider](https://registry.terraform.io/providers/hashicorp/random) for more practical random number use-cases. + + + +-- docs/cdktf/python/index.md -- +--- +page_title: "Provider: Null" +description: |- + The null provider provides no-op constructs that can be useful helpers in tricky cases. +--- + + + +# Null Provider + +The `null` provider is a rather-unusual provider that has constructs that +intentionally do nothing. This may sound strange, and indeed these constructs +do not need to be used in most cases, but they can be useful in various +situations to help orchestrate tricky behavior or work around limitations. + +The documentation of each feature of this provider, accessible via the +navigation, gives examples of situations where these constructs may prove +useful. + +Usage of the `null` provider can make a Terraform configuration harder to +understand. While it can be useful in certain cases, it should be applied with +care and other solutions preferred when available. + + +## Schema + + +-- docs/cdktf/python/resources/resource.md -- +--- + + +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "null_resource Resource - terraform-provider-null" +subcategory: "" +description: |- + The null_resource resource implements the standard resource lifecycle but takes no further action. On Terraform 1.4 and later, use the terraform_data resource type https://developer.hashicorp.com/terraform/language/resources/terraform-data instead. + The triggers argument allows specifying an arbitrary set of values that, when changed, will cause the resource to be replaced. +--- + +# null_resource + +The `null_resource` resource implements the standard resource lifecycle but takes no further action. On Terraform 1.4 and later, use the [terraform_data resource type](https://developer.hashicorp.com/terraform/language/resources/terraform-data) instead. + +The `triggers` argument allows specifying an arbitrary set of values that, when changed, will cause the resource to be replaced. + +## Example Usage + +```python +# DO NOT EDIT. Code generated by 'cdktf convert' - Please report bugs at https://cdk.tf/bug +from cdktf import SSHProvisionerConnection, FileProvisioner +from constructs import Construct +from cdktf import Token, TerraformCount, Fn, TerraformStack +# +# Provider bindings are generated by running `cdktf get`. +# See https://cdk.tf/provider-generation for more details. +# +from imports.null.resource import Resource +from imports.aws.instance import Instance +class MyConvertedCode(TerraformStack): + def __init__(self, scope, name): + super().__init__(scope, name) + # In most cases loops should be handled in the programming language context and + # not inside of the Terraform context. If you are looping over something external, e.g. a variable or a file input + # you should consider using a for loop. If you are looping over something only known to Terraform, e.g. a result of a data source + # you need to keep this like it is. + cluster_count = TerraformCount.of(Token.as_number("3")) + cluster = Instance(self, "cluster", + ami="ami-0dcc1e21636832c5d", + instance_type="m5.large", + count=cluster_count + ) + null_provider_resource_cluster = Resource(self, "cluster_1", + connection=SSHProvisionerConnection( + host=Fn.element(Fn.lookup_nested(cluster, ["*", "public_ip"]), 0) + ), + triggers=[{ + "cluster_instance_ids": Fn.join(",", + Token.as_list(Fn.lookup_nested(cluster, ["*", "id"]))) + } + ], + provisioners=[FileProvisioner( + type="remote-exec", + inline=["bootstrap-cluster.sh " + + Token.as_string( + Fn.join(" ", + Token.as_list(Fn.lookup_nested(cluster, ["*", "private_ip"])))) + ] + ) + ] + ) + # This allows the Terraform resource name to match the original name. You can remove the call if you don't need them to match. + null_provider_resource_cluster.override_logical_id("cluster") +``` + + +## Schema + +### Optional + +- `triggers` (Map of String) A map of arbitrary strings that, when changed, will force the null resource to be replaced, re-running any associated provisioners. + +### Read-Only + +- `id` (String) This is set to a random value at create time. + + + +-- docs/cdktf/typescript/data-sources/data_source.md -- +--- + + +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "null_data_source Data Source - terraform-provider-null" +subcategory: "" +description: |- + The null_data_source data source implements the standard data source lifecycle but does not + interact with any external APIs. + Historically, the null_data_source was typically used to construct intermediate values to re-use elsewhere in configuration. The + same can now be achieved using locals https://developer.hashicorp.com/terraform/language/values/locals or the terraform_data resource type https://developer.hashicorp.com/terraform/language/resources/terraform-data in Terraform 1.4 and later. +--- + +# null_data_source + +The `nullDataSource` data source implements the standard data source lifecycle but does not +interact with any external APIs. + +Historically, the `nullDataSource` was typically used to construct intermediate values to re-use elsewhere in configuration. The +same can now be achieved using [locals](https://developer.hashicorp.com/terraform/language/values/locals) or the [terraform_data resource type](https://developer.hashicorp.com/terraform/language/resources/terraform-data) in Terraform 1.4 and later. + +## Example Usage + +```typescript +// DO NOT EDIT. Code generated by 'cdktf convert' - Please report bugs at https://cdk.tf/bug +import { Construct } from "constructs"; +import { + Token, + TerraformCount, + Fn, + TerraformOutput, + TerraformStack, +} from "cdktf"; +/* + * Provider bindings are generated by running `cdktf get`. + * See https://cdk.tf/provider-generation for more details. + */ +import { Elb } from "./.gen/providers/aws/elb"; +import { Instance } from "./.gen/providers/aws/instance"; +import { DataNullDataSource } from "./.gen/providers/null/data-null-data-source"; +class MyConvertedCode extends TerraformStack { + constructor(scope: Construct, name: string) { + super(scope, name); + /*In most cases loops should be handled in the programming language context and + not inside of the Terraform context. If you are looping over something external, e.g. a variable or a file input + you should consider using a for loop. If you are looping over something only known to Terraform, e.g. a result of a data source + you need to keep this like it is.*/ + const blueCount = TerraformCount.of(Token.asNumber("3")); + const blue = new Instance(this, "blue", { + ami: "ami-0dcc1e21636832c5d", + instanceType: "m5.large", + count: blueCount, + }); + /*In most cases loops should be handled in the programming language context and + not inside of the Terraform context. If you are looping over something external, e.g. a variable or a file input + you should consider using a for loop. If you are looping over something only known to Terraform, e.g. a result of a data source + you need to keep this like it is.*/ + const greenCount = TerraformCount.of(Token.asNumber("3")); + const green = new Instance(this, "green", { + ami: "ami-0dcc1e21636832c5d", + instanceType: "m5.large", + count: greenCount, + }); + const values = new DataNullDataSource(this, "values", { + inputs: { + all_server_ids: Token.asString( + Fn.concat([ + Fn.lookupNested(green, ["*", "id"]), + Fn.lookupNested(blue, ["*", "id"]), + ]) + ), + all_server_ips: Token.asString( + Fn.concat([ + Fn.lookupNested(green, ["*", "private_ip"]), + Fn.lookupNested(blue, ["*", "private_ip"]), + ]) + ), + }, + }); + new TerraformOutput(this, "all_server_ids", { + value: Fn.lookupNested(values.outputs, ['"all_server_ids"']), + }); + new TerraformOutput(this, "all_server_ips", { + value: Fn.lookupNested(values.outputs, ['"all_server_ips"']), + }); + new Elb(this, "main", { + instances: Token.asList( + Fn.lookupNested(values.outputs, ['"all_server_ids"']) + ), + listener: [ + { + instancePort: 8000, + instanceProtocol: "http", + lbPort: 80, + lbProtocol: "http", + }, + ], + }); + } +} + +``` + + +## Schema + +### Optional + +- `hasComputedDefault` (String) If set, its literal value will be stored and returned. If not, its value defaults to `"default"`. This argument exists primarily for testing and has little practical use. +- `inputs` (Map of String) A map of arbitrary strings that is copied into the `outputs` attribute, and accessible directly for interpolation. + +### Read-Only + +- `id` (String, Deprecated) This attribute is only present for some legacy compatibility issues and should not be used. It will be removed in a future version. +- `outputs` (Map of String) After the data source is "read", a copy of the `inputs` map. +- `random` (String) A random value. This is primarily for testing and has little practical use; prefer the [hashicorp/random provider](https://registry.terraform.io/providers/hashicorp/random) for more practical random number use-cases. + + + +-- docs/cdktf/typescript/index.md -- +--- +page_title: "Provider: Null" +description: |- + The null provider provides no-op constructs that can be useful helpers in tricky cases. +--- + + + +# Null Provider + +The `null` provider is a rather-unusual provider that has constructs that +intentionally do nothing. This may sound strange, and indeed these constructs +do not need to be used in most cases, but they can be useful in various +situations to help orchestrate tricky behavior or work around limitations. + +The documentation of each feature of this provider, accessible via the +navigation, gives examples of situations where these constructs may prove +useful. + +Usage of the `null` provider can make a Terraform configuration harder to +understand. While it can be useful in certain cases, it should be applied with +care and other solutions preferred when available. + + +## Schema + + +-- docs/cdktf/typescript/resources/resource.md -- +--- + + +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "null_resource Resource - terraform-provider-null" +subcategory: "" +description: |- + The null_resource resource implements the standard resource lifecycle but takes no further action. On Terraform 1.4 and later, use the terraform_data resource type https://developer.hashicorp.com/terraform/language/resources/terraform-data instead. + The triggers argument allows specifying an arbitrary set of values that, when changed, will cause the resource to be replaced. +--- + +# null_resource + +The `nullResource` resource implements the standard resource lifecycle but takes no further action. On Terraform 1.4 and later, use the [terraform_data resource type](https://developer.hashicorp.com/terraform/language/resources/terraform-data) instead. + +The `triggers` argument allows specifying an arbitrary set of values that, when changed, will cause the resource to be replaced. + +## Example Usage + +```typescript +// DO NOT EDIT. Code generated by 'cdktf convert' - Please report bugs at https://cdk.tf/bug +import { Construct } from "constructs"; +import { Token, TerraformCount, Fn, TerraformStack } from "cdktf"; +/* + * Provider bindings are generated by running `cdktf get`. + * See https://cdk.tf/provider-generation for more details. + */ +import { Resource } from "./.gen/providers/null/resource"; +import { Instance } from "./.gen/providers/aws/instance"; +class MyConvertedCode extends TerraformStack { + constructor(scope: Construct, name: string) { + super(scope, name); + /*In most cases loops should be handled in the programming language context and + not inside of the Terraform context. If you are looping over something external, e.g. a variable or a file input + you should consider using a for loop. If you are looping over something only known to Terraform, e.g. a result of a data source + you need to keep this like it is.*/ + const clusterCount = TerraformCount.of(Token.asNumber("3")); + const cluster = new Instance(this, "cluster", { + ami: "ami-0dcc1e21636832c5d", + instanceType: "m5.large", + count: clusterCount, + }); + const nullProviderResourceCluster = new Resource(this, "cluster_1", { + connection: { + host: Fn.element(Fn.lookupNested(cluster, ["*", "public_ip"]), 0), + }, + triggers: [ + { + cluster_instance_ids: Fn.join( + ",", + Token.asList(Fn.lookupNested(cluster, ["*", "id"])) + ), + }, + ], + provisioners: [ + { + type: "remote-exec", + inline: [ + "bootstrap-cluster.sh " + + Token.asString( + Fn.join( + " ", + Token.asList(Fn.lookupNested(cluster, ["*", "private_ip"])) + ) + ), + ], + }, + ], + }); + /*This allows the Terraform resource name to match the original name. You can remove the call if you don't need them to match.*/ + nullProviderResourceCluster.overrideLogicalId("cluster"); + } +} + +``` + + +## Schema + +### Optional + +- `triggers` (Map of String) A map of arbitrary strings that, when changed, will force the null resource to be replaced, re-running any associated provisioners. + +### Read-Only + +- `id` (String) This is set to a random value at create time. + + + +-- docs/data-sources/data_source.md -- +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "null_data_source Data Source - terraform-provider-null" +subcategory: "" +description: |- + The null_data_source data source implements the standard data source lifecycle but does not + interact with any external APIs. + Historically, the null_data_source was typically used to construct intermediate values to re-use elsewhere in configuration. The + same can now be achieved using locals https://developer.hashicorp.com/terraform/language/values/locals or the terraform_data resource type https://developer.hashicorp.com/terraform/language/resources/terraform-data in Terraform 1.4 and later. +--- + +# null_data_source + +The `null_data_source` data source implements the standard data source lifecycle but does not +interact with any external APIs. + +Historically, the `null_data_source` was typically used to construct intermediate values to re-use elsewhere in configuration. The +same can now be achieved using [locals](https://developer.hashicorp.com/terraform/language/values/locals) or the [terraform_data resource type](https://developer.hashicorp.com/terraform/language/resources/terraform-data) in Terraform 1.4 and later. + +## Example Usage + +```terraform +resource "aws_instance" "green" { + count = 3 + ami = "ami-0dcc1e21636832c5d" + instance_type = "m5.large" + + # ... +} + +resource "aws_instance" "blue" { + count = 3 + ami = "ami-0dcc1e21636832c5d" + instance_type = "m5.large" + + # ... +} + +data "null_data_source" "values" { + inputs = { + all_server_ids = concat( + aws_instance.green[*].id, + aws_instance.blue[*].id, + ) + all_server_ips = concat( + aws_instance.green[*].private_ip, + aws_instance.blue[*].private_ip, + ) + } +} + +resource "aws_elb" "main" { + instances = data.null_data_source.values.outputs["all_server_ids"] + + # ... + listener { + instance_port = 8000 + instance_protocol = "http" + lb_port = 80 + lb_protocol = "http" + } +} + +output "all_server_ids" { + value = data.null_data_source.values.outputs["all_server_ids"] +} + +output "all_server_ips" { + value = data.null_data_source.values.outputs["all_server_ips"] +} +``` + + +## Schema + +### Optional + +- `has_computed_default` (String) If set, its literal value will be stored and returned. If not, its value defaults to `"default"`. This argument exists primarily for testing and has little practical use. +- `inputs` (Map of String) A map of arbitrary strings that is copied into the `outputs` attribute, and accessible directly for interpolation. + +### Read-Only + +- `id` (String, Deprecated) This attribute is only present for some legacy compatibility issues and should not be used. It will be removed in a future version. +- `outputs` (Map of String) After the data source is "read", a copy of the `inputs` map. +- `random` (String) A random value. This is primarily for testing and has little practical use; prefer the [hashicorp/random provider](https://registry.terraform.io/providers/hashicorp/random) for more practical random number use-cases. + +-- docs/index.md -- +--- +page_title: "Provider: Null" +description: |- + The null provider provides no-op constructs that can be useful helpers in tricky cases. +--- + +# Null Provider + +The `null` provider is a rather-unusual provider that has constructs that +intentionally do nothing. This may sound strange, and indeed these constructs +do not need to be used in most cases, but they can be useful in various +situations to help orchestrate tricky behavior or work around limitations. + +The documentation of each feature of this provider, accessible via the +navigation, gives examples of situations where these constructs may prove +useful. + +Usage of the `null` provider can make a Terraform configuration harder to +understand. While it can be useful in certain cases, it should be applied with +care and other solutions preferred when available. + + +## Schema +-- docs/resources/resource.md -- +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "null_resource Resource - terraform-provider-null" +subcategory: "" +description: |- + The null_resource resource implements the standard resource lifecycle but takes no further action. On Terraform 1.4 and later, use the terraform_data resource type https://developer.hashicorp.com/terraform/language/resources/terraform-data instead. + The triggers argument allows specifying an arbitrary set of values that, when changed, will cause the resource to be replaced. +--- + +# null_resource + +The `null_resource` resource implements the standard resource lifecycle but takes no further action. On Terraform 1.4 and later, use the [terraform_data resource type](https://developer.hashicorp.com/terraform/language/resources/terraform-data) instead. + +The `triggers` argument allows specifying an arbitrary set of values that, when changed, will cause the resource to be replaced. + +## Example Usage + +```terraform +resource "aws_instance" "cluster" { + count = 3 + ami = "ami-0dcc1e21636832c5d" + instance_type = "m5.large" + + # ... +} + +# The primary use-case for the null resource is as a do-nothing container +# for arbitrary actions taken by a provisioner. +# +# In this example, three EC2 instances are created and then a +# null_resource instance is used to gather data about all three +# and execute a single action that affects them all. Due to the triggers +# map, the null_resource will be replaced each time the instance ids +# change, and thus the remote-exec provisioner will be re-run. +resource "null_resource" "cluster" { + # Changes to any instance of the cluster requires re-provisioning + triggers = { + cluster_instance_ids = join(",", aws_instance.cluster[*].id) + } + + # Bootstrap script can run on any instance of the cluster + # So we just choose the first in this case + connection { + host = element(aws_instance.cluster[*].public_ip, 0) + } + + provisioner "remote-exec" { + # Bootstrap script called with private_ip of each node in the cluster + inline = [ + "bootstrap-cluster.sh ${join(" ", + aws_instance.cluster[*].private_ip)}", + ] + } +} +``` + + +## Schema + +### Optional + +- `triggers` (Map of String) A map of arbitrary strings that, when changed, will force the null resource to be replaced, re-running any associated provisioners. + +### Read-Only + +- `id` (String) This is set to a random value at create time. + +-- examples/data-sources/null_data_source/data-source.tf -- +resource "aws_instance" "green" { + count = 3 + ami = "ami-0dcc1e21636832c5d" + instance_type = "m5.large" + + # ... +} + +resource "aws_instance" "blue" { + count = 3 + ami = "ami-0dcc1e21636832c5d" + instance_type = "m5.large" + + # ... +} + +data "null_data_source" "values" { + inputs = { + all_server_ids = concat( + aws_instance.green[*].id, + aws_instance.blue[*].id, + ) + all_server_ips = concat( + aws_instance.green[*].private_ip, + aws_instance.blue[*].private_ip, + ) + } +} + +resource "aws_elb" "main" { + instances = data.null_data_source.values.outputs["all_server_ids"] + + # ... + listener { + instance_port = 8000 + instance_protocol = "http" + lb_port = 80 + lb_protocol = "http" + } +} + +output "all_server_ids" { + value = data.null_data_source.values.outputs["all_server_ids"] +} + +output "all_server_ips" { + value = data.null_data_source.values.outputs["all_server_ips"] +} +-- examples/resources/null_resource/resource.tf -- +resource "aws_instance" "cluster" { + count = 3 + ami = "ami-0dcc1e21636832c5d" + instance_type = "m5.large" + + # ... +} + +# The primary use-case for the null resource is as a do-nothing container +# for arbitrary actions taken by a provisioner. +# +# In this example, three EC2 instances are created and then a +# null_resource instance is used to gather data about all three +# and execute a single action that affects them all. Due to the triggers +# map, the null_resource will be replaced each time the instance ids +# change, and thus the remote-exec provisioner will be re-run. +resource "null_resource" "cluster" { + # Changes to any instance of the cluster requires re-provisioning + triggers = { + cluster_instance_ids = join(",", aws_instance.cluster[*].id) + } + + # Bootstrap script can run on any instance of the cluster + # So we just choose the first in this case + connection { + host = element(aws_instance.cluster[*].public_ip, 0) + } + + provisioner "remote-exec" { + # Bootstrap script called with private_ip of each node in the cluster + inline = [ + "bootstrap-cluster.sh ${join(" ", + aws_instance.cluster[*].private_ip)}", + ] + } +} + +-- schema.json -- +{ + "format_version": "1.0", + "provider_schemas": { + "registry.terraform.io/hashicorp/null": { + "provider": { + "version": 0, + "block": { + "description_kind": "plain" + } + }, + "resource_schemas": { + "null_resource": { + "version": 0, + "block": { + "attributes": { + "id": { + "type": "string", + "description": "This is set to a random value at create time.", + "description_kind": "plain", + "computed": true + }, + "triggers": { + "type": [ + "map", + "string" + ], + "description": "A map of arbitrary strings that, when changed, will force the null resource to be replaced, re-running any associated provisioners.", + "description_kind": "plain", + "optional": true + } + }, + "description": "The `null_resource` resource implements the standard resource lifecycle but takes no further action.\n\nThe `triggers` argument allows specifying an arbitrary set of values that, when changed, will cause the resource to be replaced.", + "description_kind": "plain" + } + } + }, + "data_source_schemas": { + "null_data_source": { + "version": 0, + "block": { + "attributes": { + "has_computed_default": { + "type": "string", + "description": "If set, its literal value will be stored and returned. If not, its value defaults to `\"default\"`. This argument exists primarily for testing and has little practical use.", + "description_kind": "plain", + "optional": true, + "computed": true + }, + "id": { + "type": "string", + "description": "This attribute is only present for some legacy compatibility issues and should not be used. It will be removed in a future version.", + "description_kind": "plain", + "deprecated": true, + "computed": true + }, + "inputs": { + "type": [ + "map", + "string" + ], + "description": "A map of arbitrary strings that is copied into the `outputs` attribute, and accessible directly for interpolation.", + "description_kind": "plain", + "optional": true + }, + "outputs": { + "type": [ + "map", + "string" + ], + "description": "After the data source is \"read\", a copy of the `inputs` map.", + "description_kind": "plain", + "computed": true + }, + "random": { + "type": "string", + "description": "A random value. This is primarily for testing and has little practical use; prefer the [hashicorp/random provider](https://registry.terraform.io/providers/hashicorp/random) for more practical random number use-cases.", + "description_kind": "plain", + "computed": true + } + }, + "description": "The `null_data_source` data source implements the standard data source lifecycle but does not\ninteract with any external APIs.\n\nHistorically, the `null_data_source` was typically used to construct intermediate values to re-use elsewhere in configuration. The\nsame can now be achieved using [locals](https://www.terraform.io/docs/language/values/locals.html).\n", + "description_kind": "plain", + "deprecated": true + } + } + } + } + } +} \ No newline at end of file From e9f15abf9ac9b000170b111187a1d692421b7bc6 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Thu, 16 Nov 2023 18:43:03 -0500 Subject: [PATCH 08/12] Update README.md --- README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2fc6c6bc..ef1d2f86 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ Usage: tfplugindocs generate [] --ignore-deprecated don't generate documentation for deprecated resources and data-sources (default: "false") --provider-dir relative or absolute path to the root provider code directory when running the command outside the root provider code directory --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 schema generation via Terraform --rendered-provider-name provider name, as generated in documentation (ex. page titles, ...) --rendered-website-dir output directory based on provider-dir (default: "docs") --tf-version terraform binary version to download @@ -198,9 +199,15 @@ This can be autogenerated by running `make generate` or running `go generate ./. This repo uses the `testscript` [package](https://pkg.go.dev/github.com/rogpeppe/go-internal/testscript) for acceptance testing. -You can run `make testacc` to run the acceptance tests. By default, the acceptance tests will create a temporary directory in `/tmp/tftmp` for testing but you can change this location in `cmd/tfplugindocs/main_test.go` +There are two types of acceptance tests: full provider build tests in `tfplugindocs/testdata/scripts/provider-build` and provider schema json tests in `tfplugindocs/testdata/scripts/schema-json`. -The test scripts are defined in the `tfplugindocs/testdata/scripts` directory. Each script includes the test, golden files, and the provider source code needed to run the test. +Provider build tests run the default `tfplugindocs` command which builds the provider source code and runs Terraform to retrieve the schema. These tests require the full provider source code to build a valid provider binary. + +Schema json tests run the `tfplugindocs` command with the `--providers-schema=` flag to specify a provider schemas json file. This allows the test to skip the provider build and Terraform run and use the specified file to generate docs. + +You can run `make testacc` to run the full suite of acceptance tests. By default, the provider build acceptance tests will create a temporary directory in `/tmp/tftmp` for testing, but you can change this location in `cmd/tfplugindocs/main_test.go`. The schema json tests uses the `testscript` package's [default work directory](https://pkg.go.dev/github.com/rogpeppe/go-internal/testscript#Params.WorkdirRoot). + +The test scripts are defined in the `tfplugindocs/testdata/scripts` directory. Each script includes the test, golden files, and the provider source code or schema JSON file needed to run the test. Each script is a [text archive](https://pkg.go.dev/golang.org/x/tools/txtar). You can install the `txtar` CLI locally by running `go install golang.org/x/exp/cmd/txtar@latest` to extract the files in the test script for debugging. You can also use `txtar` CLI archive files into the `.txtar` format to create new tests or modify existing ones. From ee3ac15f3bea6855351ae8464f09ffb812fd4419 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Thu, 16 Nov 2023 18:46:08 -0500 Subject: [PATCH 09/12] Add copyright headers --- internal/provider/generate_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/provider/generate_test.go b/internal/provider/generate_test.go index c2883dc8..747117f1 100644 --- a/internal/provider/generate_test.go +++ b/internal/provider/generate_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package provider import ( From e94ed90fb7d0e2f04e33269f438da93491b14d2b Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Fri, 17 Nov 2023 14:45:41 -0500 Subject: [PATCH 10/12] Add more schema json tests --- ...k_provider_success_generic_templates.txtar | 460 ++++++++++++++++++ ...ork_provider_success_named_templates.txtar | 448 +++++++++++++++++ ...mework_provider_success_no_templates.txtar | 217 +++++++++ 3 files changed, 1125 insertions(+) create mode 100644 cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_generic_templates.txtar create mode 100644 cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_named_templates.txtar create mode 100644 cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_no_templates.txtar diff --git a/cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_generic_templates.txtar b/cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_generic_templates.txtar new file mode 100644 index 00000000..996b6ef0 --- /dev/null +++ b/cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_generic_templates.txtar @@ -0,0 +1,460 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +# Successful run of tfplugindocs on a Framework provider with "generic" template paths (i.e. templates/resources.md.tmpl) +# Templates test all implemented data fields and functions. +[!unix] skip +exec tfplugindocs --provider-name=terraform-provider-scaffolding --providers-schema=schema.json +cmp stdout expected-output.txt +cmpenv docs/index.md expected-index.md +cmpenv docs/data-sources/example.md expected-datasource.md +cmpenv docs/resources/example.md expected-resource.md + +-- expected-output.txt -- +rendering website for provider "terraform-provider-scaffolding" (as "terraform-provider-scaffolding") +copying any existing content to tmp dir +exporting schema from JSON file +getting provider schema +rendering missing docs +generating missing resource content +resource "scaffolding_example" fallback template exists +generating template for "scaffolding_example" +generating missing data source content +resource "scaffolding_example" fallback template exists +generating template for "scaffolding_example" +generating missing provider content +provider "terraform-provider-scaffolding" template exists, skipping +rendering static website +cleaning rendered website dir +rendering templated website to static markdown +rendering "data-sources/example.md.tmpl" +rendering "index.md.tmpl" +rendering "resources/example.md.tmpl" +-- expected-datasource.md -- +# Data Fields + +Name: scaffolding_example +Type: Data Source +Description: Example data source +HasExample: true +ExampleFile: $WORK/examples/data-sources/scaffolding_example/data-source.tf +HasImport: false +ProviderName: terraform-provider-scaffolding +ProviderShortName: scaffolding +RenderedProviderName: terraform-provider-scaffolding +SchemaMarkdown: +## Schema + +### Optional + +- `configurable_attribute` (String) Example configurable attribute + +### Read-Only + +- `id` (String) Example identifier + + + +# Functions + +lower: data source +plainmarkdown: Data Source +prefixlines: Prefix: Data Source +split: [scaffolding example] +title: Data Source +trimspace: Data Source +upper: DATA SOURCE + +# Conditionals and File Functions + +printf codefile: + + +printf tffile: +## Example Usage + +```terraform +data "scaffolding_example" "example" { + configurable_attribute = "some-value" +} +``` + +codefile: + + +tffile: +## Example Usage + +```terraform +data "scaffolding_example" "example" { + configurable_attribute = "some-value" +} +``` +-- expected-index.md -- +# Data Fields + +Description: Example provider +HasExample: true +ExampleFile: $WORK/examples/provider/provider.tf +ProviderName: terraform-provider-scaffolding +ProviderShortName: scaffolding +RenderedProviderName: terraform-provider-scaffolding +SchemaMarkdown: +## Schema + +### Optional + +- `endpoint` (String) Example provider attribute + + + +# Functions + +lower: terraform-provider-scaffolding +plainmarkdown: terraform-provider-scaffolding +prefixlines: Prefix: terraform-provider-scaffolding +split: [terraform provider scaffolding] +title: Terraform-Provider-Scaffolding +trimspace: terraform-provider-scaffolding +upper: TERRAFORM-PROVIDER-SCAFFOLDING + +# Conditionals and File Functions + +printf tffile: +## Example Usage + +{{tffile "$WORK/examples/provider/provider.tf"}} + +tffile: +## Example Usage + +```terraform +provider "scaffolding" { + # example configuration here +} +``` +-- expected-resource.md -- +# Data Fields + +Name: scaffolding_example +Type: Resource +Description: Example resource +HasExample: true +ExampleFile: $WORK/examples/resources/scaffolding_example/resource.tf +HasImport: true +ImportFile: $WORK/examples/resources/scaffolding_example/import.sh +ProviderName: terraform-provider-scaffolding +ProviderShortName: scaffolding +RenderedProviderName: terraform-provider-scaffolding +SchemaMarkdown: +## Schema + +### Optional + +- `configurable_attribute` (String) Example configurable attribute +- `defaulted` (String) Example configurable attribute with default value + +### Read-Only + +- `id` (String) Example identifier + + + +# Functions + +lower: resource +plainmarkdown: Resource +prefixlines: Prefix: Resource +split: [scaffolding example] +title: Resource +trimspace: Resource +upper: RESOURCE + +# Conditionals and File Functions + +printf codefile: +## Import + +Import is supported using the following syntax: + +```shell +terraform import scaffolding_example.example +``` + +printf tffile: +## Example Usage + +```terraform +resource "scaffolding_example" "example" { + configurable_attribute = "some-value" +} +``` + +codefile: +## Import + +Import is supported using the following syntax: + +```shell +terraform import scaffolding_example.example +``` + +tffile: +## Example Usage + +```terraform +resource "scaffolding_example" "example" { + configurable_attribute = "some-value" +} +``` +-- templates/data-sources.md.tmpl -- +# Data Fields + +Name: {{.Name}} +Type: {{.Type}} +Description: {{.Description}} +HasExample: {{.HasExample}} +ExampleFile: {{.ExampleFile}} +HasImport: {{.HasImport}} +ProviderName: {{.ProviderName}} +ProviderShortName: {{.ProviderShortName}} +RenderedProviderName: {{.RenderedProviderName}} +SchemaMarkdown: {{.SchemaMarkdown}} + +# Functions + +lower: {{ .Type | lower }} +plainmarkdown: {{ .Type | plainmarkdown }} +prefixlines: {{ .Type | prefixlines "Prefix: " }} +split: {{ split .Name "_" }} +title: {{ .Type | title }} +trimspace: {{ .Type | trimspace }} +upper: {{ .Type | upper }} + +# Conditionals and File Functions + +printf codefile: +{{ if .HasImport -}} +## Import + +Import is supported using the following syntax: + +{{ printf "{{codefile \"shell\" %q}}" .ImportFile }} +{{- end }} + +printf tffile: +{{ if .HasExample -}} +## Example Usage + +{{ printf "{{tffile %q}}" .ExampleFile }} +{{- end }} + +codefile: +{{ if .HasImport -}} +## Import + +Import is supported using the following syntax: + +{{codefile "shell" .ImportFile }} +{{- end }} + +tffile: +{{ if .HasExample -}} +## Example Usage + +{{tffile .ExampleFile }} +{{- end }} +-- templates/index.md.tmpl -- +# Data Fields + +Description: {{.Description}} +HasExample: {{.HasExample}} +ExampleFile: {{.ExampleFile}} +ProviderName: {{.ProviderName}} +ProviderShortName: {{.ProviderShortName}} +RenderedProviderName: {{.RenderedProviderName}} +SchemaMarkdown: {{.SchemaMarkdown}} + +# Functions + +lower: {{ .ProviderName | lower }} +plainmarkdown: {{ .ProviderName | plainmarkdown }} +prefixlines: {{ .ProviderName | prefixlines "Prefix: " }} +split: {{ split .ProviderName "-" }} +title: {{ .ProviderName | title }} +trimspace: {{ .ProviderName | trimspace }} +upper: {{ .ProviderName | upper }} + +# Conditionals and File Functions + +printf tffile: +{{ if .HasExample -}} +## Example Usage + +{{ printf "{{tffile %q}}" .ExampleFile }} +{{- end }} + +tffile: +{{ if .HasExample -}} +## Example Usage + +{{tffile .ExampleFile }} +{{- end }} +-- templates/resources.md.tmpl -- +# Data Fields + +Name: {{.Name}} +Type: {{.Type}} +Description: {{.Description}} +HasExample: {{.HasExample}} +ExampleFile: {{.ExampleFile}} +HasImport: {{.HasImport}} +ImportFile: {{.ImportFile}} +ProviderName: {{.ProviderName}} +ProviderShortName: {{.ProviderShortName}} +RenderedProviderName: {{.RenderedProviderName}} +SchemaMarkdown: {{.SchemaMarkdown}} + +# Functions + +lower: {{ .Type | lower }} +plainmarkdown: {{ .Type | plainmarkdown }} +prefixlines: {{ .Type | prefixlines "Prefix: " }} +split: {{ split .Name "_" }} +title: {{ .Type | title }} +trimspace: {{ .Type | trimspace }} +upper: {{ .Type | upper }} + +# Conditionals and File Functions + +printf codefile: +{{ if .HasImport -}} +## Import + +Import is supported using the following syntax: + +{{ printf "{{codefile \"shell\" %q}}" .ImportFile }} +{{- end }} + +printf tffile: +{{ if .HasExample -}} +## Example Usage + +{{ printf "{{tffile %q}}" .ExampleFile }} +{{- end }} + +codefile: +{{ if .HasImport -}} +## Import + +Import is supported using the following syntax: + +{{codefile "shell" .ImportFile }} +{{- end }} + +tffile: +{{ if .HasExample -}} +## Example Usage + +{{tffile .ExampleFile }} +{{- end }} +-- examples/README.md -- +# Examples + +This directory contains examples that are mostly used for documentation, but can also be run/tested manually via the Terraform CLI. + +The document generation tool looks for files in the following locations by default. All other *.tf files besides the ones mentioned below are ignored by the documentation tool. This is useful for creating examples that can run and/or ar testable even if some parts are not relevant for the documentation. + +* **provider/provider.tf** example file for the provider index page +* **data-sources/`full data source name`/data-source.tf** example file for the named data source page +* **resources/`full resource name`/resource.tf** example file for the named data source page +-- examples/data-sources/scaffolding_example/data-source.tf -- +data "scaffolding_example" "example" { + configurable_attribute = "some-value" +} +-- examples/provider/provider.tf -- +provider "scaffolding" { + # example configuration here +} +-- examples/resources/scaffolding_example/resource.tf -- +resource "scaffolding_example" "example" { + configurable_attribute = "some-value" +} +-- examples/resources/scaffolding_example/import.sh -- +terraform import scaffolding_example.example +-- 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" + } + } + } + } + } +} \ No newline at end of file diff --git a/cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_named_templates.txtar b/cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_named_templates.txtar new file mode 100644 index 00000000..dc1722c2 --- /dev/null +++ b/cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_named_templates.txtar @@ -0,0 +1,448 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +# Successful run of tfplugindocs on a Framework provider with "named" template paths (i.e. templates/resources/.md.tmpl) +# Templates test all implemented data fields and functions. +[!unix] skip +exec tfplugindocs --provider-name=terraform-provider-scaffolding --providers-schema=schema.json +cmp stdout expected-output.txt +cmpenv docs/index.md expected-index.md +cmpenv docs/data-sources/example.md expected-datasource.md +cmpenv docs/resources/example.md expected-resource.md + +-- expected-output.txt -- +rendering website for provider "terraform-provider-scaffolding" (as "terraform-provider-scaffolding") +copying any existing content to tmp dir +exporting schema from JSON file +getting provider schema +rendering missing docs +generating missing resource content +resource "scaffolding_example" template exists, skipping +generating missing data source content +resource "scaffolding_example" template exists, skipping +generating missing provider content +provider "terraform-provider-scaffolding" template exists, skipping +rendering static website +cleaning rendered website dir +rendering templated website to static markdown +rendering "data-sources/example.md.tmpl" +rendering "index.md.tmpl" +rendering "resources/example.md.tmpl" +-- expected-datasource.md -- +# Data Fields + +Name: scaffolding_example +Type: Data Source +Description: Example data source +HasExample: true +ExampleFile: $WORK/examples/data-sources/scaffolding_example/data-source.tf +HasImport: false +ProviderName: terraform-provider-scaffolding +ProviderShortName: scaffolding +RenderedProviderName: terraform-provider-scaffolding +SchemaMarkdown: +## Schema + +### Optional + +- `configurable_attribute` (String) Example configurable attribute + +### Read-Only + +- `id` (String) Example identifier + + + +# Functions + +lower: data source +plainmarkdown: Data Source +prefixlines: Prefix: Data Source +split: [scaffolding example] +title: Data Source +trimspace: Data Source +upper: DATA SOURCE + +# Conditionals and File Functions + +printf codefile: + + +printf tffile: +## Example Usage + +{{tffile "$WORK/examples/data-sources/scaffolding_example/data-source.tf"}} + +codefile: + + +tffile: +## Example Usage + +```terraform +data "scaffolding_example" "example" { + configurable_attribute = "some-value" +} +``` +-- expected-index.md -- +# Data Fields + +Description: Example provider +HasExample: true +ExampleFile: $WORK/examples/provider/provider.tf +ProviderName: terraform-provider-scaffolding +ProviderShortName: scaffolding +RenderedProviderName: terraform-provider-scaffolding +SchemaMarkdown: +## Schema + +### Optional + +- `endpoint` (String) Example provider attribute + + + +# Functions + +lower: terraform-provider-scaffolding +plainmarkdown: terraform-provider-scaffolding +prefixlines: Prefix: terraform-provider-scaffolding +split: [terraform provider scaffolding] +title: Terraform-Provider-Scaffolding +trimspace: terraform-provider-scaffolding +upper: TERRAFORM-PROVIDER-SCAFFOLDING + +# Conditionals and File Functions + +printf tffile: +## Example Usage + +{{tffile "$WORK/examples/provider/provider.tf"}} + +tffile: +## Example Usage + +```terraform +provider "scaffolding" { + # example configuration here +} +``` +-- expected-resource.md -- +# Data Fields + +Name: scaffolding_example +Type: Resource +Description: Example resource +HasExample: true +ExampleFile: $WORK/examples/resources/scaffolding_example/resource.tf +HasImport: true +ImportFile: $WORK/examples/resources/scaffolding_example/import.sh +ProviderName: terraform-provider-scaffolding +ProviderShortName: scaffolding +RenderedProviderName: terraform-provider-scaffolding +SchemaMarkdown: +## Schema + +### Optional + +- `configurable_attribute` (String) Example configurable attribute +- `defaulted` (String) Example configurable attribute with default value + +### Read-Only + +- `id` (String) Example identifier + + + +# Functions + +lower: resource +plainmarkdown: Resource +prefixlines: Prefix: Resource +split: [scaffolding example] +title: Resource +trimspace: Resource +upper: RESOURCE + +# Conditionals and File Functions + +printf codefile: +## Import + +Import is supported using the following syntax: + +{{codefile "shell" "$WORK/examples/resources/scaffolding_example/import.sh"}} + +printf tffile: +## Example Usage + +{{tffile "$WORK/examples/resources/scaffolding_example/resource.tf"}} + +codefile: +## Import + +Import is supported using the following syntax: + +```shell +terraform import scaffolding_example.example +``` + +tffile: +## Example Usage + +```terraform +resource "scaffolding_example" "example" { + configurable_attribute = "some-value" +} +``` +-- templates/data-sources/example.md.tmpl -- +# Data Fields + +Name: {{.Name}} +Type: {{.Type}} +Description: {{.Description}} +HasExample: {{.HasExample}} +ExampleFile: {{.ExampleFile}} +HasImport: {{.HasImport}} +ProviderName: {{.ProviderName}} +ProviderShortName: {{.ProviderShortName}} +RenderedProviderName: {{.RenderedProviderName}} +SchemaMarkdown: {{.SchemaMarkdown}} + +# Functions + +lower: {{ .Type | lower }} +plainmarkdown: {{ .Type | plainmarkdown }} +prefixlines: {{ .Type | prefixlines "Prefix: " }} +split: {{ split .Name "_" }} +title: {{ .Type | title }} +trimspace: {{ .Type | trimspace }} +upper: {{ .Type | upper }} + +# Conditionals and File Functions + +printf codefile: +{{ if .HasImport -}} +## Import + +Import is supported using the following syntax: + +{{ printf "{{codefile \"shell\" %q}}" .ImportFile }} +{{- end }} + +printf tffile: +{{ if .HasExample -}} +## Example Usage + +{{ printf "{{tffile %q}}" .ExampleFile }} +{{- end }} + +codefile: +{{ if .HasImport -}} +## Import + +Import is supported using the following syntax: + +{{codefile "shell" .ImportFile }} +{{- end }} + +tffile: +{{ if .HasExample -}} +## Example Usage + +{{tffile .ExampleFile }} +{{- end }} +-- templates/index.md.tmpl -- +# Data Fields + +Description: {{.Description}} +HasExample: {{.HasExample}} +ExampleFile: {{.ExampleFile}} +ProviderName: {{.ProviderName}} +ProviderShortName: {{.ProviderShortName}} +RenderedProviderName: {{.RenderedProviderName}} +SchemaMarkdown: {{.SchemaMarkdown}} + +# Functions + +lower: {{ .ProviderName | lower }} +plainmarkdown: {{ .ProviderName | plainmarkdown }} +prefixlines: {{ .ProviderName | prefixlines "Prefix: " }} +split: {{ split .ProviderName "-" }} +title: {{ .ProviderName | title }} +trimspace: {{ .ProviderName | trimspace }} +upper: {{ .ProviderName | upper }} + +# Conditionals and File Functions + +printf tffile: +{{ if .HasExample -}} +## Example Usage + +{{ printf "{{tffile %q}}" .ExampleFile }} +{{- end }} + +tffile: +{{ if .HasExample -}} +## Example Usage + +{{tffile .ExampleFile }} +{{- end }} +-- templates/resources/example.md.tmpl -- +# Data Fields + +Name: {{.Name}} +Type: {{.Type}} +Description: {{.Description}} +HasExample: {{.HasExample}} +ExampleFile: {{.ExampleFile}} +HasImport: {{.HasImport}} +ImportFile: {{.ImportFile}} +ProviderName: {{.ProviderName}} +ProviderShortName: {{.ProviderShortName}} +RenderedProviderName: {{.RenderedProviderName}} +SchemaMarkdown: {{.SchemaMarkdown}} + +# Functions + +lower: {{ .Type | lower }} +plainmarkdown: {{ .Type | plainmarkdown }} +prefixlines: {{ .Type | prefixlines "Prefix: " }} +split: {{ split .Name "_" }} +title: {{ .Type | title }} +trimspace: {{ .Type | trimspace }} +upper: {{ .Type | upper }} + +# Conditionals and File Functions + +printf codefile: +{{ if .HasImport -}} +## Import + +Import is supported using the following syntax: + +{{ printf "{{codefile \"shell\" %q}}" .ImportFile }} +{{- end }} + +printf tffile: +{{ if .HasExample -}} +## Example Usage + +{{ printf "{{tffile %q}}" .ExampleFile }} +{{- end }} + +codefile: +{{ if .HasImport -}} +## Import + +Import is supported using the following syntax: + +{{codefile "shell" .ImportFile }} +{{- end }} + +tffile: +{{ if .HasExample -}} +## Example Usage + +{{tffile .ExampleFile }} +{{- end }} +-- examples/README.md -- +# Examples + +This directory contains examples that are mostly used for documentation, but can also be run/tested manually via the Terraform CLI. + +The document generation tool looks for files in the following locations by default. All other *.tf files besides the ones mentioned below are ignored by the documentation tool. This is useful for creating examples that can run and/or ar testable even if some parts are not relevant for the documentation. + +* **provider/provider.tf** example file for the provider index page +* **data-sources/`full data source name`/data-source.tf** example file for the named data source page +* **resources/`full resource name`/resource.tf** example file for the named data source page +-- examples/data-sources/scaffolding_example/data-source.tf -- +data "scaffolding_example" "example" { + configurable_attribute = "some-value" +} +-- examples/provider/provider.tf -- +provider "scaffolding" { + # example configuration here +} +-- examples/resources/scaffolding_example/resource.tf -- +resource "scaffolding_example" "example" { + configurable_attribute = "some-value" +} +-- examples/resources/scaffolding_example/import.sh -- +terraform import scaffolding_example.example +-- 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" + } + } + } + } + } +} \ No newline at end of file diff --git a/cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_no_templates.txtar b/cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_no_templates.txtar new file mode 100644 index 00000000..00f55847 --- /dev/null +++ b/cmd/tfplugindocs/testdata/scripts/schema-json/generate/framework_provider_success_no_templates.txtar @@ -0,0 +1,217 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +# Successful run of tfplugindocs on a Framework provider with examples but no templates or pre-exiting docs. +[!unix] skip +exec tfplugindocs --provider-name=terraform-provider-scaffolding --providers-schema=schema.json +cmp stdout expected-output.txt +cmp docs/index.md expected-index.md +cmp docs/data-sources/example.md expected-datasource.md +cmp docs/resources/example.md expected-resource.md + +-- expected-output.txt -- +rendering website for provider "terraform-provider-scaffolding" (as "terraform-provider-scaffolding") +exporting schema from JSON file +getting provider schema +rendering missing docs +generating missing resource content +generating template for "scaffolding_example" +generating missing data source content +generating template for "scaffolding_example" +generating missing provider content +generating template for "terraform-provider-scaffolding" +rendering static website +cleaning rendered website dir +rendering templated website to static markdown +rendering "data-sources/example.md.tmpl" +rendering "index.md.tmpl" +rendering "resources/example.md.tmpl" +-- expected-datasource.md -- +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "scaffolding_example Data Source - terraform-provider-scaffolding" +subcategory: "" +description: |- + Example data source +--- + +# scaffolding_example (Data Source) + +Example data source + +## Example Usage + +```terraform +data "scaffolding_example" "example" { + configurable_attribute = "some-value" +} +``` + + +## Schema + +### Optional + +- `configurable_attribute` (String) Example configurable attribute + +### Read-Only + +- `id` (String) Example identifier +-- expected-index.md -- +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "scaffolding Provider" +subcategory: "" +description: |- + Example provider +--- + +# scaffolding Provider + +Example provider + +## Example Usage + +```terraform +provider "scaffolding" { + # example configuration here +} +``` + + +## Schema + +### Optional + +- `endpoint` (String) Example provider attribute +-- expected-resource.md -- +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "scaffolding_example Resource - terraform-provider-scaffolding" +subcategory: "" +description: |- + Example resource +--- + +# scaffolding_example (Resource) + +Example resource + +## Example Usage + +```terraform +resource "scaffolding_example" "example" { + configurable_attribute = "some-value" +} +``` + + +## Schema + +### Optional + +- `configurable_attribute` (String) Example configurable attribute +- `defaulted` (String) Example configurable attribute with default value + +### Read-Only + +- `id` (String) Example identifier +-- examples/README.md -- +# Examples + +This directory contains examples that are mostly used for documentation, but can also be run/tested manually via the Terraform CLI. + +The document generation tool looks for files in the following locations by default. All other *.tf files besides the ones mentioned below are ignored by the documentation tool. This is useful for creating examples that can run and/or ar testable even if some parts are not relevant for the documentation. + +* **provider/provider.tf** example file for the provider index page +* **data-sources/`full data source name`/data-source.tf** example file for the named data source page +* **resources/`full resource name`/resource.tf** example file for the named data source page +-- examples/data-sources/scaffolding_example/data-source.tf -- +data "scaffolding_example" "example" { + configurable_attribute = "some-value" +} +-- examples/provider/provider.tf -- +provider "scaffolding" { + # example configuration here +} +-- examples/resources/scaffolding_example/resource.tf -- +resource "scaffolding_example" "example" { + configurable_attribute = "some-value" +} + +-- 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" + } + } + } + } + } +} \ No newline at end of file From 285ccd1cbb6bbde16298a50a499ece64eb3bce72 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Fri, 17 Nov 2023 15:11:33 -0500 Subject: [PATCH 11/12] Add changelog entry --- .changes/unreleased/ENHANCEMENTS-20231117-151029.yaml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changes/unreleased/ENHANCEMENTS-20231117-151029.yaml diff --git a/.changes/unreleased/ENHANCEMENTS-20231117-151029.yaml b/.changes/unreleased/ENHANCEMENTS-20231117-151029.yaml new file mode 100644 index 00000000..c5744540 --- /dev/null +++ b/.changes/unreleased/ENHANCEMENTS-20231117-151029.yaml @@ -0,0 +1,6 @@ +kind: ENHANCEMENTS +body: 'generate: Add `provider-schema` flag to pass in a file path to a provider schemas + file, allowing the command to skip schema generation' +time: 2023-11-17T15:10:29.850914-05:00 +custom: + Issue: "299" From 4bdcc79145fc8cc5211c8d69fdc86fa1fbc21c36 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Mon, 20 Nov 2023 13:45:29 -0500 Subject: [PATCH 12/12] Apply documentation suggestions Co-authored-by: Brian Flad --- .changes/unreleased/ENHANCEMENTS-20231117-151029.yaml | 4 ++-- README.md | 4 ++-- internal/cmd/generate.go | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.changes/unreleased/ENHANCEMENTS-20231117-151029.yaml b/.changes/unreleased/ENHANCEMENTS-20231117-151029.yaml index c5744540..4ebbfee0 100644 --- a/.changes/unreleased/ENHANCEMENTS-20231117-151029.yaml +++ b/.changes/unreleased/ENHANCEMENTS-20231117-151029.yaml @@ -1,6 +1,6 @@ kind: ENHANCEMENTS -body: 'generate: Add `provider-schema` flag to pass in a file path to a provider schemas - file, allowing the command to skip schema generation' +body: 'generate: Add `provider-schema` flag to pass in a file path to a provider schema JSON + file, allowing the command to skip building the provider and calling Terraform CLI' time: 2023-11-17T15:10:29.850914-05:00 custom: Issue: "299" diff --git a/README.md b/README.md index ef1d2f86..bf82fb63 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Usage: tfplugindocs generate [] --ignore-deprecated don't generate documentation for deprecated resources and data-sources (default: "false") --provider-dir relative or absolute path to the root provider code directory when running the command outside the root provider code directory --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 schema generation via Terraform + --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 --rendered-provider-name provider name, as generated in documentation (ex. page titles, ...) --rendered-website-dir output directory based on provider-dir (default: "docs") --tf-version terraform binary version to download @@ -203,7 +203,7 @@ There are two types of acceptance tests: full provider build tests in `tfplugind Provider build tests run the default `tfplugindocs` command which builds the provider source code and runs Terraform to retrieve the schema. These tests require the full provider source code to build a valid provider binary. -Schema json tests run the `tfplugindocs` command with the `--providers-schema=` flag to specify a provider schemas json file. This allows the test to skip the provider build and Terraform run and use the specified file to generate docs. +Schema json tests run the `tfplugindocs` command with the `--providers-schema=` flag to specify a provider schemas json file. This allows the test to skip the provider build and Terraform CLI call, instead using the specified file to generate docs. You can run `make testacc` to run the full suite of acceptance tests. By default, the provider build acceptance tests will create a temporary directory in `/tmp/tftmp` for testing, but you can change this location in `cmd/tfplugindocs/main_test.go`. The schema json tests uses the `testscript` package's [default work directory](https://pkg.go.dev/github.com/rogpeppe/go-internal/testscript#Params.WorkdirRoot). diff --git a/internal/cmd/generate.go b/internal/cmd/generate.go index 58dc56c7..a62907a1 100644 --- a/internal/cmd/generate.go +++ b/internal/cmd/generate.go @@ -74,7 +74,7 @@ func (cmd *generateCmd) Flags() *flag.FlagSet { fs := flag.NewFlagSet("generate", 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 when running the command outside the root provider code directory") - 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 schema generation via Terraform") + 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.flagRenderedProviderName, "rendered-provider-name", "", "provider name, as generated in documentation (ex. page titles, ...)") fs.StringVar(&cmd.flagRenderedWebsiteDir, "rendered-website-dir", "docs", "output directory based on provider-dir") fs.StringVar(&cmd.flagExamplesDir, "examples-dir", "examples", "examples directory based on provider-dir")