You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
terraformDummyRepo2/vendor/github.com/hashicorp/terraform-plugin-docs/internal/provider/generate.go

634 lines
19 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/hashicorp/go-version"
install "github.com/hashicorp/hc-install"
"github.com/hashicorp/hc-install/checkpoint"
"github.com/hashicorp/hc-install/fs"
"github.com/hashicorp/hc-install/product"
"github.com/hashicorp/hc-install/releases"
"github.com/hashicorp/hc-install/src"
"github.com/hashicorp/terraform-exec/tfexec"
tfjson "github.com/hashicorp/terraform-json"
"github.com/mitchellh/cli"
)
var (
examplesResourceFileTemplate = resourceFileTemplate("resources/{{.Name}}/resource.tf")
examplesResourceImportTemplate = resourceFileTemplate("resources/{{.Name}}/import.sh")
examplesDataSourceFileTemplate = resourceFileTemplate("data-sources/{{ .Name }}/data-source.tf")
examplesProviderFileTemplate = providerFileTemplate("provider/provider.tf")
websiteResourceFileTemplate = resourceFileTemplate("resources/{{ .ShortName }}.md.tmpl")
websiteResourceFallbackFileTemplate = resourceFileTemplate("resources.md.tmpl")
websiteResourceFileStatic = []resourceFileTemplate{
resourceFileTemplate("resources/{{ .ShortName }}.md"),
// TODO: warn for all of these, as they won't render? massage them to the proper output file name?
resourceFileTemplate("resources/{{ .ShortName }}.markdown"),
resourceFileTemplate("resources/{{ .ShortName }}.html.markdown"),
resourceFileTemplate("resources/{{ .ShortName }}.html.md"),
resourceFileTemplate("r/{{ .ShortName }}.markdown"),
resourceFileTemplate("r/{{ .ShortName }}.md"),
resourceFileTemplate("r/{{ .ShortName }}.html.markdown"),
resourceFileTemplate("r/{{ .ShortName }}.html.md"),
}
websiteDataSourceFileTemplate = resourceFileTemplate("data-sources/{{ .ShortName }}.md.tmpl")
websiteDataSourceFallbackFileTemplate = resourceFileTemplate("data-sources.md.tmpl")
websiteDataSourceFileStatic = []resourceFileTemplate{
resourceFileTemplate("data-sources/{{ .ShortName }}.md"),
// TODO: warn for all of these, as they won't render? massage them to the proper output file name?
resourceFileTemplate("data-sources/{{ .ShortName }}.markdown"),
resourceFileTemplate("data-sources/{{ .ShortName }}.html.markdown"),
resourceFileTemplate("data-sources/{{ .ShortName }}.html.md"),
resourceFileTemplate("d/{{ .ShortName }}.markdown"),
resourceFileTemplate("d/{{ .ShortName }}.md"),
resourceFileTemplate("d/{{ .ShortName }}.html.markdown"),
resourceFileTemplate("d/{{ .ShortName }}.html.md"),
}
websiteProviderFileTemplate = providerFileTemplate("index.md.tmpl")
websiteProviderFileStatic = []providerFileTemplate{
providerFileTemplate("index.markdown"),
providerFileTemplate("index.md"),
providerFileTemplate("index.html.markdown"),
providerFileTemplate("index.html.md"),
}
)
type generator struct {
ignoreDeprecated bool
tfVersion string
// providerDir is the absolute path to the root provider directory
providerDir string
providerName string
renderedProviderName string
renderedWebsiteDir string
examplesDir string
templatesDir string
websiteTmpDir string
ui cli.Ui
}
func (g *generator) infof(format string, a ...interface{}) {
g.ui.Info(fmt.Sprintf(format, a...))
}
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 {
// Ensure provider directory is resolved absolute path
if providerDir == "" {
wd, err := os.Getwd()
if err != nil {
return fmt.Errorf("error getting working directory: %w", err)
}
providerDir = wd
} else {
absProviderDir, err := filepath.Abs(providerDir)
if err != nil {
return fmt.Errorf("error getting absolute path with provider directory %q: %w", providerDir, err)
}
providerDir = absProviderDir
}
// Verify provider directory
providerDirFileInfo, err := os.Stat(providerDir)
if err != nil {
return fmt.Errorf("error getting information for provider directory %q: %w", providerDir, err)
}
if !providerDirFileInfo.IsDir() {
return fmt.Errorf("expected %q to be a directory", providerDir)
}
g := &generator{
ignoreDeprecated: ignoreDeprecated,
tfVersion: tfVersion,
providerDir: providerDir,
providerName: providerName,
renderedProviderName: renderedProviderName,
renderedWebsiteDir: renderedWebsiteDir,
examplesDir: examplesDir,
templatesDir: templatesDir,
websiteTmpDir: websiteTmpDir,
ui: ui,
}
ctx := context.Background()
return g.Generate(ctx)
}
func (g *generator) Generate(ctx context.Context) error {
var err error
providerName := g.providerName
if g.providerName == "" {
providerName = filepath.Base(g.providerDir)
}
if g.renderedProviderName == "" {
g.renderedProviderName = providerName
}
g.infof("rendering website for provider %q (as %q)", providerName, g.renderedProviderName)
switch {
case g.websiteTmpDir == "":
g.websiteTmpDir, err = os.MkdirTemp("", "tfws")
if err != nil {
return 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
}
g.infof("creating tmp dir %q", g.websiteTmpDir)
err = os.MkdirAll(g.websiteTmpDir, 0755)
if err != nil {
return err
}
}
templatesDirInfo, err := os.Stat(g.ProviderTemplatesDir())
switch {
case os.IsNotExist(err):
// do nothing, no template dir
case err != nil:
return err
default:
if !templatesDirInfo.IsDir() {
return fmt.Errorf("template path is not a directory: %s", g.ProviderTemplatesDir())
}
g.infof("copying any existing content to tmp dir")
err = cp(g.ProviderTemplatesDir(), g.TempTemplatesDir())
if err != nil {
return err
}
}
g.infof("exporting schema from Terraform")
providerSchema, err := g.terraformProviderSchema(ctx, providerName)
if err != nil {
return err
}
g.infof("rendering missing docs")
err = g.renderMissingDocs(providerName, providerSchema)
if err != nil {
return err
}
g.infof("rendering static website")
err = g.renderStaticWebsite(providerName, providerSchema)
if err != nil {
return err
}
return nil
}
// ProviderDocsDir returns the absolute path to the joined provider and
// given website documentation directory, which defaults to "docs".
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 {
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 {
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
// copied.
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)
if err != nil {
return fmt.Errorf("unable to render path for resource %q: %w", name, err)
}
tmplPath = filepath.Join(g.TempTemplatesDir(), tmplPath)
if fileExists(tmplPath) {
g.infof("resource %q template exists, skipping", name)
return nil
}
for _, candidate := range websiteStaticCandidateTemplates {
candidatePath, err := candidate.Render(g.providerDir, name, providerName)
if err != nil {
return fmt.Errorf("unable to render path for resource %q: %w", name, err)
}
candidatePath = filepath.Join(g.TempTemplatesDir(), candidatePath)
if fileExists(candidatePath) {
g.infof("resource %q static file exists, skipping", name)
return nil
}
}
examplePath, err := examplesFileTemplate.Render(g.providerDir, name, providerName)
if err != nil {
return fmt.Errorf("unable to render example file path for %q: %w", name, err)
}
if examplePath != "" {
examplePath = filepath.Join(g.ProviderExamplesDir(), examplePath)
}
if !fileExists(examplePath) {
examplePath = ""
}
importPath := ""
if examplesImportTemplate != nil {
importPath, err = examplesImportTemplate.Render(g.providerDir, name, providerName)
if err != nil {
return fmt.Errorf("unable to render example import file path for %q: %w", name, err)
}
if importPath != "" {
importPath = filepath.Join(g.ProviderExamplesDir(), importPath)
}
if !fileExists(importPath) {
importPath = ""
}
}
targetResourceTemplate := defaultResourceTemplate
fallbackTmplPath, err := fallbackWebsiteFileTemplate.Render(g.providerDir, name, providerName)
if err != nil {
return fmt.Errorf("unable to render path for resource %q: %w", name, err)
}
fallbackTmplPath = filepath.Join(g.TempTemplatesDir(), fallbackTmplPath)
if fileExists(fallbackTmplPath) {
g.infof("resource %q fallback template exists", name)
tmplData, err := os.ReadFile(fallbackTmplPath)
if err != nil {
return fmt.Errorf("unable to read file %q: %w", fallbackTmplPath, err)
}
targetResourceTemplate = resourceTemplate(tmplData)
}
g.infof("generating template for %q", name)
md, err := targetResourceTemplate.Render(g.providerDir, name, providerName, g.renderedProviderName, typeName, examplePath, importPath, schema)
if err != nil {
return fmt.Errorf("unable to render template for %q: %w", name, err)
}
err = writeFile(tmplPath, md)
if err != nil {
return fmt.Errorf("unable to write file %q: %w", tmplPath, err)
}
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)
if err != nil {
return fmt.Errorf("unable to render path for provider %q: %w", providerName, err)
}
tmplPath = filepath.Join(g.TempTemplatesDir(), tmplPath)
if fileExists(tmplPath) {
g.infof("provider %q template exists, skipping", providerName)
return nil
}
for _, candidate := range websiteStaticCandidateTemplates {
candidatePath, err := candidate.Render(g.providerDir, providerName)
if err != nil {
return fmt.Errorf("unable to render path for provider %q: %w", providerName, err)
}
candidatePath = filepath.Join(g.TempTemplatesDir(), candidatePath)
if fileExists(candidatePath) {
g.infof("provider %q static file exists, skipping", providerName)
return nil
}
}
examplePath, err := examplesFileTemplate.Render(g.providerDir, providerName)
if err != nil {
return fmt.Errorf("unable to render example file path for %q: %w", providerName, err)
}
if examplePath != "" {
examplePath = filepath.Join(g.ProviderExamplesDir(), examplePath)
}
if !fileExists(examplePath) {
examplePath = ""
}
g.infof("generating template for %q", providerName)
md, err := defaultProviderTemplate.Render(g.providerDir, providerName, g.renderedProviderName, examplePath, schema)
if err != nil {
return fmt.Errorf("unable to render template for %q: %w", providerName, err)
}
err = writeFile(tmplPath, md)
if err != nil {
return fmt.Errorf("unable to write file %q: %w", tmplPath, err)
}
return nil
}
func (g *generator) renderMissingDocs(providerName string, 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,
websiteResourceFileTemplate,
websiteResourceFallbackFileTemplate,
websiteResourceFileStatic,
examplesResourceFileTemplate,
&examplesResourceImportTemplate)
if err != nil {
return fmt.Errorf("unable to render doc %q: %w", name, err)
}
}
g.infof("generating missing data source content")
for name, schema := range providerSchema.DataSourceSchemas {
if g.ignoreDeprecated && schema.Block.Deprecated {
continue
}
err := g.renderMissingResourceDoc(providerName, name, "Data Source", schema,
websiteDataSourceFileTemplate,
websiteDataSourceFallbackFileTemplate,
websiteDataSourceFileStatic,
examplesDataSourceFileTemplate,
nil)
if err != nil {
return fmt.Errorf("unable to render doc %q: %w", name, err)
}
}
g.infof("generating missing provider content")
err := g.renderMissingProviderDoc(providerName, providerSchema.ConfigSchema,
websiteProviderFileTemplate,
websiteProviderFileStatic,
examplesProviderFileTemplate,
)
if err != nil {
return fmt.Errorf("unable to render provider doc: %w", err)
}
return nil
}
func (g *generator) renderStaticWebsite(providerName string, providerSchema *tfjson.ProviderSchema) error {
g.infof("cleaning rendered website dir")
err := os.RemoveAll(g.ProviderDocsDir())
if err != nil {
return err
}
shortName := providerShortName(providerName)
g.infof("rendering templated website to static markdown")
err = filepath.Walk(g.websiteTmpDir, func(path string, info os.FileInfo, _ error) error {
if info.IsDir() {
// skip directories
return nil
}
rel, err := filepath.Rel(filepath.Join(g.TempTemplatesDir()), path)
if err != nil {
return err
}
relDir, relFile := filepath.Split(rel)
relDir = filepath.ToSlash(relDir)
// skip special top-level generic resource and data source templates
if relDir == "" && (relFile == "resources.md.tmpl" || relFile == "data-sources.md.tmpl") {
return nil
}
renderedPath := filepath.Join(g.ProviderDocsDir(), rel)
err = os.MkdirAll(filepath.Dir(renderedPath), 0755)
if err != nil {
return err
}
ext := filepath.Ext(path)
if ext != ".tmpl" {
g.infof("copying non-template file: %q", rel)
return cp(path, renderedPath)
}
renderedPath = strings.TrimSuffix(renderedPath, ext)
tmplData, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("unable to read file %q: %w", rel, err)
}
out, err := os.Create(renderedPath)
if err != nil {
return err
}
defer out.Close()
g.infof("rendering %q", rel)
switch relDir {
case "data-sources/":
resSchema, resName := resourceSchema(providerSchema.DataSourceSchemas, shortName, relFile)
exampleFilePath := filepath.Join(g.ProviderExamplesDir(), "data-sources", resName, "data-source.tf")
if resSchema != nil {
tmpl := resourceTemplate(tmplData)
render, err := tmpl.Render(g.providerDir, resName, providerName, g.renderedProviderName, "Data Source", exampleFilePath, "", resSchema)
if err != nil {
return fmt.Errorf("unable to render data source template %q: %w", rel, err)
}
_, err = out.WriteString(render)
if err != nil {
return fmt.Errorf("unable to write rendered string: %w", err)
}
return nil
}
g.warnf("data source entitled %q, or %q does not exist", shortName, resName)
case "resources/":
resSchema, resName := resourceSchema(providerSchema.ResourceSchemas, shortName, relFile)
exampleFilePath := filepath.Join(g.ProviderExamplesDir(), "resources", resName, "resource.tf")
importFilePath := filepath.Join(g.ProviderExamplesDir(), "resources", resName, "import.sh")
if resSchema != nil {
tmpl := resourceTemplate(tmplData)
render, err := tmpl.Render(g.providerDir, resName, providerName, g.renderedProviderName, "Resource", exampleFilePath, importFilePath, resSchema)
if err != nil {
return fmt.Errorf("unable to render resource template %q: %w", rel, err)
}
_, err = out.WriteString(render)
if err != nil {
return fmt.Errorf("unable to write regindered string: %w", err)
}
return nil
}
g.warnf("resource entitled %q, or %q does not exist", shortName, resName)
case "": // provider
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)
if err != nil {
return fmt.Errorf("unable to render provider template %q: %w", rel, err)
}
_, err = out.WriteString(render)
if err != nil {
return fmt.Errorf("unable to write rendered string: %w", err)
}
return nil
}
}
tmpl := docTemplate(tmplData)
err = tmpl.Render(g.providerDir, out)
if err != nil {
return fmt.Errorf("unable to render template %q: %w", rel, err)
}
return nil
})
if err != nil {
return err
}
return nil
}
func (g *generator) terraformProviderSchema(ctx context.Context, providerName string) (*tfjson.ProviderSchema, error) {
var err error
shortName := providerShortName(providerName)
tmpDir, err := os.MkdirTemp("", "tfws")
if err != nil {
return nil, err
}
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))
switch runtime.GOOS {
case "windows":
outFile = outFile + ".exe"
}
buildCmd := exec.Command("go", "build", "-o", outFile)
buildCmd.Dir = g.providerDir
// TODO: constrain env here to make it a little safer?
_, err = runCmd(buildCmd)
if err != nil {
return nil, err
}
err = writeFile(filepath.Join(tmpDir, "provider.tf"), fmt.Sprintf(`
provider %[1]q {
}
`, shortName))
if err != nil {
return nil, err
}
i := install.NewInstaller()
var sources []src.Source
if g.tfVersion != "" {
g.infof("downloading Terraform CLI binary version from releases.hashicorp.com: %s", g.tfVersion)
sources = []src.Source{
&releases.ExactVersion{
Product: product.Terraform,
Version: version.Must(version.NewVersion(g.tfVersion)),
InstallDir: tmpDir,
},
}
} else {
g.infof("using Terraform CLI binary from PATH if available, otherwise downloading latest Terraform CLI binary")
sources = []src.Source{
&fs.AnyVersion{
Product: &product.Terraform,
},
&checkpoint.LatestVersion{
InstallDir: tmpDir,
Product: product.Terraform,
},
}
}
tfBin, err := i.Ensure(context.Background(), sources)
if err != nil {
return nil, err
}
tf, err := tfexec.NewTerraform(tmpDir, tfBin)
if err != nil {
return nil, err
}
g.infof("running terraform init")
err = tf.Init(ctx, tfexec.Get(false), tfexec.PluginDir("./plugins"))
if err != nil {
return nil, err
}
g.infof("getting provider schema")
schemas, err := tf.ProvidersSchema(ctx)
if err != nil {
return nil, 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)
}