From 3e86e3a75fe81fb760b7470ad7de6540a227201a Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Wed, 6 Dec 2023 16:55:31 -0500 Subject: [PATCH 01/17] Add migrate subcommand --- cmd/tfplugindocs/main_test.go | 9 + .../migrate/time_provider_success.txtar | 462 ++++++++++++++++++ internal/cmd/migrate.go | 96 ++++ internal/cmd/run.go | 9 + internal/provider/migrate.go | 147 ++++++ 5 files changed, 723 insertions(+) create mode 100644 cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success.txtar create mode 100644 internal/cmd/migrate.go create mode 100644 internal/provider/migrate.go diff --git a/cmd/tfplugindocs/main_test.go b/cmd/tfplugindocs/main_test.go index b8799966..173c303d 100644 --- a/cmd/tfplugindocs/main_test.go +++ b/cmd/tfplugindocs/main_test.go @@ -46,3 +46,12 @@ func Test_SchemaJson_GenerateAcceptanceTests(t *testing.T) { Dir: "testdata/scripts/schema-json/generate", }) } + +func Test_SchemaJson_MigrateAcceptanceTests(t *testing.T) { + t.Parallel() + + testscript.Run(t, testscript.Params{ + Dir: "testdata/scripts/schema-json/migrate", + WorkdirRoot: "/Users/sgoods/test", + }) +} diff --git a/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success.txtar b/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success.txtar new file mode 100644 index 00000000..d105f3d9 --- /dev/null +++ b/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success.txtar @@ -0,0 +1,462 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +# Successful run of tfplugindocs -migrate on the Time provider +[!unix] skip + +# Check tool tips +exec tfplugindocs migrate -help +#cmp stderr expected-help-output.txt + +# Run migrate command +exec tfplugindocs migrate +#cmp stdout expected-output.txt +cmpenv website/docs/index.html.markdown templates/index.html.markdown +cmpenv website/docs/r/offset.html.markdown templates/resources/offset.html.markdown + +-- expected-output.txt -- +-- expected-help-output.txt -- + +Usage: tfplugindocs migrate [] + + --new-website-source-dir new website templates directory based on provider-dir (default: "templates") + --old-website-source-dir old website directory based on provider-dir (default: "website") + --provider-dir relative or absolute path to the root provider code directory when running the command outside the root provider code directory + + +-- website/docs/index.html.markdown -- +--- +layout: "time" +page_title: "Provider: Time" +description: |- + The time provider is used to interact with time-based resources. +--- + +# Time Provider + +The time provider is used to interact with time-based resources. The provider itself has no configuration options. + +Use the navigation to the left to read about the available resources. + +## Resource "Triggers" + +Certain time resources, only perform actions during specific lifecycle actions: + +- `time_offset`: Saves base timestamp into Terraform state only when created. +- `time_sleep`: Sleeps when created and/or destroyed. +- `time_static`: Saves base timestamp into Terraform state only when created. + +These resources provide an optional map argument called `triggers` that can be populated with arbitrary key/value pairs. When the keys or values of this argument are updated, Terraform will re-perform the desired action, such as updating the base timestamp or sleeping again. + +For example: + +```hcl +resource "time_static" "ami_update" { + triggers = { + # Save the time each switch of an AMI id + ami_id = data.aws_ami.example.id + } +} + +resource "aws_instance" "server" { + # Read the AMI id "through" the time_static resource to ensure that + # both will change together. + ami = time_static.ami_update.triggers.ami_id + + tags = { + AmiUpdateTime = time_static.ami_update.rfc3339 + } + + # ... (other aws_instance arguments) ... +} +``` + +`triggers` are *not* treated as sensitive attributes; a value used for `triggers` will be displayed in Terraform UI output as plaintext. + +To force a these actions to reoccur without updating `triggers`, the [`terraform taint` command](https://www.terraform.io/docs/commands/taint.html) can be used to produce the action on the next run. +-- website/docs/r/offset.html.markdown -- +--- +layout: "time" +page_title: "Time: time_offset" +description: |- + Manages a offset time resource. +--- + +# Resource: time_offset + +Manages an offset time resource, which keeps an UTC timestamp stored in the Terraform state that is offset from a locally sourced base timestamp. This prevents perpetual differences caused by using the [`timestamp()` function](https://www.terraform.io/docs/configuration/functions/timestamp.html). + +-> Further manipulation of incoming or outgoing values can be accomplished with the [`formatdate()` function](https://www.terraform.io/docs/configuration/functions/formatdate.html) and the [`timeadd()` function](https://www.terraform.io/docs/configuration/functions/timeadd.html). + +## Example Usage + +### Basic Usage + +```hcl +resource "time_offset" "example" { + offset_days = 7 +} + +output "one_week_from_now" { + value = time_offset.example.rfc3339 +} +``` + +### Triggers Usage + +```hcl +resource "time_offset" "ami_update" { + triggers = { + # Save the time each switch of an AMI id + ami_id = data.aws_ami.example.id + } + + offset_days = 7 +} + +resource "aws_instance" "server" { + # Read the AMI id "through" the time_offset resource to ensure that + # both will change together. + ami = time_offset.ami_update.triggers.ami_id + + tags = { + ExpirationTime = time_offset.ami_update.rfc3339 + } + + # ... (other aws_instance arguments) ... +} +``` + +## Argument Reference + +~> **NOTE:** At least one of the `offset_` arguments must be configured. + +The following arguments are optional: + +* `base_rfc3339` - (Optional) Configure the base timestamp with an UTC [RFC3339 time string](https://tools.ietf.org/html/rfc3339#section-5.8) (`YYYY-MM-DDTHH:MM:SSZ`). Defaults to the current time. +* `triggers` - (Optional) Arbitrary map of values that, when changed, will trigger a new base timestamp value to be saved. See [the main provider documentation](../index.html) for more information. +* `offset_days` - (Optional) Number of days to offset the base timestamp. Conflicts with other `offset_` arguments. +* `offset_hours` - (Optional) Number of hours to offset the base timestamp. Conflicts with other `offset_` arguments. +* `offset_minutes` - (Optional) Number of minutes to offset the base timestamp. Conflicts with other `offset_` arguments. +* `offset_months` - (Optional) Number of months to offset the base timestamp. Conflicts with other `offset_` arguments. +* `offset_seconds` - (Optional) Number of seconds to offset the base timestamp. Conflicts with other `offset_` arguments. +* `offset_years` - (Optional) Number of years to offset the base timestamp. Conflicts with other `offset_` arguments. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `day` - Number day of offset timestamp. +* `hour` - Number hour of offset timestamp. +* `id` - UTC RFC3339 format of the base timestamp, e.g. `2020-02-12T06:36:13Z`. +* `minute` - Number minute of offset timestamp. +* `month` - Number month of offset timestamp. +* `rfc3339` - UTC RFC3339 format of the offset timestamp, e.g. `2020-02-12T06:36:13Z`. +* `second` - Number second of offset timestamp. +* `unix` - Number of seconds since epoch time, e.g. `1581489373`. +* `year` - Number year of offset timestamp. + +## Import + +This resource can be imported using the base UTC RFC3339 timestamp and offset years, months, days, hours, minutes, and seconds, separated by commas (`,`), e.g. + +```console +$ terraform import time_offset.example 2020-02-12T06:36:13Z,0,0,7,0,0,0 +``` + +The `triggers` argument cannot be imported. +-- website/docs/r/rotating.html.markdown -- +--- +layout: "time" +page_title: "Time: time_rotating" +description: |- + Manages a rotating time resource. +--- + +# Resource: time_rotating + +Manages a rotating time resource, which keeps a rotating UTC timestamp stored in the Terraform state and proposes resource recreation when the locally sourced current time is beyond the rotation time. This rotation only occurs when Terraform is executed, meaning there will be drift between the rotation timestamp and actual rotation. The new rotation timestamp offset includes this drift. This prevents perpetual differences caused by using the [`timestamp()` function](https://www.terraform.io/docs/configuration/functions/timestamp.html) by only forcing a new value on the set cadence. + +-> Further manipulation of incoming or outgoing values can be accomplished with the [`formatdate()` function](https://www.terraform.io/docs/configuration/functions/formatdate.html) and the [`timeadd()` function](https://www.terraform.io/docs/configuration/functions/timeadd.html). + +## Example Usage + +This example configuration will rotate (destroy/create) the resource every 30 days. + +```hcl +resource "time_rotating" "example" { + rotation_days = 30 +} +``` + +## Argument Reference + +~> **NOTE:** At least one of the `rotation_` arguments must be configured. + +The following arguments are optional: + +* `rfc3339` - (Optional) Configure the base timestamp with an UTC [RFC3339 time string](https://tools.ietf.org/html/rfc3339#section-5.8) (`YYYY-MM-DDTHH:MM:SSZ`). Defaults to the current time. +* `rotation_days` - (Optional) Number of days to add to the base timestamp to configure the rotation timestamp. When the current time has passed the rotation timestamp, the resource will trigger recreation. Conflicts with other `rotation_` arguments. +* `rotation_hours` - (Optional) Number of hours to add to the base timestamp to configure the rotation timestamp. When the current time has passed the rotation timestamp, the resource will trigger recreation. Conflicts with other `rotation_` arguments. +* `rotation_minutes` - (Optional) Number of minutes to add to the base timestamp to configure the rotation timestamp. When the current time has passed the rotation timestamp, the resource will trigger recreation. Conflicts with other `rotation_` arguments. +* `rotation_months` - (Optional) Number of months to add to the base timestamp to configure the rotation timestamp. When the current time has passed the rotation timestamp, the resource will trigger recreation. Conflicts with other `rotation_` arguments. +* `rotation_rfc3339` - (Optional) Configure the rotation timestamp with an UTC [RFC3339 time string](https://tools.ietf.org/html/rfc3339#section-5.8) (`YYYY-MM-DDTHH:MM:SSZ`). When the current time has passed the rotation timestamp, the resource will trigger recreation. Conflicts with other `rotation_` arguments. +* `rotation_years` - (Optional) Number of years to add to the base timestamp to configure the rotation timestamp. When the current time has passed the rotation timestamp, the resource will trigger recreation. Conflicts with other `rotation_` arguments. +* `triggers` - (Optional) Arbitrary map of values that, when changed, will trigger a new base timestamp value to be saved. These conditions recreate the resource in addition to other rotation arguments. See [the main provider documentation](../index.html) for more information. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `day` - Number day of timestamp. +* `hour` - Number hour of timestamp. +* `id` - UTC RFC3339 format of the base timestamp, e.g. `2020-02-12T06:36:13Z`. +* `minute` - Number minute of timestamp. +* `month` - Number month of timestamp. +* `second` - Number second of timestamp. +* `unix` - Number of seconds since epoch time, e.g. `1581489373`. +* `year` - Number year of timestamp. + +## Import + +This resource can be imported using the base UTC RFC3339 value and rotation years, months, days, hours, and minutes, separated by commas (`,`), e.g. for 30 days + +```console +$ terraform import time_rotation.example 2020-02-12T06:36:13Z,0,0,30,0,0 +``` + +Otherwise, to import with the rotation RFC3339 value, the base UTC RFC3339 value and rotation UTC RFC3339 value, separated by commas (`,`), e.g. + +```console +$ terraform import time_rotation.example 2020-02-12T06:36:13Z,2020-02-13T06:36:13Z +``` + +The `triggers` argument cannot be imported. +-- website/docs/r/sleep.html.markdown -- +--- +layout: "time" +page_title: "Time: time_sleep" +description: |- + Manages a static time resource. +--- + +# Resource: time_sleep + +Manages a resource that delays creation and/or destruction, typically for further resources. This prevents cross-platform compatibility and destroy-time issues with using the [`local-exec` provisioner](https://www.terraform.io/docs/provisioners/local-exec.html). + +-> In many cases, this resource should be considered a workaround for issues that should be reported and handled in downstream Terraform Provider logic. Downstream resources can usually introduce or adjust retries in their code to handle time delay issues for all Terraform configurations or upstream resources can be improved to better wait for a resource to be fully ready and available. + +## Example Usage + +### Delay Create Usage + +```hcl +# This resource will destroy (potentially immediately) after null_resource.next +resource "null_resource" "previous" {} + +resource "time_sleep" "wait_30_seconds" { + depends_on = [null_resource.previous] + + create_duration = "30s" +} + +# This resource will create (at least) 30 seconds after null_resource.previous +resource "null_resource" "next" { + depends_on = [time_sleep.wait_30_seconds] +} +``` + +### Delay Destroy Usage + +```hcl +# This resource will destroy (at least) 30 seconds after null_resource.next +resource "null_resource" "previous" {} + +resource "time_sleep" "wait_30_seconds" { + depends_on = [null_resource.previous] + + destroy_duration = "30s" +} + +# This resource will create (potentially immediately) after null_resource.previous +resource "null_resource" "next" { + depends_on = [time_sleep.wait_30_seconds] +} +``` + +### Triggers Usage + +```hcl +resource "aws_ram_resource_association" "example" { + resource_arn = aws_subnet.example.arn + resource_share_arn = aws_ram_resource_share.example.arn +} + +# AWS resources shared via Resource Access Manager can take a few seconds to +# propagate across AWS accounts after RAM returns a successful association. +resource "time_sleep" "ram_resource_propagation" { + create_duration = "60s" + + triggers = { + # This sets up a proper dependency on the RAM association + subnet_arn = aws_ram_resource_association.example.resource_arn + subnet_id = aws_subnet.example.id + } +} + +resource "aws_db_subnet_group" "example" { + name = "example" + + # Read the Subnet identifier "through" the time_sleep resource to ensure a + # proper dependency and that both will change together. + subnet_ids = [time_sleep.ram_resource_propagation.triggers["subnet_id"]] +} +``` + +## Argument Reference + +The following arguments are optional: + +* `create_duration` - (Optional) [Time duration][1] to delay resource creation. For example, `30s` for 30 seconds or `5m` for 5 minutes. Updating this value by itself will not trigger a delay. +* `destroy_duration` - (Optional) [Time duration][1] to delay resource destroy. For example, `30s` for 30 seconds or `5m` for 5 minutes. Updating this value by itself will not trigger a delay. This value or any updates to it must be successfully applied into the Terraform state before destroying this resource to take effect. +* `triggers` - (Optional) Arbitrary map of values that, when changed, will run any creation or destroy delays again. See [the main provider documentation](../index.html) for more information. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - UTC RFC3339 timestamp of the creation or import, e.g. `2020-02-12T06:36:13Z`. + +## Import + +This resource can be imported with the `create_duration` and `destroy_duration`, separated by a comma (`,`). + +e.g. For 30 seconds create duration with no destroy duration: + +```console +$ terraform import time_sleep.example 30s, +``` + +e.g. For 30 seconds destroy duration with no create duration: + +```console +$ terraform import time_sleep.example ,30s +``` + +The `triggers` argument cannot be imported. + +[1]: https://golang.org/pkg/time/#ParseDuration +-- website/docs/r/static.html.markdown -- +--- +layout: "time" +page_title: "Time: time_static" +description: |- + Manages a static time resource. +--- + +# Resource: time_static + +Manages a static time resource, which keeps a locally sourced UTC timestamp stored in the Terraform state. This prevents perpetual differences caused by using the [`timestamp()` function](https://www.terraform.io/docs/configuration/functions/timestamp.html). + +-> Further manipulation of incoming or outgoing values can be accomplished with the [`formatdate()` function](https://www.terraform.io/docs/configuration/functions/formatdate.html) and the [`timeadd()` function](https://www.terraform.io/docs/configuration/functions/timeadd.html). + +## Example Usage + +### Basic Usage + +```hcl +resource "time_static" "example" {} + +output "current_time" { + value = time_static.example.rfc3339 +} +``` + +### Triggers Usage + +```hcl +resource "time_static" "ami_update" { + triggers = { + # Save the time each switch of an AMI id + ami_id = data.aws_ami.example.id + } +} + +resource "aws_instance" "server" { + # Read the AMI id "through" the time_static resource to ensure that + # both will change together. + ami = time_static.ami_update.triggers.ami_id + + tags = { + AmiUpdateTime = time_static.ami_update.rfc3339 + } + + # ... (other aws_instance arguments) ... +} +``` + +## Argument Reference + +The following arguments are optional: + +* `triggers` - (Optional) Arbitrary map of values that, when changed, will trigger a new base timestamp value to be saved. See [the main provider documentation](../index.html) for more information. +* `rfc3339` - (Optional) Configure the base timestamp with an UTC [RFC3339 time string](https://tools.ietf.org/html/rfc3339#section-5.8) (`YYYY-MM-DDTHH:MM:SSZ`). Defaults to the current time. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `day` - Number day of timestamp. +* `hour` - Number hour of timestamp. +* `id` - UTC RFC3339 timestamp format, e.g. `2020-02-12T06:36:13Z`. +* `minute` - Number minute of timestamp. +* `month` - Number month of timestamp. +* `rfc3339` - UTC RFC3339 format of timestamp, e.g. `2020-02-12T06:36:13Z`. +* `second` - Number second of timestamp. +* `unix` - Number of seconds since epoch time, e.g. `1581489373`. +* `year` - Number year of timestamp. + +## Import + +This resource can be imported using the UTC RFC3339 value, e.g. + +```console +$ terraform import time_static.example 2020-02-12T06:36:13Z +``` + +The `triggers` argument cannot be imported. +-- website/time.erb -- +<% wrap_layout :inner do %> + <% content_for :sidebar do %> + + <% end %> + <%= yield %> +<% end %> + diff --git a/internal/cmd/migrate.go b/internal/cmd/migrate.go new file mode 100644 index 00000000..fb0e90e9 --- /dev/null +++ b/internal/cmd/migrate.go @@ -0,0 +1,96 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package cmd + +import ( + "flag" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-docs/internal/provider" +) + +type migrateCmd struct { + commonCmd + + flagProviderDir string + flagOldWebsiteSourceDir string + flagNewWebsiteSourceDir string +} + +func (cmd *migrateCmd) Synopsis() string { + return "migrates website files from old directory structure (website/docs/r) to tfplugindocs supported structure (templates/)" +} + +func (cmd *migrateCmd) Help() string { + strBuilder := &strings.Builder{} + + longestName := 0 + longestUsage := 0 + cmd.Flags().VisitAll(func(f *flag.Flag) { + if len(f.Name) > longestName { + longestName = len(f.Name) + } + if len(f.Usage) > longestUsage { + longestUsage = len(f.Usage) + } + }) + + strBuilder.WriteString("\nUsage: tfplugindocs migrate []\n\n") + cmd.Flags().VisitAll(func(f *flag.Flag) { + if f.DefValue != "" { + strBuilder.WriteString(fmt.Sprintf(" --%s %s%s%s (default: %q)\n", + f.Name, + strings.Repeat(" ", longestName-len(f.Name)+2), + f.Usage, + strings.Repeat(" ", longestUsage-len(f.Usage)+2), + f.DefValue, + )) + } else { + strBuilder.WriteString(fmt.Sprintf(" --%s %s%s%s\n", + f.Name, + strings.Repeat(" ", longestName-len(f.Name)+2), + f.Usage, + strings.Repeat(" ", longestUsage-len(f.Usage)+2), + )) + } + }) + strBuilder.WriteString("\n") + + return strBuilder.String() +} + +func (cmd *migrateCmd) Flags() *flag.FlagSet { + fs := flag.NewFlagSet("migrate", flag.ExitOnError) + + 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.flagOldWebsiteSourceDir, "old-website-source-dir", "website", "old website directory based on provider-dir") + fs.StringVar(&cmd.flagNewWebsiteSourceDir, "new-website-source-dir", "templates", "new website templates directory based on provider-dir") + return fs +} + +func (cmd *migrateCmd) Run(args []string) int { + fs := cmd.Flags() + err := fs.Parse(args) + if err != nil { + cmd.ui.Error(fmt.Sprintf("unable to parse flags: %s", err)) + return 1 + } + + return cmd.run(cmd.runInternal) +} + +func (cmd *migrateCmd) runInternal() error { + err := provider.Migrate( + cmd.ui, + cmd.flagProviderDir, + cmd.flagOldWebsiteSourceDir, + cmd.flagNewWebsiteSourceDir, + ) + if err != nil { + return fmt.Errorf("unable to migrate website: %w", err) + } + + return nil +} diff --git a/internal/cmd/run.go b/internal/cmd/run.go index bf0266b4..a1f5391b 100644 --- a/internal/cmd/run.go +++ b/internal/cmd/run.go @@ -57,10 +57,19 @@ func initCommands(ui cli.Ui) map[string]cli.CommandFactory { }, nil } + migrateFactory := func() (cli.Command, error) { + return &migrateCmd{ + commonCmd: commonCmd{ + ui: ui, + }, + }, nil + } + return map[string]cli.CommandFactory{ "": defaultFactory, "generate": generateFactory, "validate": validateFactory, + "migrate": migrateFactory, //"serve": serveFactory, } } diff --git a/internal/provider/migrate.go b/internal/provider/migrate.go new file mode 100644 index 00000000..7d24ebff --- /dev/null +++ b/internal/provider/migrate.go @@ -0,0 +1,147 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "github.com/mitchellh/cli" +) + +type migrator struct { + // providerDir is the absolute path to the root provider directory + providerDir string + + oldWebsiteDir string + newWebsiteDir string + + ui cli.Ui +} + +func (m *migrator) infof(format string, a ...interface{}) { + m.ui.Info(fmt.Sprintf(format, a...)) +} + +func Migrate(ui cli.Ui, providerDir string, oldWebsiteDir string, newWebsiteDir string) 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) + } + + m := &migrator{ + providerDir: providerDir, + oldWebsiteDir: oldWebsiteDir, + newWebsiteDir: newWebsiteDir, + + ui: ui, + } + + ctx := context.Background() + + return m.Migrate(ctx) +} + +func (m migrator) Migrate(ctx context.Context) error { + + //TODO: add code to verify vaild dir + + err := filepath.Walk(m.OldProviderWebsiteDir(), func(path string, info os.FileInfo, _ error) error { + if info.IsDir() { + // skip directories + return nil + } + + rel, err := filepath.Rel(filepath.Join(m.OldProviderWebsiteDir()), path) + if err != nil { + return fmt.Errorf("unable to retrieve the relative path of basepath %q and targetpath %q: %w", + filepath.Join(m.OldProviderWebsiteDir()), path, err) + } + + relDir, relFile := filepath.Split(rel) + relDir = filepath.ToSlash(relDir) + + _, err = os.ReadFile(path) + if err != nil { + return fmt.Errorf("unable to read file %q: %w", rel, err) + } + + m.infof("copying %q", rel) + switch relDir { + case "docs/d/": + dest := filepath.Join(m.NewProviderWebsiteDir(), "datasources", relFile) + m.infof("copying to %q", dest) + err = cp(rel, dest) + if err != nil { + return err + } + case "docs/r/": + dest := filepath.Join(m.NewProviderWebsiteDir(), "resources", relFile) + m.infof("copying to %q", dest) + err = cp(path, dest) + if err != nil { + return err + } + case "docs/": // provider + if relFile == "index.html.markdown" { + dest := filepath.Join(m.NewProviderWebsiteDir(), relFile) + m.infof("copying to %q", dest) + err = cp(path, dest) + if err != nil { + return err + } + } + } + + if err != nil { + return fmt.Errorf("unable to migrate template %q: %w", rel, err) + } + return nil + }) + if err != nil { + return fmt.Errorf("unable to migrate website: %w", err) + } + + return nil +} + +// OldProviderWebsiteDir returns the absolute path to the joined provider and +// given old website directory, which defaults to "website". +func (m *migrator) OldProviderWebsiteDir() string { + return filepath.Join(m.providerDir, m.oldWebsiteDir) +} + +// NewProviderWebsiteDir returns the absolute path to the joined provider and +// given new templates directory, which defaults to "templates". +func (m *migrator) NewProviderWebsiteDir() string { + return filepath.Join(m.providerDir, m.newWebsiteDir) +} From 51683626c9b848391ef3ad59b08b575f5ee182a3 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Thu, 7 Dec 2023 15:59:57 -0500 Subject: [PATCH 02/17] Change migrated file extensions to `.md.tmpl` --- .../schema-json/migrate/time_provider_success.txtar | 4 ++-- internal/provider/migrate.go | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success.txtar b/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success.txtar index d105f3d9..04eab499 100644 --- a/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success.txtar +++ b/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success.txtar @@ -11,8 +11,8 @@ exec tfplugindocs migrate -help # Run migrate command exec tfplugindocs migrate #cmp stdout expected-output.txt -cmpenv website/docs/index.html.markdown templates/index.html.markdown -cmpenv website/docs/r/offset.html.markdown templates/resources/offset.html.markdown +cmpenv website/docs/index.html.markdown templates/index.md.tmpl +cmpenv website/docs/r/offset.html.markdown templates/resources/offset.md.tmpl -- expected-output.txt -- -- expected-help-output.txt -- diff --git a/internal/provider/migrate.go b/internal/provider/migrate.go index 7d24ebff..92de67bc 100644 --- a/internal/provider/migrate.go +++ b/internal/provider/migrate.go @@ -8,6 +8,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "github.com/mitchellh/cli" ) @@ -98,14 +99,16 @@ func (m migrator) Migrate(ctx context.Context) error { m.infof("copying %q", rel) switch relDir { case "docs/d/": - dest := filepath.Join(m.NewProviderWebsiteDir(), "datasources", relFile) + tmplFile := strings.Replace(relFile, ".html.markdown", ".md.tmpl", 1) + dest := filepath.Join(m.NewProviderWebsiteDir(), "datasources", tmplFile) m.infof("copying to %q", dest) err = cp(rel, dest) if err != nil { return err } case "docs/r/": - dest := filepath.Join(m.NewProviderWebsiteDir(), "resources", relFile) + tmplFile := strings.Replace(relFile, ".html.markdown", ".md.tmpl", 1) + dest := filepath.Join(m.NewProviderWebsiteDir(), "resources", tmplFile) m.infof("copying to %q", dest) err = cp(path, dest) if err != nil { @@ -113,7 +116,8 @@ func (m migrator) Migrate(ctx context.Context) error { } case "docs/": // provider if relFile == "index.html.markdown" { - dest := filepath.Join(m.NewProviderWebsiteDir(), relFile) + tmplFile := strings.Replace(relFile, ".html.markdown", ".md.tmpl", 1) + dest := filepath.Join(m.NewProviderWebsiteDir(), tmplFile) m.infof("copying to %q", dest) err = cp(path, dest) if err != nil { From 6cc8db517ad46c825af0dea7d551b7cbde4e021a Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 19 Dec 2023 18:44:39 -0500 Subject: [PATCH 03/17] Finish `migrate` subcommand implementation --- README.md | 38 +- cmd/tfplugindocs/main_test.go | 3 +- .../migrate/time_provider_success.txtar | 471 +++++++++++++++++- go.mod | 23 +- go.sum | 58 ++- internal/cmd/migrate.go | 11 +- internal/provider/migrate.go | 245 +++++++-- internal/provider/util.go | 48 ++ 8 files changed, 819 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index bf82fb63..3905abd6 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,19 @@ $ tfplugindocs validate --help Usage: tfplugindocs validate [] ``` +`migrate` command: + +```shell +$ tfplugindocs migrate --help + +Usage: tfplugindocs migrate [] + + --examples-dir examples directory based on provider-dir (default: "examples") + --old-website-source-dir old website directory based on provider-dir; files will be migrated from this directory (default: "website") + --provider-dir relative or absolute path to the root provider code directory when running the command outside the root provider code directory + --templates-dir new website templates directory based on provider-dir; files will be migrated to this directory (default: "templates") +``` + ### How it Works When you run `tfplugindocs`, by default from the root directory of a provider codebase, the tool takes the following actions: @@ -100,6 +113,18 @@ Otherwise, the provider developer can set an arbitrary description like this: // ... ``` +#### Migrate subcommand + +The `migrate` subcommand can be used to migrate from the old directory structure (`website/docs/r`) to the `tfplugindocs` supported structure (`templates/`). + +The `migrate` subcommand takes the following actions: +- Copies the `--old-website-source-dir` folder to the `--tempates-dir` folder (will create this folder if it doesn't exist) +- Renames `docs/d/` and `docs/r/` subdirectories to `data-sources/` and `resources/` respectively +- Change file suffixes for template files from `.html.markdown` to `.md.tmpl` +- Extracts code blocks from website docs to create individual example files in `--examples-dir` (will create this folder if it doesn't exist) +- replace extracted example code in website templates with `tfplugindocs` template code referring to example files. +- Copies non-template files to `--templates-dir` folder + ### Conventional Paths The generation of missing documentation is based on a number of assumptions / conventional paths. @@ -130,6 +155,17 @@ For examples: | `examples/resources//resource.tf` | Resource example config | | `examples/resources//import.sh` | Resource example import command | +#### Migration + +The `migrate` subcommand assumes the following conventional paths for the old website structure: + +| Path | Description | +|---------------------------------------------------|----------------------| +| `website/` | Root of website docs | +| `website/docs/index.html.markdown` | Docs index page | +| `website/docs/d/.html.markdown` | Data source page | +| `website/docs/r/.html.markdown` | Resource page | + ### Templates The templates are implemented with Go [`text/template`](https://golang.org/pkg/text/template/) @@ -179,7 +215,7 @@ using the following data fields and functions: | `tffile` | A special case of the `codefile` function, designed for Terraform files (i.e. `.tf`). | | `trimspace` | Equivalent to [`strings.TrimSpace`](https://pkg.go.dev/strings#TrimSpace). | | `upper` | Equivalent to [`strings.ToUpper`](https://pkg.go.dev/strings#ToUpper). | - + ## Disclaimer This is still under development: while it's being used for production-ready providers, you might still find bugs diff --git a/cmd/tfplugindocs/main_test.go b/cmd/tfplugindocs/main_test.go index 173c303d..703dd4cb 100644 --- a/cmd/tfplugindocs/main_test.go +++ b/cmd/tfplugindocs/main_test.go @@ -51,7 +51,6 @@ func Test_SchemaJson_MigrateAcceptanceTests(t *testing.T) { t.Parallel() testscript.Run(t, testscript.Params{ - Dir: "testdata/scripts/schema-json/migrate", - WorkdirRoot: "/Users/sgoods/test", + Dir: "testdata/scripts/schema-json/migrate", }) } diff --git a/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success.txtar b/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success.txtar index 04eab499..71cd11a8 100644 --- a/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success.txtar +++ b/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success.txtar @@ -10,18 +10,85 @@ exec tfplugindocs migrate -help # Run migrate command exec tfplugindocs migrate -#cmp stdout expected-output.txt -cmpenv website/docs/index.html.markdown templates/index.md.tmpl -cmpenv website/docs/r/offset.html.markdown templates/resources/offset.md.tmpl +cmpenv stdout expected-output.txt + +# Check template files +cmpenv templates/index.md.tmpl exp-templates/index.md.tmpl +cmpenv templates/resources/offset.md.tmpl exp-templates/resources/offset.md.tmpl +cmpenv templates/resources/rotating.md.tmpl exp-templates/resources/rotating.md.tmpl +cmpenv templates/resources/sleep.md.tmpl exp-templates/resources/sleep.md.tmpl +cmpenv templates/resources/static.md.tmpl exp-templates/resources/static.md.tmpl + +# Check copied non-template files +cmpenv website/time.erb templates/time.erb + +# Check generated example files +cmpenv examples/example_1.tf examples/example_1.tf + +cmpenv examples/resources/offset/example_1.tf exp-examples/resources/offset/example_1.tf +cmpenv examples/resources/offset/example_2.tf exp-examples/resources/offset/example_2.tf +cmpenv examples/resources/offset/import_1.sh exp-examples/resources/offset/import_1.sh + +cmpenv examples/resources/rotating/example_1.tf exp-examples/resources/rotating/example_1.tf +cmpenv examples/resources/rotating/import_1.sh exp-examples/resources/rotating/import_1.sh +cmpenv examples/resources/rotating/import_2.sh exp-examples/resources/rotating/import_2.sh + +cmpenv examples/resources/sleep/example_1.tf exp-examples/resources/sleep/example_1.tf +cmpenv examples/resources/sleep/example_2.tf exp-examples/resources/sleep/example_2.tf +cmpenv examples/resources/sleep/example_3.tf exp-examples/resources/sleep/example_3.tf +cmpenv examples/resources/sleep/import_1.sh exp-examples/resources/sleep/import_1.sh +cmpenv examples/resources/sleep/import_2.sh exp-examples/resources/sleep/import_2.sh + +cmpenv examples/resources/static/example_1.tf examples/resources/static/example_1.tf +cmpenv examples/resources/static/example_2.tf examples/resources/static/example_2.tf +cmpenv examples/resources/static/import_1.sh examples/resources/static/import_1.sh -- expected-output.txt -- +migrating website from "$WORK/website" to "$WORK/templates" +migrating provider index +extracting YAML frontmatter to "$WORK/templates/index.md.tmpl" +extracting code examples from "index.md.tmpl" +creating example file "$WORK/examples/example_1.tf" +finished creating template "$WORK/templates/index.md.tmpl" +migrating resource "offset" +extracting YAML frontmatter to "$WORK/templates/resources/offset.md.tmpl" +extracting code examples from "offset.md.tmpl" +creating example file "$WORK/examples/resources/offset/example_1.tf" +creating example file "$WORK/examples/resources/offset/example_2.tf" +creating import file "$WORK/examples/resources/offset/import_1.sh" +finished creating template "$WORK/templates/resources/offset.md.tmpl" +migrating resource "rotating" +extracting YAML frontmatter to "$WORK/templates/resources/rotating.md.tmpl" +extracting code examples from "rotating.md.tmpl" +creating example file "$WORK/examples/resources/rotating/example_1.tf" +creating import file "$WORK/examples/resources/rotating/import_1.sh" +creating import file "$WORK/examples/resources/rotating/import_2.sh" +finished creating template "$WORK/templates/resources/rotating.md.tmpl" +migrating resource "sleep" +extracting YAML frontmatter to "$WORK/templates/resources/sleep.md.tmpl" +extracting code examples from "sleep.md.tmpl" +creating example file "$WORK/examples/resources/sleep/example_1.tf" +creating example file "$WORK/examples/resources/sleep/example_2.tf" +creating example file "$WORK/examples/resources/sleep/example_3.tf" +creating import file "$WORK/examples/resources/sleep/import_1.sh" +creating import file "$WORK/examples/resources/sleep/import_2.sh" +finished creating template "$WORK/templates/resources/sleep.md.tmpl" +migrating resource "static" +extracting YAML frontmatter to "$WORK/templates/resources/static.md.tmpl" +extracting code examples from "static.md.tmpl" +creating example file "$WORK/examples/resources/static/example_1.tf" +creating example file "$WORK/examples/resources/static/example_2.tf" +creating import file "$WORK/examples/resources/static/import_1.sh" +finished creating template "$WORK/templates/resources/static.md.tmpl" +copying non-template file "time.erb" -- expected-help-output.txt -- Usage: tfplugindocs migrate [] - --new-website-source-dir new website templates directory based on provider-dir (default: "templates") - --old-website-source-dir old website directory based on provider-dir (default: "website") + --examples-dir examples directory based on provider-dir (default: "examples") + --old-website-source-dir old website directory based on provider-dir; files will be migrated from this directory (default: "website") --provider-dir relative or absolute path to the root provider code directory when running the command outside the root provider code directory + --templates-dir new website templates directory based on provider-dir; files will be migrated to this directory (default: "templates") -- website/docs/index.html.markdown -- @@ -460,3 +527,397 @@ The `triggers` argument cannot be imported. <%= yield %> <% end %> +-- exp-examples/example_1.tf -- +resource "time_static" "ami_update" { + triggers = { + # Save the time each switch of an AMI id + ami_id = data.aws_ami.example.id + } +} + +resource "aws_instance" "server" { + # Read the AMI id "through" the time_static resource to ensure that + # both will change together. + ami = time_static.ami_update.triggers.ami_id + + tags = { + AmiUpdateTime = time_static.ami_update.rfc3339 + } + + # ... (other aws_instance arguments) ... +} +-- exp-examples/resources/offset/example_1.tf -- +resource "time_offset" "example" { + offset_days = 7 +} + +output "one_week_from_now" { + value = time_offset.example.rfc3339 +} +-- exp-examples/resources/offset/example_2.tf -- +resource "time_offset" "ami_update" { + triggers = { + # Save the time each switch of an AMI id + ami_id = data.aws_ami.example.id + } + + offset_days = 7 +} + +resource "aws_instance" "server" { + # Read the AMI id "through" the time_offset resource to ensure that + # both will change together. + ami = time_offset.ami_update.triggers.ami_id + + tags = { + ExpirationTime = time_offset.ami_update.rfc3339 + } + + # ... (other aws_instance arguments) ... +} +-- exp-examples/resources/offset/import_1.sh -- +$ terraform import time_offset.example 2020-02-12T06:36:13Z,0,0,7,0,0,0 +-- exp-examples/resources/rotating/example_1.tf -- +resource "time_rotating" "example" { + rotation_days = 30 +} +-- exp-examples/resources/rotating/import_1.sh -- +$ terraform import time_rotation.example 2020-02-12T06:36:13Z,0,0,30,0,0 +-- exp-examples/resources/rotating/import_2.sh -- +$ terraform import time_rotation.example 2020-02-12T06:36:13Z,2020-02-13T06:36:13Z +-- exp-examples/resources/sleep/example_1.tf -- +# This resource will destroy (potentially immediately) after null_resource.next +resource "null_resource" "previous" {} + +resource "time_sleep" "wait_30_seconds" { + depends_on = [null_resource.previous] + + create_duration = "30s" +} + +# This resource will create (at least) 30 seconds after null_resource.previous +resource "null_resource" "next" { + depends_on = [time_sleep.wait_30_seconds] +} +-- exp-examples/resources/sleep/example_2.tf -- +# This resource will destroy (at least) 30 seconds after null_resource.next +resource "null_resource" "previous" {} + +resource "time_sleep" "wait_30_seconds" { + depends_on = [null_resource.previous] + + destroy_duration = "30s" +} + +# This resource will create (potentially immediately) after null_resource.previous +resource "null_resource" "next" { + depends_on = [time_sleep.wait_30_seconds] +} +-- exp-examples/resources/sleep/example_3.tf -- +resource "aws_ram_resource_association" "example" { + resource_arn = aws_subnet.example.arn + resource_share_arn = aws_ram_resource_share.example.arn +} + +# AWS resources shared via Resource Access Manager can take a few seconds to +# propagate across AWS accounts after RAM returns a successful association. +resource "time_sleep" "ram_resource_propagation" { + create_duration = "60s" + + triggers = { + # This sets up a proper dependency on the RAM association + subnet_arn = aws_ram_resource_association.example.resource_arn + subnet_id = aws_subnet.example.id + } +} + +resource "aws_db_subnet_group" "example" { + name = "example" + + # Read the Subnet identifier "through" the time_sleep resource to ensure a + # proper dependency and that both will change together. + subnet_ids = [time_sleep.ram_resource_propagation.triggers["subnet_id"]] +} +-- exp-examples/resources/sleep/import_1.sh -- +$ terraform import time_sleep.example 30s, +-- exp-examples/resources/sleep/import_2.sh -- +$ terraform import time_sleep.example ,30s +-- exp-examples/resources/static/example_1.tf -- +resource "time_static" "example" {} + +output "current_time" { + value = time_static.example.rfc3339 +} +-- exp-examples/resources/static/example_2.tf -- +resource "time_static" "ami_update" { + triggers = { + # Save the time each switch of an AMI id + ami_id = data.aws_ami.example.id + } +} + +resource "aws_instance" "server" { + # Read the AMI id "through" the time_static resource to ensure that + # both will change together. + ami = time_static.ami_update.triggers.ami_id + + tags = { + AmiUpdateTime = time_static.ami_update.rfc3339 + } + + # ... (other aws_instance arguments) ... +} +-- exp-examples/resources/static/import_1.sh -- +$ terraform import time_static.example 2020-02-12T06:36:13Z +-- exp-templates/index.md.tmpl -- +--- +layout: "time" +page_title: "Provider: Time" +description: |- + The time provider is used to interact with time-based resources. +--- +# Time Provider + +The time provider is used to interact with time-based resources. The provider itself has no configuration options. + +Use the navigation to the left to read about the available resources. + +## Resource "Triggers" + +Certain time resources, only perform actions during specific lifecycle actions: + +- `time_offset`: Saves base timestamp into Terraform state only when created. +- `time_sleep`: Sleeps when created and/or destroyed. +- `time_static`: Saves base timestamp into Terraform state only when created. + +These resources provide an optional map argument called `triggers` that can be populated with arbitrary key/value pairs. When the keys or values of this argument are updated, Terraform will re-perform the desired action, such as updating the base timestamp or sleeping again. + +For example: + +{{tffile "$WORK/examples/example_1.tf"}} + +`triggers` are *not* treated as sensitive attributes; a value used for `triggers` will be displayed in Terraform UI output as plaintext. + +To force a these actions to reoccur without updating `triggers`, the [`terraform taint` command](https://www.terraform.io/docs/commands/taint.html) can be used to produce the action on the next run. +-- exp-templates/resources/offset.md.tmpl -- +--- +layout: "time" +page_title: "Time: time_offset" +description: |- + Manages a offset time resource. +--- +# Resource: time_offset + +Manages an offset time resource, which keeps an UTC timestamp stored in the Terraform state that is offset from a locally sourced base timestamp. This prevents perpetual differences caused by using the [`timestamp()` function](https://www.terraform.io/docs/configuration/functions/timestamp.html). + +-> Further manipulation of incoming or outgoing values can be accomplished with the [`formatdate()` function](https://www.terraform.io/docs/configuration/functions/formatdate.html) and the [`timeadd()` function](https://www.terraform.io/docs/configuration/functions/timeadd.html). + +## Example Usage + +### Basic Usage + +{{tffile "$WORK/examples/resources/offset/example_1.tf"}} + +### Triggers Usage + +{{tffile "$WORK/examples/resources/offset/example_2.tf"}} + +## Argument Reference + +~> **NOTE:** At least one of the `offset_` arguments must be configured. + +The following arguments are optional: + +* `base_rfc3339` - (Optional) Configure the base timestamp with an UTC [RFC3339 time string](https://tools.ietf.org/html/rfc3339#section-5.8) (`YYYY-MM-DDTHH:MM:SSZ`). Defaults to the current time. +* `triggers` - (Optional) Arbitrary map of values that, when changed, will trigger a new base timestamp value to be saved. See [the main provider documentation](../index.html) for more information. +* `offset_days` - (Optional) Number of days to offset the base timestamp. Conflicts with other `offset_` arguments. +* `offset_hours` - (Optional) Number of hours to offset the base timestamp. Conflicts with other `offset_` arguments. +* `offset_minutes` - (Optional) Number of minutes to offset the base timestamp. Conflicts with other `offset_` arguments. +* `offset_months` - (Optional) Number of months to offset the base timestamp. Conflicts with other `offset_` arguments. +* `offset_seconds` - (Optional) Number of seconds to offset the base timestamp. Conflicts with other `offset_` arguments. +* `offset_years` - (Optional) Number of years to offset the base timestamp. Conflicts with other `offset_` arguments. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `day` - Number day of offset timestamp. +* `hour` - Number hour of offset timestamp. +* `id` - UTC RFC3339 format of the base timestamp, e.g. `2020-02-12T06:36:13Z`. +* `minute` - Number minute of offset timestamp. +* `month` - Number month of offset timestamp. +* `rfc3339` - UTC RFC3339 format of the offset timestamp, e.g. `2020-02-12T06:36:13Z`. +* `second` - Number second of offset timestamp. +* `unix` - Number of seconds since epoch time, e.g. `1581489373`. +* `year` - Number year of offset timestamp. + +## Import + +This resource can be imported using the base UTC RFC3339 timestamp and offset years, months, days, hours, minutes, and seconds, separated by commas (`,`), e.g. + +{{codefile "shell" "$WORK/examples/resources/offset/import_1.sh"}} + +The `triggers` argument cannot be imported. +-- exp-templates/resources/rotating.md.tmpl -- +--- +layout: "time" +page_title: "Time: time_rotating" +description: |- + Manages a rotating time resource. +--- +# Resource: time_rotating + +Manages a rotating time resource, which keeps a rotating UTC timestamp stored in the Terraform state and proposes resource recreation when the locally sourced current time is beyond the rotation time. This rotation only occurs when Terraform is executed, meaning there will be drift between the rotation timestamp and actual rotation. The new rotation timestamp offset includes this drift. This prevents perpetual differences caused by using the [`timestamp()` function](https://www.terraform.io/docs/configuration/functions/timestamp.html) by only forcing a new value on the set cadence. + +-> Further manipulation of incoming or outgoing values can be accomplished with the [`formatdate()` function](https://www.terraform.io/docs/configuration/functions/formatdate.html) and the [`timeadd()` function](https://www.terraform.io/docs/configuration/functions/timeadd.html). + +## Example Usage + +This example configuration will rotate (destroy/create) the resource every 30 days. + +{{tffile "$WORK/examples/resources/rotating/example_1.tf"}} + +## Argument Reference + +~> **NOTE:** At least one of the `rotation_` arguments must be configured. + +The following arguments are optional: + +* `rfc3339` - (Optional) Configure the base timestamp with an UTC [RFC3339 time string](https://tools.ietf.org/html/rfc3339#section-5.8) (`YYYY-MM-DDTHH:MM:SSZ`). Defaults to the current time. +* `rotation_days` - (Optional) Number of days to add to the base timestamp to configure the rotation timestamp. When the current time has passed the rotation timestamp, the resource will trigger recreation. Conflicts with other `rotation_` arguments. +* `rotation_hours` - (Optional) Number of hours to add to the base timestamp to configure the rotation timestamp. When the current time has passed the rotation timestamp, the resource will trigger recreation. Conflicts with other `rotation_` arguments. +* `rotation_minutes` - (Optional) Number of minutes to add to the base timestamp to configure the rotation timestamp. When the current time has passed the rotation timestamp, the resource will trigger recreation. Conflicts with other `rotation_` arguments. +* `rotation_months` - (Optional) Number of months to add to the base timestamp to configure the rotation timestamp. When the current time has passed the rotation timestamp, the resource will trigger recreation. Conflicts with other `rotation_` arguments. +* `rotation_rfc3339` - (Optional) Configure the rotation timestamp with an UTC [RFC3339 time string](https://tools.ietf.org/html/rfc3339#section-5.8) (`YYYY-MM-DDTHH:MM:SSZ`). When the current time has passed the rotation timestamp, the resource will trigger recreation. Conflicts with other `rotation_` arguments. +* `rotation_years` - (Optional) Number of years to add to the base timestamp to configure the rotation timestamp. When the current time has passed the rotation timestamp, the resource will trigger recreation. Conflicts with other `rotation_` arguments. +* `triggers` - (Optional) Arbitrary map of values that, when changed, will trigger a new base timestamp value to be saved. These conditions recreate the resource in addition to other rotation arguments. See [the main provider documentation](../index.html) for more information. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `day` - Number day of timestamp. +* `hour` - Number hour of timestamp. +* `id` - UTC RFC3339 format of the base timestamp, e.g. `2020-02-12T06:36:13Z`. +* `minute` - Number minute of timestamp. +* `month` - Number month of timestamp. +* `second` - Number second of timestamp. +* `unix` - Number of seconds since epoch time, e.g. `1581489373`. +* `year` - Number year of timestamp. + +## Import + +This resource can be imported using the base UTC RFC3339 value and rotation years, months, days, hours, and minutes, separated by commas (`,`), e.g. for 30 days + +{{codefile "shell" "$WORK/examples/resources/rotating/import_1.sh"}} + +Otherwise, to import with the rotation RFC3339 value, the base UTC RFC3339 value and rotation UTC RFC3339 value, separated by commas (`,`), e.g. + +{{codefile "shell" "$WORK/examples/resources/rotating/import_2.sh"}} + +The `triggers` argument cannot be imported. +-- exp-templates/resources/sleep.md.tmpl -- +--- +layout: "time" +page_title: "Time: time_sleep" +description: |- + Manages a static time resource. +--- +# Resource: time_sleep + +Manages a resource that delays creation and/or destruction, typically for further resources. This prevents cross-platform compatibility and destroy-time issues with using the [`local-exec` provisioner](https://www.terraform.io/docs/provisioners/local-exec.html). + +-> In many cases, this resource should be considered a workaround for issues that should be reported and handled in downstream Terraform Provider logic. Downstream resources can usually introduce or adjust retries in their code to handle time delay issues for all Terraform configurations or upstream resources can be improved to better wait for a resource to be fully ready and available. + +## Example Usage + +### Delay Create Usage + +{{tffile "$WORK/examples/resources/sleep/example_1.tf"}} + +### Delay Destroy Usage + +{{tffile "$WORK/examples/resources/sleep/example_2.tf"}} + +### Triggers Usage + +{{tffile "$WORK/examples/resources/sleep/example_3.tf"}} + +## Argument Reference + +The following arguments are optional: + +* `create_duration` - (Optional) [Time duration](https://golang.org/pkg/time/#ParseDuration) to delay resource creation. For example, `30s` for 30 seconds or `5m` for 5 minutes. Updating this value by itself will not trigger a delay. +* `destroy_duration` - (Optional) [Time duration](https://golang.org/pkg/time/#ParseDuration) to delay resource destroy. For example, `30s` for 30 seconds or `5m` for 5 minutes. Updating this value by itself will not trigger a delay. This value or any updates to it must be successfully applied into the Terraform state before destroying this resource to take effect. +* `triggers` - (Optional) Arbitrary map of values that, when changed, will run any creation or destroy delays again. See [the main provider documentation](../index.html) for more information. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - UTC RFC3339 timestamp of the creation or import, e.g. `2020-02-12T06:36:13Z`. + +## Import + +This resource can be imported with the `create_duration` and `destroy_duration`, separated by a comma (`,`). + +e.g. For 30 seconds create duration with no destroy duration: + +{{codefile "shell" "$WORK/examples/resources/sleep/import_1.sh"}} + +e.g. For 30 seconds destroy duration with no create duration: + +{{codefile "shell" "$WORK/examples/resources/sleep/import_2.sh"}} + +The `triggers` argument cannot be imported. +-- exp-templates/resources/static.md.tmpl -- +--- +layout: "time" +page_title: "Time: time_static" +description: |- + Manages a static time resource. +--- +# Resource: time_static + +Manages a static time resource, which keeps a locally sourced UTC timestamp stored in the Terraform state. This prevents perpetual differences caused by using the [`timestamp()` function](https://www.terraform.io/docs/configuration/functions/timestamp.html). + +-> Further manipulation of incoming or outgoing values can be accomplished with the [`formatdate()` function](https://www.terraform.io/docs/configuration/functions/formatdate.html) and the [`timeadd()` function](https://www.terraform.io/docs/configuration/functions/timeadd.html). + +## Example Usage + +### Basic Usage + +{{tffile "$WORK/examples/resources/static/example_1.tf"}} + +### Triggers Usage + +{{tffile "$WORK/examples/resources/static/example_2.tf"}} + +## Argument Reference + +The following arguments are optional: + +* `triggers` - (Optional) Arbitrary map of values that, when changed, will trigger a new base timestamp value to be saved. See [the main provider documentation](../index.html) for more information. +* `rfc3339` - (Optional) Configure the base timestamp with an UTC [RFC3339 time string](https://tools.ietf.org/html/rfc3339#section-5.8) (`YYYY-MM-DDTHH:MM:SSZ`). Defaults to the current time. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `day` - Number day of timestamp. +* `hour` - Number hour of timestamp. +* `id` - UTC RFC3339 timestamp format, e.g. `2020-02-12T06:36:13Z`. +* `minute` - Number minute of timestamp. +* `month` - Number month of timestamp. +* `rfc3339` - UTC RFC3339 format of timestamp, e.g. `2020-02-12T06:36:13Z`. +* `second` - Number second of timestamp. +* `unix` - Number of seconds since epoch time, e.g. `1581489373`. +* `year` - Number year of timestamp. + +## Import + +This resource can be imported using the UTC RFC3339 value, e.g. + +{{codefile "shell" "$WORK/examples/resources/static/import_1.sh"}} + +The `triggers` argument cannot be imported. \ No newline at end of file diff --git a/go.mod b/go.mod index a33126f1..a5fe3188 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/hashicorp/terraform-plugin-docs go 1.19 require ( + github.com/Kunde21/markdownfmt/v3 v3.1.0 github.com/google/go-cmp v0.6.0 github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/hc-install v0.6.1 @@ -12,6 +13,8 @@ require ( github.com/mitchellh/cli v1.1.5 github.com/rogpeppe/go-internal v1.11.0 github.com/russross/blackfriday v1.6.0 + github.com/yuin/goldmark v1.6.0 + github.com/yuin/goldmark-meta v1.1.0 github.com/zclconf/go-cty v1.14.1 golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df golang.org/x/text v0.14.0 @@ -26,8 +29,8 @@ require ( github.com/armon/go-radix v1.0.0 // indirect github.com/bgentry/speakeasy v0.1.0 // indirect github.com/cloudflare/circl v1.3.3 // indirect - github.com/fatih/color v1.13.0 // indirect - github.com/google/uuid v1.3.0 // indirect + github.com/fatih/color v1.16.0 // indirect + github.com/google/uuid v1.4.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-checkpoint v0.5.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect @@ -35,14 +38,18 @@ require ( github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/huandu/xstrings v1.3.2 // indirect github.com/imdario/mergo v0.3.15 // indirect - github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/posener/complete v1.2.3 // indirect github.com/shopspring/decimal v1.3.1 // indirect - github.com/spf13/cast v1.5.0 // indirect - golang.org/x/crypto v0.13.0 // indirect - golang.org/x/mod v0.13.0 // indirect - golang.org/x/sys v0.12.0 // indirect - golang.org/x/tools v0.13.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/stretchr/testify v1.8.4 // indirect + golang.org/x/crypto v0.16.0 // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/tools v0.16.0 // indirect + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 0e990701..349e58b2 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +github.com/Kunde21/markdownfmt/v3 v3.1.0 h1:KiZu9LKs+wFFBQKhrZJrFZwtLnCCWJahL+S+E/3VnM0= +github.com/Kunde21/markdownfmt/v3 v3.1.0/go.mod h1:tPXN1RTyOzJwhfHoon9wUr4HGYmWgVxSQN6VBJDkrVc= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= @@ -26,9 +28,9 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= -github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= github.com/go-git/go-git/v5 v5.9.0 h1:cD9SFA7sHVRdJ7AYck1ZaAa/yeuBvGPxwXDL8cxrObY= @@ -37,8 +39,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -69,17 +71,17 @@ github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mitchellh/cli v1.1.5 h1:OxRIeJXpAMztws/XHlN2vu6imG5Dpq+j61AzAX5fLng= github.com/mitchellh/cli v1.1.5/go.mod h1:v8+iFts2sPIKUV1ltktPXMCC8fumSKFItNcD2cLtRR4= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= @@ -104,16 +106,21 @@ github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5g github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/skeema/knownhosts v1.2.0 h1:h9r9cf0+u7wSE+M183ZtMGgOJKiL96brpaz5ekfJCpM= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= -github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68= +github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc= +github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0= github.com/zclconf/go-cty v1.14.1 h1:t9fyA35fwjjUMcmL5hLER+e/rEPqrbCK1/OSE4SI9KA= github.com/zclconf/go-cty v1.14.1/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -122,14 +129,14 @@ golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= -golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -137,17 +144,14 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -155,8 +159,8 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -174,13 +178,17 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM= +golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/cmd/migrate.go b/internal/cmd/migrate.go index fb0e90e9..44218ac3 100644 --- a/internal/cmd/migrate.go +++ b/internal/cmd/migrate.go @@ -16,7 +16,8 @@ type migrateCmd struct { flagProviderDir string flagOldWebsiteSourceDir string - flagNewWebsiteSourceDir string + flagTemplatesDir string + flagExamplesDir string } func (cmd *migrateCmd) Synopsis() string { @@ -65,8 +66,9 @@ func (cmd *migrateCmd) Flags() *flag.FlagSet { fs := flag.NewFlagSet("migrate", flag.ExitOnError) 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.flagOldWebsiteSourceDir, "old-website-source-dir", "website", "old website directory based on provider-dir") - fs.StringVar(&cmd.flagNewWebsiteSourceDir, "new-website-source-dir", "templates", "new website templates directory based on provider-dir") + fs.StringVar(&cmd.flagOldWebsiteSourceDir, "old-website-source-dir", "website", "old website directory based on provider-dir; files will be migrated from this directory") + fs.StringVar(&cmd.flagTemplatesDir, "templates-dir", "templates", "new website templates directory based on provider-dir; files will be migrated to this directory") + fs.StringVar(&cmd.flagExamplesDir, "examples-dir", "examples", "examples directory based on provider-dir") return fs } @@ -86,7 +88,8 @@ func (cmd *migrateCmd) runInternal() error { cmd.ui, cmd.flagProviderDir, cmd.flagOldWebsiteSourceDir, - cmd.flagNewWebsiteSourceDir, + cmd.flagTemplatesDir, + cmd.flagExamplesDir, ) if err != nil { return fmt.Errorf("unable to migrate website: %w", err) diff --git a/internal/provider/migrate.go b/internal/provider/migrate.go index 92de67bc..4a867211 100644 --- a/internal/provider/migrate.go +++ b/internal/provider/migrate.go @@ -4,13 +4,22 @@ package provider import ( - "context" + "bufio" + "bytes" "fmt" "os" "path/filepath" + "strconv" "strings" "github.com/mitchellh/cli" + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/text" +) + +var ( + exampleImportCodeTemplate = "{{codefile \"shell\" \"%s\"}}" + exampleTFCodeTemplate = "{{tffile \"%s\"}}" ) type migrator struct { @@ -18,7 +27,8 @@ type migrator struct { providerDir string oldWebsiteDir string - newWebsiteDir string + templatesDir string + examplesDir string ui cli.Ui } @@ -27,8 +37,11 @@ func (m *migrator) infof(format string, a ...interface{}) { m.ui.Info(fmt.Sprintf(format, a...)) } -func Migrate(ui cli.Ui, providerDir string, oldWebsiteDir string, newWebsiteDir string) error { +func (m *migrator) warnf(format string, a ...interface{}) { + m.ui.Warn(fmt.Sprintf(format, a...)) +} +func Migrate(ui cli.Ui, providerDir string, oldWebsiteDir string, templatesDir string, examplesDir string) error { // Ensure provider directory is resolved absolute path if providerDir == "" { wd, err := os.Getwd() @@ -62,19 +75,17 @@ func Migrate(ui cli.Ui, providerDir string, oldWebsiteDir string, newWebsiteDir m := &migrator{ providerDir: providerDir, oldWebsiteDir: oldWebsiteDir, - newWebsiteDir: newWebsiteDir, + templatesDir: templatesDir, + examplesDir: examplesDir, ui: ui, } - ctx := context.Background() - - return m.Migrate(ctx) + return m.Migrate() } -func (m migrator) Migrate(ctx context.Context) error { - - //TODO: add code to verify vaild dir +func (m *migrator) Migrate() error { + m.infof("migrating website from %q to %q", m.OldProviderWebsiteDir(), m.ProviderTemplatesDir()) err := filepath.Walk(m.OldProviderWebsiteDir(), func(path string, info os.FileInfo, _ error) error { if info.IsDir() { @@ -91,41 +102,51 @@ func (m migrator) Migrate(ctx context.Context) error { relDir, relFile := filepath.Split(rel) relDir = filepath.ToSlash(relDir) - _, err = os.ReadFile(path) + data, err := os.ReadFile(path) if err != nil { return fmt.Errorf("unable to read file %q: %w", rel, err) } - m.infof("copying %q", rel) switch relDir { - case "docs/d/": - tmplFile := strings.Replace(relFile, ".html.markdown", ".md.tmpl", 1) - dest := filepath.Join(m.NewProviderWebsiteDir(), "datasources", tmplFile) - m.infof("copying to %q", dest) - err = cp(rel, dest) + case "docs/d/": //data-sources + datasourceName := strings.TrimSuffix(relFile, ".html.markdown") + m.infof("migrating data source %q", datasourceName) + + exampleRelDir := filepath.Join("data-sources", datasourceName) + templateRelDir := "data-sources" + fileName := datasourceName + ".md.tmpl" + err := m.MigrateTemplate(data, templateRelDir, exampleRelDir, fileName) if err != nil { - return err + return fmt.Errorf("unable to migrate template %q: %w", rel, err) } - case "docs/r/": - tmplFile := strings.Replace(relFile, ".html.markdown", ".md.tmpl", 1) - dest := filepath.Join(m.NewProviderWebsiteDir(), "resources", tmplFile) - m.infof("copying to %q", dest) - err = cp(path, dest) + + case "docs/r/": //resources + resourceName := strings.TrimSuffix(relFile, ".html.markdown") + m.infof("migrating resource %q", resourceName) + + exampleRelDir := filepath.Join("resources", resourceName) + templateRelDir := "resources" + fileName := resourceName + ".md.tmpl" + err := m.MigrateTemplate(data, templateRelDir, exampleRelDir, fileName) if err != nil { - return err + return fmt.Errorf("unable to migrate template %q: %w", rel, err) } + case "docs/": // provider if relFile == "index.html.markdown" { - tmplFile := strings.Replace(relFile, ".html.markdown", ".md.tmpl", 1) - dest := filepath.Join(m.NewProviderWebsiteDir(), tmplFile) - m.infof("copying to %q", dest) - err = cp(path, dest) + m.infof("migrating provider index") + err := m.MigrateTemplate(data, "", "", "index.md.tmpl") if err != nil { - return err + return fmt.Errorf("unable to migrate template %q: %w", rel, err) } } + default: + m.infof("copying non-template file %q", rel) + err := cp(path, filepath.Join(m.ProviderTemplatesDir(), relFile)) + if err != nil { + return fmt.Errorf("unable to copy file %q: %w", rel, err) + } } - if err != nil { return fmt.Errorf("unable to migrate template %q: %w", rel, err) } @@ -138,14 +159,172 @@ func (m migrator) Migrate(ctx context.Context) error { return nil } +func (m *migrator) MigrateTemplate(data []byte, templateRelDir string, exampleRelDir, filename string) error { + templateFilePath := filepath.Join(m.ProviderTemplatesDir(), templateRelDir, filename) + + err := os.MkdirAll(filepath.Dir(templateFilePath), 0755) + if err != nil { + return fmt.Errorf("unable to create directory %q: %w", templateFilePath, err) + } + m.infof("extracting YAML frontmatter to %q", templateFilePath) + err = m.ExtractFrontMatter(data, templateFilePath) + if err != nil { + return fmt.Errorf("unable to extract front matter to %q: %w", templateFilePath, err) + } + m.infof("extracting code examples from %q", filename) + err = m.ExtractCodeExamples(data, exampleRelDir, templateFilePath) + if err != nil { + return fmt.Errorf("unable to extract code examples from %q: %w", templateFilePath, err) + } + return nil +} + +func (m *migrator) ExtractFrontMatter(content []byte, templateFile string) error { + fileScanner := bufio.NewScanner(bytes.NewReader(content)) + fileScanner.Split(bufio.ScanLines) + + hasFirstLine := fileScanner.Scan() + if !hasFirstLine || fileScanner.Text() != "---" { + m.warnf("no frontmatter found in %q", templateFile) + return nil + } + err := appendFile(templateFile, []byte(fileScanner.Text()+"\n")) + if err != nil { + return fmt.Errorf("unable to append frontmatter to %q: %w", templateFile, err) + } + exited := false + for fileScanner.Scan() { + err = appendFile(templateFile, []byte(fileScanner.Text()+"\n")) + if err != nil { + return fmt.Errorf("unable to append frontmatter to %q: %w", templateFile, err) + } + if fileScanner.Text() == "---" { + exited = true + break + } + } + + if !exited { + return fmt.Errorf("cannot find ending of frontmatter block in %q", templateFile) + } + + return nil +} + +func (m *migrator) ExtractCodeExamples(content []byte, newRelDir string, templateFilePath string) error { + templateFile, err := os.OpenFile(templateFilePath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) + if err != nil { + return fmt.Errorf("unable to open file %q: %w", templateFilePath, err) + } + defer func(f *os.File) { + err := f.Close() + if err != nil { + m.warnf("unable to close file %q: %q", templateFilePath, err) + } + }(templateFile) + + md := newMarkdownRenderer() + p := md.Parser() + root := p.Parse(text.NewReader(content)) + + exampleCount := 0 + importCount := 0 + + err = ast.Walk(root, func(node ast.Node, enter bool) (ast.WalkStatus, error) { + // skip the root node + if !enter || node.Type() == ast.TypeDocument { + return ast.WalkContinue, nil + } + + if fencedNode, isFenced := node.(*ast.FencedCodeBlock); isFenced && fencedNode.Info != nil { + var ext, exampleName, examplePath, template string + + lang := string(fencedNode.Info.Text(content)[:]) + switch lang { + case "hcl", "terraform": + exampleCount++ + ext = ".tf" + exampleName = "example_" + strconv.Itoa(exampleCount) + ext + examplePath = filepath.Join(m.ProviderExamplesDir(), newRelDir, exampleName) + template = fmt.Sprintf(exampleTFCodeTemplate, examplePath) + m.infof("creating example file %q", examplePath) + case "console": + importCount++ + ext = ".sh" + exampleName = "import_" + strconv.Itoa(importCount) + ext + examplePath = filepath.Join(m.ProviderExamplesDir(), newRelDir, exampleName) + template = fmt.Sprintf(exampleImportCodeTemplate, examplePath) + m.infof("creating import file %q", examplePath) + default: + // Render node as is + m.infof("skipping code block with unknown language %q", lang) + err = md.Renderer().Render(templateFile, content, node) + if err != nil { + return ast.WalkStop, fmt.Errorf("unable to render node: %w", err) + } + return ast.WalkSkipChildren, nil + } + + // add code block text to buffer + codeBuf := bytes.Buffer{} + for i := 0; i < node.Lines().Len(); i++ { + line := node.Lines().At(i) + _, _ = codeBuf.Write(line.Value(content)) + } + + // create example file from code block + err = writeFile(examplePath, codeBuf.String()) + if err != nil { + return ast.WalkStop, fmt.Errorf("unable to write file %q: %w", examplePath, err) + } + + // replace original code block with tfplugindocs template + _, err = templateFile.WriteString("\n\n" + template) + if err != nil { + return ast.WalkStop, fmt.Errorf("unable to write to template %q: %w", template, err) + } + + return ast.WalkSkipChildren, nil + } + + // Render non-code nodes as is + err = md.Renderer().Render(templateFile, content, node) + if err != nil { + return ast.WalkStop, fmt.Errorf("unable to render node: %w", err) + } + if node.HasChildren() { + return ast.WalkSkipChildren, nil + } + + return ast.WalkContinue, nil + }) + if err != nil { + return fmt.Errorf("unable to walk AST: %w", err) + } + + _, err = templateFile.WriteString("\n") + if err != nil { + return fmt.Errorf("unable to write to template %q: %w", templateFilePath, err) + } + m.infof("finished creating template %q", templateFilePath) + + return nil +} + // OldProviderWebsiteDir returns the absolute path to the joined provider and // given old website directory, which defaults to "website". func (m *migrator) OldProviderWebsiteDir() string { return filepath.Join(m.providerDir, m.oldWebsiteDir) } -// NewProviderWebsiteDir returns the absolute path to the joined provider and +// ProviderTemplatesDir returns the absolute path to the joined provider and // given new templates directory, which defaults to "templates". -func (m *migrator) NewProviderWebsiteDir() string { - return filepath.Join(m.providerDir, m.newWebsiteDir) +func (m *migrator) ProviderTemplatesDir() string { + return filepath.Join(m.providerDir, m.templatesDir) +} + +// ProviderExamplesDir returns the absolute path to the joined provider and +// given examples directory, which defaults to "examples". +func (m *migrator) ProviderExamplesDir() string { + return filepath.Join(m.providerDir, m.examplesDir) } diff --git a/internal/provider/util.go b/internal/provider/util.go index bb4f50e4..047583f8 100644 --- a/internal/provider/util.go +++ b/internal/provider/util.go @@ -12,7 +12,12 @@ import ( "path/filepath" "strings" + "github.com/Kunde21/markdownfmt/v3/markdown" tfjson "github.com/hashicorp/terraform-json" + "github.com/yuin/goldmark" + meta "github.com/yuin/goldmark-meta" + "github.com/yuin/goldmark/extension" + "github.com/yuin/goldmark/parser" ) func providerShortName(n string) string { @@ -94,6 +99,31 @@ func writeFile(path string, data string) error { return nil } +// appendFile appends data to the specified file or creates the file if it doesn't exist. +func appendFile(path string, data []byte) error { + dir, _ := filepath.Split(path) + err := os.MkdirAll(dir, 0755) + if err != nil { + return fmt.Errorf("unable to make dir %q: %w", dir, err) + } + + f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644) + if err != nil { + return fmt.Errorf("unable to open file %q: %w", path, err) + } + + _, err = f.Write(data) + if err != nil { + return fmt.Errorf("unable to write file %q: %w", path, err) + } + err = f.Close() + if err != nil { + return fmt.Errorf("unable to close file %q: %w", path, err) + } + + return nil +} + func runCmd(cmd *exec.Cmd) ([]byte, error) { output, err := cmd.CombinedOutput() if err != nil { @@ -160,3 +190,21 @@ func extractSchemaFromFile(path string) (*tfjson.ProviderSchemas, error) { return schemas, nil } + +func newMarkdownRenderer() goldmark.Markdown { + mr := markdown.NewRenderer() + extensions := []goldmark.Extender{ + extension.GFM, + meta.Meta, // We need this to skip YAML frontmatter when parsing. + } + parserOptions := []parser.Option{ + parser.WithAttribute(), // We need this to enable # headers {#custom-ids}. + } + + gm := goldmark.New( + goldmark.WithExtensions(extensions...), + goldmark.WithParserOptions(parserOptions...), + goldmark.WithRenderer(mr), + ) + return gm +} From 952eaff9b5566602424888557a7604fd2182a62c Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Wed, 20 Dec 2023 14:04:42 -0500 Subject: [PATCH 04/17] Remove `-help` output check --- .../migrate/time_provider_success.txtar | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success.txtar b/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success.txtar index 71cd11a8..97d6d804 100644 --- a/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success.txtar +++ b/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success.txtar @@ -4,10 +4,6 @@ # Successful run of tfplugindocs -migrate on the Time provider [!unix] skip -# Check tool tips -exec tfplugindocs migrate -help -#cmp stderr expected-help-output.txt - # Run migrate command exec tfplugindocs migrate cmpenv stdout expected-output.txt @@ -81,16 +77,6 @@ creating example file "$WORK/examples/resources/static/example_2.tf" creating import file "$WORK/examples/resources/static/import_1.sh" finished creating template "$WORK/templates/resources/static.md.tmpl" copying non-template file "time.erb" --- expected-help-output.txt -- - -Usage: tfplugindocs migrate [] - - --examples-dir examples directory based on provider-dir (default: "examples") - --old-website-source-dir old website directory based on provider-dir; files will be migrated from this directory (default: "website") - --provider-dir relative or absolute path to the root provider code directory when running the command outside the root provider code directory - --templates-dir new website templates directory based on provider-dir; files will be migrated to this directory (default: "templates") - - -- website/docs/index.html.markdown -- --- layout: "time" From 9d76267155ccb98b8bef878d5d1a7fcef8fbf08e Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Wed, 20 Dec 2023 14:14:25 -0500 Subject: [PATCH 05/17] Add changelog entry --- .changes/unreleased/FEATURES-20231220-141244.yaml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changes/unreleased/FEATURES-20231220-141244.yaml diff --git a/.changes/unreleased/FEATURES-20231220-141244.yaml b/.changes/unreleased/FEATURES-20231220-141244.yaml new file mode 100644 index 00000000..cfafd7ec --- /dev/null +++ b/.changes/unreleased/FEATURES-20231220-141244.yaml @@ -0,0 +1,7 @@ +kind: FEATURES +body: adds new `migrate` subcommand migrates that existing provider docs using the + old website directory structure (`website/docs/`) to a `terraform-plugin-docs`-supported + templates directory. +time: 2023-12-20T14:12:44.820323-05:00 +custom: + Issue: "314" From a67b6961117c3385d8d47624dd8e90b19abfaa62 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Thu, 21 Dec 2023 11:54:34 -0500 Subject: [PATCH 06/17] Update changelog entry Co-authored-by: Brian Flad --- .changes/unreleased/FEATURES-20231220-141244.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changes/unreleased/FEATURES-20231220-141244.yaml b/.changes/unreleased/FEATURES-20231220-141244.yaml index cfafd7ec..a73d15e8 100644 --- a/.changes/unreleased/FEATURES-20231220-141244.yaml +++ b/.changes/unreleased/FEATURES-20231220-141244.yaml @@ -1,5 +1,5 @@ kind: FEATURES -body: adds new `migrate` subcommand migrates that existing provider docs using the +body: migrate: Added new `migrate` subcommand that migrates existing provider docs using the old website directory structure (`website/docs/`) to a `terraform-plugin-docs`-supported templates directory. time: 2023-12-20T14:12:44.820323-05:00 From fd96f85375d760f0efb9b1c8f99d72a27b09c65b Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Thu, 21 Dec 2023 12:22:04 -0500 Subject: [PATCH 07/17] fix changelog entry --- .changes/unreleased/FEATURES-20231220-141244.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changes/unreleased/FEATURES-20231220-141244.yaml b/.changes/unreleased/FEATURES-20231220-141244.yaml index a73d15e8..dc8e0ee6 100644 --- a/.changes/unreleased/FEATURES-20231220-141244.yaml +++ b/.changes/unreleased/FEATURES-20231220-141244.yaml @@ -1,7 +1,7 @@ kind: FEATURES -body: migrate: Added new `migrate` subcommand that migrates existing provider docs using the +body: 'migrate: Added new `migrate` subcommand that migrates existing provider docs using the old website directory structure (`website/docs/`) to a `terraform-plugin-docs`-supported - templates directory. + templates directory.' time: 2023-12-20T14:12:44.820323-05:00 custom: Issue: "314" From 5240d476155c45defc92fe9ec2f2ea5d693b2800 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Mon, 8 Jan 2024 16:31:30 -0500 Subject: [PATCH 08/17] Remove `--old-website-source-dir` flag and support migrating `docs/` subdirectory --- .../unreleased/FEATURES-20231220-141244.yaml | 2 +- README.md | 30 +- .../time_provider_success_docs_website.txtar | 870 ++++++++++++++++++ ...ime_provider_success_legacy_website.txtar} | 2 +- internal/cmd/migrate.go | 4 +- internal/provider/migrate.go | 90 +- 6 files changed, 962 insertions(+), 36 deletions(-) create mode 100644 cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_docs_website.txtar rename cmd/tfplugindocs/testdata/scripts/schema-json/migrate/{time_provider_success.txtar => time_provider_success_legacy_website.txtar} (99%) diff --git a/.changes/unreleased/FEATURES-20231220-141244.yaml b/.changes/unreleased/FEATURES-20231220-141244.yaml index dc8e0ee6..ee1fb4d0 100644 --- a/.changes/unreleased/FEATURES-20231220-141244.yaml +++ b/.changes/unreleased/FEATURES-20231220-141244.yaml @@ -1,6 +1,6 @@ kind: FEATURES body: 'migrate: Added new `migrate` subcommand that migrates existing provider docs using the - old website directory structure (`website/docs/`) to a `terraform-plugin-docs`-supported + rendered website source directories (`website/docs/` or `/docs/`) to a `terraform-plugin-docs`-supported templates directory.' time: 2023-12-20T14:12:44.820323-05:00 custom: diff --git a/README.md b/README.md index 3905abd6..78b3b23e 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ The primary way users will interact with this is the `tfplugindocs` CLI tool to ## `tfplugindocs` -The `tfplugindocs` CLI has two main commands, `validate` and `generate` (`generate` is the default). +The `tfplugindocs` CLI has three main commands, `migrate`, `validate` and `generate` (`generate` is the default). This tool will let you generate documentation for your provider from live example `.tf` files and markdown templates. It will also export schema information from the provider (using `terraform providers schema -json`), and sync the schema with the reference documents. @@ -28,6 +28,7 @@ Usage: tfplugindocs [--version] [--help] [] Available commands are: the generate command is run by default generate generates a plugin website from code, templates, and examples + migrate migrates website files from either the legacy rendered website directory (`website/docs/r`) or the docs rendered website directory (`docs/resources`) to the tfplugindocs supported structure (`templates/`). validate validates a plugin website for the current directory ``` @@ -67,7 +68,6 @@ $ tfplugindocs migrate --help Usage: tfplugindocs migrate [] --examples-dir examples directory based on provider-dir (default: "examples") - --old-website-source-dir old website directory based on provider-dir; files will be migrated from this directory (default: "website") --provider-dir relative or absolute path to the root provider code directory when running the command outside the root provider code directory --templates-dir new website templates directory based on provider-dir; files will be migrated to this directory (default: "templates") ``` @@ -115,12 +115,15 @@ Otherwise, the provider developer can set an arbitrary description like this: #### Migrate subcommand -The `migrate` subcommand can be used to migrate from the old directory structure (`website/docs/r`) to the `tfplugindocs` supported structure (`templates/`). +The `migrate` subcommand can be used to migrate website files from either the legacy rendered website directory (`website/docs/r`) or the docs +rendered website directory (`docs/resources`) to the `tfplugindocs` supported structure (`templates/`). Markdown files in the rendered website +directory will be converted to `tfplugindocs` templates. The `migrate` subcommand takes the following actions: -- Copies the `--old-website-source-dir` folder to the `--tempates-dir` folder (will create this folder if it doesn't exist) -- Renames `docs/d/` and `docs/r/` subdirectories to `data-sources/` and `resources/` respectively -- Change file suffixes for template files from `.html.markdown` to `.md.tmpl` +- Determines the rendered website directory based on the `--provider-dir` argument +- Copies the contents of the rendered website directory to the `--tempates-dir` folder (will create this folder if it doesn't exist) +- (if the rendered website is using legacy format) Renames `docs/d/` and `docs/r/` subdirectories to `data-sources/` and `resources/` respectively +- Change file suffixes for Markdown files to `.md.tmpl` to create website templates - Extracts code blocks from website docs to create individual example files in `--examples-dir` (will create this folder if it doesn't exist) - replace extracted example code in website templates with `tfplugindocs` template code referring to example files. - Copies non-template files to `--templates-dir` folder @@ -157,7 +160,9 @@ For examples: #### Migration -The `migrate` subcommand assumes the following conventional paths for the old website structure: +The `migrate` subcommand assumes the following conventional paths for the rendered website directory: + +Legacy website directory structure: | Path | Description | |---------------------------------------------------|----------------------| @@ -166,6 +171,17 @@ The `migrate` subcommand assumes the following conventional paths for the old we | `website/docs/d/.html.markdown` | Data source page | | `website/docs/r/.html.markdown` | Resource page | +Docs website directory structure: + +| Path | Description | +|------------------------------------------------------|----------------------| +| `docs/` | Root of website docs | +| `docs/index.html.markdown` | Docs index page | +| `docs/data-sources/.html.markdown` | Data source page | +| `docs/resources/.html.markdown` | Resource page | + +Files in these directories will be migrated to the `templates/` directory. + ### Templates The templates are implemented with Go [`text/template`](https://golang.org/pkg/text/template/) diff --git a/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_docs_website.txtar b/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_docs_website.txtar new file mode 100644 index 00000000..84084ac8 --- /dev/null +++ b/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_docs_website.txtar @@ -0,0 +1,870 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +# Successful run of tfplugindocs -migrate on the Time provider using the docs website layout +[!unix] skip + +# Run migrate command +exec tfplugindocs migrate +cmpenv stdout expected-output.txt + +# Check template files +cmpenv templates/index.md.tmpl exp-templates/index.md.tmpl +cmpenv templates/resources/offset.md.tmpl exp-templates/resources/offset.md.tmpl +cmpenv templates/resources/rotating.md.tmpl exp-templates/resources/rotating.md.tmpl +cmpenv templates/resources/sleep.md.tmpl exp-templates/resources/sleep.md.tmpl +cmpenv templates/resources/static.md.tmpl exp-templates/resources/static.md.tmpl + +# Check generated example files +cmpenv examples/example_1.tf examples/example_1.tf + +cmpenv examples/resources/offset/example_1.tf exp-examples/resources/offset/example_1.tf +cmpenv examples/resources/offset/example_2.tf exp-examples/resources/offset/example_2.tf +cmpenv examples/resources/offset/import_1.sh exp-examples/resources/offset/import_1.sh + +cmpenv examples/resources/rotating/example_1.tf exp-examples/resources/rotating/example_1.tf +cmpenv examples/resources/rotating/import_1.sh exp-examples/resources/rotating/import_1.sh +cmpenv examples/resources/rotating/import_2.sh exp-examples/resources/rotating/import_2.sh + +cmpenv examples/resources/sleep/example_1.tf exp-examples/resources/sleep/example_1.tf +cmpenv examples/resources/sleep/example_2.tf exp-examples/resources/sleep/example_2.tf +cmpenv examples/resources/sleep/example_3.tf exp-examples/resources/sleep/example_3.tf +cmpenv examples/resources/sleep/import_1.sh exp-examples/resources/sleep/import_1.sh +cmpenv examples/resources/sleep/import_2.sh exp-examples/resources/sleep/import_2.sh + +cmpenv examples/resources/static/example_1.tf examples/resources/static/example_1.tf +cmpenv examples/resources/static/example_2.tf examples/resources/static/example_2.tf +cmpenv examples/resources/static/import_1.sh examples/resources/static/import_1.sh + +-- expected-output.txt -- +migrating website from "$WORK/docs" to "$WORK/templates" +migrating provider index +extracting YAML frontmatter to "$WORK/templates/index.md.tmpl" +extracting code examples from "index.md.tmpl" +creating example file "$WORK/examples/example_1.tf" +finished creating template "$WORK/templates/index.md.tmpl" +migrating resource "offset" +extracting YAML frontmatter to "$WORK/templates/resources/offset.md.tmpl" +extracting code examples from "offset.md.tmpl" +creating example file "$WORK/examples/resources/offset/example_1.tf" +creating example file "$WORK/examples/resources/offset/example_2.tf" +creating import file "$WORK/examples/resources/offset/import_1.sh" +finished creating template "$WORK/templates/resources/offset.md.tmpl" +migrating resource "rotating" +extracting YAML frontmatter to "$WORK/templates/resources/rotating.md.tmpl" +extracting code examples from "rotating.md.tmpl" +creating example file "$WORK/examples/resources/rotating/example_1.tf" +creating import file "$WORK/examples/resources/rotating/import_1.sh" +creating import file "$WORK/examples/resources/rotating/import_2.sh" +finished creating template "$WORK/templates/resources/rotating.md.tmpl" +migrating resource "sleep" +extracting YAML frontmatter to "$WORK/templates/resources/sleep.md.tmpl" +extracting code examples from "sleep.md.tmpl" +creating example file "$WORK/examples/resources/sleep/example_1.tf" +creating example file "$WORK/examples/resources/sleep/example_2.tf" +creating example file "$WORK/examples/resources/sleep/example_3.tf" +creating import file "$WORK/examples/resources/sleep/import_1.sh" +creating import file "$WORK/examples/resources/sleep/import_2.sh" +finished creating template "$WORK/templates/resources/sleep.md.tmpl" +migrating resource "static" +extracting YAML frontmatter to "$WORK/templates/resources/static.md.tmpl" +extracting code examples from "static.md.tmpl" +creating example file "$WORK/examples/resources/static/example_1.tf" +creating example file "$WORK/examples/resources/static/example_2.tf" +creating import file "$WORK/examples/resources/static/import_1.sh" +finished creating template "$WORK/templates/resources/static.md.tmpl" +-- docs/index.html.markdown -- +--- +layout: "time" +page_title: "Provider: Time" +description: |- + The time provider is used to interact with time-based resources. +--- + +# Time Provider + +The time provider is used to interact with time-based resources. The provider itself has no configuration options. + +Use the navigation to the left to read about the available resources. + +## Resource "Triggers" + +Certain time resources, only perform actions during specific lifecycle actions: + +- `time_offset`: Saves base timestamp into Terraform state only when created. +- `time_sleep`: Sleeps when created and/or destroyed. +- `time_static`: Saves base timestamp into Terraform state only when created. + +These resources provide an optional map argument called `triggers` that can be populated with arbitrary key/value pairs. When the keys or values of this argument are updated, Terraform will re-perform the desired action, such as updating the base timestamp or sleeping again. + +For example: + +```hcl +resource "time_static" "ami_update" { + triggers = { + # Save the time each switch of an AMI id + ami_id = data.aws_ami.example.id + } +} + +resource "aws_instance" "server" { + # Read the AMI id "through" the time_static resource to ensure that + # both will change together. + ami = time_static.ami_update.triggers.ami_id + + tags = { + AmiUpdateTime = time_static.ami_update.rfc3339 + } + + # ... (other aws_instance arguments) ... +} +``` + +`triggers` are *not* treated as sensitive attributes; a value used for `triggers` will be displayed in Terraform UI output as plaintext. + +To force a these actions to reoccur without updating `triggers`, the [`terraform taint` command](https://www.terraform.io/docs/commands/taint.html) can be used to produce the action on the next run. +-- docs/resources/offset.html.markdown -- +--- +layout: "time" +page_title: "Time: time_offset" +description: |- + Manages a offset time resource. +--- + +# Resource: time_offset + +Manages an offset time resource, which keeps an UTC timestamp stored in the Terraform state that is offset from a locally sourced base timestamp. This prevents perpetual differences caused by using the [`timestamp()` function](https://www.terraform.io/docs/configuration/functions/timestamp.html). + +-> Further manipulation of incoming or outgoing values can be accomplished with the [`formatdate()` function](https://www.terraform.io/docs/configuration/functions/formatdate.html) and the [`timeadd()` function](https://www.terraform.io/docs/configuration/functions/timeadd.html). + +## Example Usage + +### Basic Usage + +```hcl +resource "time_offset" "example" { + offset_days = 7 +} + +output "one_week_from_now" { + value = time_offset.example.rfc3339 +} +``` + +### Triggers Usage + +```hcl +resource "time_offset" "ami_update" { + triggers = { + # Save the time each switch of an AMI id + ami_id = data.aws_ami.example.id + } + + offset_days = 7 +} + +resource "aws_instance" "server" { + # Read the AMI id "through" the time_offset resource to ensure that + # both will change together. + ami = time_offset.ami_update.triggers.ami_id + + tags = { + ExpirationTime = time_offset.ami_update.rfc3339 + } + + # ... (other aws_instance arguments) ... +} +``` + +## Argument Reference + +~> **NOTE:** At least one of the `offset_` arguments must be configured. + +The following arguments are optional: + +* `base_rfc3339` - (Optional) Configure the base timestamp with an UTC [RFC3339 time string](https://tools.ietf.org/html/rfc3339#section-5.8) (`YYYY-MM-DDTHH:MM:SSZ`). Defaults to the current time. +* `triggers` - (Optional) Arbitrary map of values that, when changed, will trigger a new base timestamp value to be saved. See [the main provider documentation](../index.html) for more information. +* `offset_days` - (Optional) Number of days to offset the base timestamp. Conflicts with other `offset_` arguments. +* `offset_hours` - (Optional) Number of hours to offset the base timestamp. Conflicts with other `offset_` arguments. +* `offset_minutes` - (Optional) Number of minutes to offset the base timestamp. Conflicts with other `offset_` arguments. +* `offset_months` - (Optional) Number of months to offset the base timestamp. Conflicts with other `offset_` arguments. +* `offset_seconds` - (Optional) Number of seconds to offset the base timestamp. Conflicts with other `offset_` arguments. +* `offset_years` - (Optional) Number of years to offset the base timestamp. Conflicts with other `offset_` arguments. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `day` - Number day of offset timestamp. +* `hour` - Number hour of offset timestamp. +* `id` - UTC RFC3339 format of the base timestamp, e.g. `2020-02-12T06:36:13Z`. +* `minute` - Number minute of offset timestamp. +* `month` - Number month of offset timestamp. +* `rfc3339` - UTC RFC3339 format of the offset timestamp, e.g. `2020-02-12T06:36:13Z`. +* `second` - Number second of offset timestamp. +* `unix` - Number of seconds since epoch time, e.g. `1581489373`. +* `year` - Number year of offset timestamp. + +## Import + +This resource can be imported using the base UTC RFC3339 timestamp and offset years, months, days, hours, minutes, and seconds, separated by commas (`,`), e.g. + +```console +$ terraform import time_offset.example 2020-02-12T06:36:13Z,0,0,7,0,0,0 +``` + +The `triggers` argument cannot be imported. +-- docs/resources/rotating.html.markdown -- +--- +layout: "time" +page_title: "Time: time_rotating" +description: |- + Manages a rotating time resource. +--- + +# Resource: time_rotating + +Manages a rotating time resource, which keeps a rotating UTC timestamp stored in the Terraform state and proposes resource recreation when the locally sourced current time is beyond the rotation time. This rotation only occurs when Terraform is executed, meaning there will be drift between the rotation timestamp and actual rotation. The new rotation timestamp offset includes this drift. This prevents perpetual differences caused by using the [`timestamp()` function](https://www.terraform.io/docs/configuration/functions/timestamp.html) by only forcing a new value on the set cadence. + +-> Further manipulation of incoming or outgoing values can be accomplished with the [`formatdate()` function](https://www.terraform.io/docs/configuration/functions/formatdate.html) and the [`timeadd()` function](https://www.terraform.io/docs/configuration/functions/timeadd.html). + +## Example Usage + +This example configuration will rotate (destroy/create) the resource every 30 days. + +```hcl +resource "time_rotating" "example" { + rotation_days = 30 +} +``` + +## Argument Reference + +~> **NOTE:** At least one of the `rotation_` arguments must be configured. + +The following arguments are optional: + +* `rfc3339` - (Optional) Configure the base timestamp with an UTC [RFC3339 time string](https://tools.ietf.org/html/rfc3339#section-5.8) (`YYYY-MM-DDTHH:MM:SSZ`). Defaults to the current time. +* `rotation_days` - (Optional) Number of days to add to the base timestamp to configure the rotation timestamp. When the current time has passed the rotation timestamp, the resource will trigger recreation. Conflicts with other `rotation_` arguments. +* `rotation_hours` - (Optional) Number of hours to add to the base timestamp to configure the rotation timestamp. When the current time has passed the rotation timestamp, the resource will trigger recreation. Conflicts with other `rotation_` arguments. +* `rotation_minutes` - (Optional) Number of minutes to add to the base timestamp to configure the rotation timestamp. When the current time has passed the rotation timestamp, the resource will trigger recreation. Conflicts with other `rotation_` arguments. +* `rotation_months` - (Optional) Number of months to add to the base timestamp to configure the rotation timestamp. When the current time has passed the rotation timestamp, the resource will trigger recreation. Conflicts with other `rotation_` arguments. +* `rotation_rfc3339` - (Optional) Configure the rotation timestamp with an UTC [RFC3339 time string](https://tools.ietf.org/html/rfc3339#section-5.8) (`YYYY-MM-DDTHH:MM:SSZ`). When the current time has passed the rotation timestamp, the resource will trigger recreation. Conflicts with other `rotation_` arguments. +* `rotation_years` - (Optional) Number of years to add to the base timestamp to configure the rotation timestamp. When the current time has passed the rotation timestamp, the resource will trigger recreation. Conflicts with other `rotation_` arguments. +* `triggers` - (Optional) Arbitrary map of values that, when changed, will trigger a new base timestamp value to be saved. These conditions recreate the resource in addition to other rotation arguments. See [the main provider documentation](../index.html) for more information. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `day` - Number day of timestamp. +* `hour` - Number hour of timestamp. +* `id` - UTC RFC3339 format of the base timestamp, e.g. `2020-02-12T06:36:13Z`. +* `minute` - Number minute of timestamp. +* `month` - Number month of timestamp. +* `second` - Number second of timestamp. +* `unix` - Number of seconds since epoch time, e.g. `1581489373`. +* `year` - Number year of timestamp. + +## Import + +This resource can be imported using the base UTC RFC3339 value and rotation years, months, days, hours, and minutes, separated by commas (`,`), e.g. for 30 days + +```console +$ terraform import time_rotation.example 2020-02-12T06:36:13Z,0,0,30,0,0 +``` + +Otherwise, to import with the rotation RFC3339 value, the base UTC RFC3339 value and rotation UTC RFC3339 value, separated by commas (`,`), e.g. + +```console +$ terraform import time_rotation.example 2020-02-12T06:36:13Z,2020-02-13T06:36:13Z +``` + +The `triggers` argument cannot be imported. +-- docs/resources/sleep.html.markdown -- +--- +layout: "time" +page_title: "Time: time_sleep" +description: |- + Manages a static time resource. +--- + +# Resource: time_sleep + +Manages a resource that delays creation and/or destruction, typically for further resources. This prevents cross-platform compatibility and destroy-time issues with using the [`local-exec` provisioner](https://www.terraform.io/docs/provisioners/local-exec.html). + +-> In many cases, this resource should be considered a workaround for issues that should be reported and handled in downstream Terraform Provider logic. Downstream resources can usually introduce or adjust retries in their code to handle time delay issues for all Terraform configurations or upstream resources can be improved to better wait for a resource to be fully ready and available. + +## Example Usage + +### Delay Create Usage + +```hcl +# This resource will destroy (potentially immediately) after null_resource.next +resource "null_resource" "previous" {} + +resource "time_sleep" "wait_30_seconds" { + depends_on = [null_resource.previous] + + create_duration = "30s" +} + +# This resource will create (at least) 30 seconds after null_resource.previous +resource "null_resource" "next" { + depends_on = [time_sleep.wait_30_seconds] +} +``` + +### Delay Destroy Usage + +```hcl +# This resource will destroy (at least) 30 seconds after null_resource.next +resource "null_resource" "previous" {} + +resource "time_sleep" "wait_30_seconds" { + depends_on = [null_resource.previous] + + destroy_duration = "30s" +} + +# This resource will create (potentially immediately) after null_resource.previous +resource "null_resource" "next" { + depends_on = [time_sleep.wait_30_seconds] +} +``` + +### Triggers Usage + +```hcl +resource "aws_ram_resource_association" "example" { + resource_arn = aws_subnet.example.arn + resource_share_arn = aws_ram_resource_share.example.arn +} + +# AWS resources shared via Resource Access Manager can take a few seconds to +# propagate across AWS accounts after RAM returns a successful association. +resource "time_sleep" "ram_resource_propagation" { + create_duration = "60s" + + triggers = { + # This sets up a proper dependency on the RAM association + subnet_arn = aws_ram_resource_association.example.resource_arn + subnet_id = aws_subnet.example.id + } +} + +resource "aws_db_subnet_group" "example" { + name = "example" + + # Read the Subnet identifier "through" the time_sleep resource to ensure a + # proper dependency and that both will change together. + subnet_ids = [time_sleep.ram_resource_propagation.triggers["subnet_id"]] +} +``` + +## Argument Reference + +The following arguments are optional: + +* `create_duration` - (Optional) [Time duration][1] to delay resource creation. For example, `30s` for 30 seconds or `5m` for 5 minutes. Updating this value by itself will not trigger a delay. +* `destroy_duration` - (Optional) [Time duration][1] to delay resource destroy. For example, `30s` for 30 seconds or `5m` for 5 minutes. Updating this value by itself will not trigger a delay. This value or any updates to it must be successfully applied into the Terraform state before destroying this resource to take effect. +* `triggers` - (Optional) Arbitrary map of values that, when changed, will run any creation or destroy delays again. See [the main provider documentation](../index.html) for more information. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - UTC RFC3339 timestamp of the creation or import, e.g. `2020-02-12T06:36:13Z`. + +## Import + +This resource can be imported with the `create_duration` and `destroy_duration`, separated by a comma (`,`). + +e.g. For 30 seconds create duration with no destroy duration: + +```console +$ terraform import time_sleep.example 30s, +``` + +e.g. For 30 seconds destroy duration with no create duration: + +```console +$ terraform import time_sleep.example ,30s +``` + +The `triggers` argument cannot be imported. + +[1]: https://golang.org/pkg/time/#ParseDuration +-- docs/resources/static.html.markdown -- +--- +layout: "time" +page_title: "Time: time_static" +description: |- + Manages a static time resource. +--- + +# Resource: time_static + +Manages a static time resource, which keeps a locally sourced UTC timestamp stored in the Terraform state. This prevents perpetual differences caused by using the [`timestamp()` function](https://www.terraform.io/docs/configuration/functions/timestamp.html). + +-> Further manipulation of incoming or outgoing values can be accomplished with the [`formatdate()` function](https://www.terraform.io/docs/configuration/functions/formatdate.html) and the [`timeadd()` function](https://www.terraform.io/docs/configuration/functions/timeadd.html). + +## Example Usage + +### Basic Usage + +```hcl +resource "time_static" "example" {} + +output "current_time" { + value = time_static.example.rfc3339 +} +``` + +### Triggers Usage + +```hcl +resource "time_static" "ami_update" { + triggers = { + # Save the time each switch of an AMI id + ami_id = data.aws_ami.example.id + } +} + +resource "aws_instance" "server" { + # Read the AMI id "through" the time_static resource to ensure that + # both will change together. + ami = time_static.ami_update.triggers.ami_id + + tags = { + AmiUpdateTime = time_static.ami_update.rfc3339 + } + + # ... (other aws_instance arguments) ... +} +``` + +## Argument Reference + +The following arguments are optional: + +* `triggers` - (Optional) Arbitrary map of values that, when changed, will trigger a new base timestamp value to be saved. See [the main provider documentation](../index.html) for more information. +* `rfc3339` - (Optional) Configure the base timestamp with an UTC [RFC3339 time string](https://tools.ietf.org/html/rfc3339#section-5.8) (`YYYY-MM-DDTHH:MM:SSZ`). Defaults to the current time. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `day` - Number day of timestamp. +* `hour` - Number hour of timestamp. +* `id` - UTC RFC3339 timestamp format, e.g. `2020-02-12T06:36:13Z`. +* `minute` - Number minute of timestamp. +* `month` - Number month of timestamp. +* `rfc3339` - UTC RFC3339 format of timestamp, e.g. `2020-02-12T06:36:13Z`. +* `second` - Number second of timestamp. +* `unix` - Number of seconds since epoch time, e.g. `1581489373`. +* `year` - Number year of timestamp. + +## Import + +This resource can be imported using the UTC RFC3339 value, e.g. + +```console +$ terraform import time_static.example 2020-02-12T06:36:13Z +``` + +The `triggers` argument cannot be imported. +-- exp-examples/example_1.tf -- +resource "time_static" "ami_update" { + triggers = { + # Save the time each switch of an AMI id + ami_id = data.aws_ami.example.id + } +} + +resource "aws_instance" "server" { + # Read the AMI id "through" the time_static resource to ensure that + # both will change together. + ami = time_static.ami_update.triggers.ami_id + + tags = { + AmiUpdateTime = time_static.ami_update.rfc3339 + } + + # ... (other aws_instance arguments) ... +} +-- exp-examples/resources/offset/example_1.tf -- +resource "time_offset" "example" { + offset_days = 7 +} + +output "one_week_from_now" { + value = time_offset.example.rfc3339 +} +-- exp-examples/resources/offset/example_2.tf -- +resource "time_offset" "ami_update" { + triggers = { + # Save the time each switch of an AMI id + ami_id = data.aws_ami.example.id + } + + offset_days = 7 +} + +resource "aws_instance" "server" { + # Read the AMI id "through" the time_offset resource to ensure that + # both will change together. + ami = time_offset.ami_update.triggers.ami_id + + tags = { + ExpirationTime = time_offset.ami_update.rfc3339 + } + + # ... (other aws_instance arguments) ... +} +-- exp-examples/resources/offset/import_1.sh -- +$ terraform import time_offset.example 2020-02-12T06:36:13Z,0,0,7,0,0,0 +-- exp-examples/resources/rotating/example_1.tf -- +resource "time_rotating" "example" { + rotation_days = 30 +} +-- exp-examples/resources/rotating/import_1.sh -- +$ terraform import time_rotation.example 2020-02-12T06:36:13Z,0,0,30,0,0 +-- exp-examples/resources/rotating/import_2.sh -- +$ terraform import time_rotation.example 2020-02-12T06:36:13Z,2020-02-13T06:36:13Z +-- exp-examples/resources/sleep/example_1.tf -- +# This resource will destroy (potentially immediately) after null_resource.next +resource "null_resource" "previous" {} + +resource "time_sleep" "wait_30_seconds" { + depends_on = [null_resource.previous] + + create_duration = "30s" +} + +# This resource will create (at least) 30 seconds after null_resource.previous +resource "null_resource" "next" { + depends_on = [time_sleep.wait_30_seconds] +} +-- exp-examples/resources/sleep/example_2.tf -- +# This resource will destroy (at least) 30 seconds after null_resource.next +resource "null_resource" "previous" {} + +resource "time_sleep" "wait_30_seconds" { + depends_on = [null_resource.previous] + + destroy_duration = "30s" +} + +# This resource will create (potentially immediately) after null_resource.previous +resource "null_resource" "next" { + depends_on = [time_sleep.wait_30_seconds] +} +-- exp-examples/resources/sleep/example_3.tf -- +resource "aws_ram_resource_association" "example" { + resource_arn = aws_subnet.example.arn + resource_share_arn = aws_ram_resource_share.example.arn +} + +# AWS resources shared via Resource Access Manager can take a few seconds to +# propagate across AWS accounts after RAM returns a successful association. +resource "time_sleep" "ram_resource_propagation" { + create_duration = "60s" + + triggers = { + # This sets up a proper dependency on the RAM association + subnet_arn = aws_ram_resource_association.example.resource_arn + subnet_id = aws_subnet.example.id + } +} + +resource "aws_db_subnet_group" "example" { + name = "example" + + # Read the Subnet identifier "through" the time_sleep resource to ensure a + # proper dependency and that both will change together. + subnet_ids = [time_sleep.ram_resource_propagation.triggers["subnet_id"]] +} +-- exp-examples/resources/sleep/import_1.sh -- +$ terraform import time_sleep.example 30s, +-- exp-examples/resources/sleep/import_2.sh -- +$ terraform import time_sleep.example ,30s +-- exp-examples/resources/static/example_1.tf -- +resource "time_static" "example" {} + +output "current_time" { + value = time_static.example.rfc3339 +} +-- exp-examples/resources/static/example_2.tf -- +resource "time_static" "ami_update" { + triggers = { + # Save the time each switch of an AMI id + ami_id = data.aws_ami.example.id + } +} + +resource "aws_instance" "server" { + # Read the AMI id "through" the time_static resource to ensure that + # both will change together. + ami = time_static.ami_update.triggers.ami_id + + tags = { + AmiUpdateTime = time_static.ami_update.rfc3339 + } + + # ... (other aws_instance arguments) ... +} +-- exp-examples/resources/static/import_1.sh -- +$ terraform import time_static.example 2020-02-12T06:36:13Z +-- exp-templates/index.md.tmpl -- +--- +layout: "time" +page_title: "Provider: Time" +description: |- + The time provider is used to interact with time-based resources. +--- +# Time Provider + +The time provider is used to interact with time-based resources. The provider itself has no configuration options. + +Use the navigation to the left to read about the available resources. + +## Resource "Triggers" + +Certain time resources, only perform actions during specific lifecycle actions: + +- `time_offset`: Saves base timestamp into Terraform state only when created. +- `time_sleep`: Sleeps when created and/or destroyed. +- `time_static`: Saves base timestamp into Terraform state only when created. + +These resources provide an optional map argument called `triggers` that can be populated with arbitrary key/value pairs. When the keys or values of this argument are updated, Terraform will re-perform the desired action, such as updating the base timestamp or sleeping again. + +For example: + +{{tffile "$WORK/examples/example_1.tf"}} + +`triggers` are *not* treated as sensitive attributes; a value used for `triggers` will be displayed in Terraform UI output as plaintext. + +To force a these actions to reoccur without updating `triggers`, the [`terraform taint` command](https://www.terraform.io/docs/commands/taint.html) can be used to produce the action on the next run. +-- exp-templates/resources/offset.md.tmpl -- +--- +layout: "time" +page_title: "Time: time_offset" +description: |- + Manages a offset time resource. +--- +# Resource: time_offset + +Manages an offset time resource, which keeps an UTC timestamp stored in the Terraform state that is offset from a locally sourced base timestamp. This prevents perpetual differences caused by using the [`timestamp()` function](https://www.terraform.io/docs/configuration/functions/timestamp.html). + +-> Further manipulation of incoming or outgoing values can be accomplished with the [`formatdate()` function](https://www.terraform.io/docs/configuration/functions/formatdate.html) and the [`timeadd()` function](https://www.terraform.io/docs/configuration/functions/timeadd.html). + +## Example Usage + +### Basic Usage + +{{tffile "$WORK/examples/resources/offset/example_1.tf"}} + +### Triggers Usage + +{{tffile "$WORK/examples/resources/offset/example_2.tf"}} + +## Argument Reference + +~> **NOTE:** At least one of the `offset_` arguments must be configured. + +The following arguments are optional: + +* `base_rfc3339` - (Optional) Configure the base timestamp with an UTC [RFC3339 time string](https://tools.ietf.org/html/rfc3339#section-5.8) (`YYYY-MM-DDTHH:MM:SSZ`). Defaults to the current time. +* `triggers` - (Optional) Arbitrary map of values that, when changed, will trigger a new base timestamp value to be saved. See [the main provider documentation](../index.html) for more information. +* `offset_days` - (Optional) Number of days to offset the base timestamp. Conflicts with other `offset_` arguments. +* `offset_hours` - (Optional) Number of hours to offset the base timestamp. Conflicts with other `offset_` arguments. +* `offset_minutes` - (Optional) Number of minutes to offset the base timestamp. Conflicts with other `offset_` arguments. +* `offset_months` - (Optional) Number of months to offset the base timestamp. Conflicts with other `offset_` arguments. +* `offset_seconds` - (Optional) Number of seconds to offset the base timestamp. Conflicts with other `offset_` arguments. +* `offset_years` - (Optional) Number of years to offset the base timestamp. Conflicts with other `offset_` arguments. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `day` - Number day of offset timestamp. +* `hour` - Number hour of offset timestamp. +* `id` - UTC RFC3339 format of the base timestamp, e.g. `2020-02-12T06:36:13Z`. +* `minute` - Number minute of offset timestamp. +* `month` - Number month of offset timestamp. +* `rfc3339` - UTC RFC3339 format of the offset timestamp, e.g. `2020-02-12T06:36:13Z`. +* `second` - Number second of offset timestamp. +* `unix` - Number of seconds since epoch time, e.g. `1581489373`. +* `year` - Number year of offset timestamp. + +## Import + +This resource can be imported using the base UTC RFC3339 timestamp and offset years, months, days, hours, minutes, and seconds, separated by commas (`,`), e.g. + +{{codefile "shell" "$WORK/examples/resources/offset/import_1.sh"}} + +The `triggers` argument cannot be imported. +-- exp-templates/resources/rotating.md.tmpl -- +--- +layout: "time" +page_title: "Time: time_rotating" +description: |- + Manages a rotating time resource. +--- +# Resource: time_rotating + +Manages a rotating time resource, which keeps a rotating UTC timestamp stored in the Terraform state and proposes resource recreation when the locally sourced current time is beyond the rotation time. This rotation only occurs when Terraform is executed, meaning there will be drift between the rotation timestamp and actual rotation. The new rotation timestamp offset includes this drift. This prevents perpetual differences caused by using the [`timestamp()` function](https://www.terraform.io/docs/configuration/functions/timestamp.html) by only forcing a new value on the set cadence. + +-> Further manipulation of incoming or outgoing values can be accomplished with the [`formatdate()` function](https://www.terraform.io/docs/configuration/functions/formatdate.html) and the [`timeadd()` function](https://www.terraform.io/docs/configuration/functions/timeadd.html). + +## Example Usage + +This example configuration will rotate (destroy/create) the resource every 30 days. + +{{tffile "$WORK/examples/resources/rotating/example_1.tf"}} + +## Argument Reference + +~> **NOTE:** At least one of the `rotation_` arguments must be configured. + +The following arguments are optional: + +* `rfc3339` - (Optional) Configure the base timestamp with an UTC [RFC3339 time string](https://tools.ietf.org/html/rfc3339#section-5.8) (`YYYY-MM-DDTHH:MM:SSZ`). Defaults to the current time. +* `rotation_days` - (Optional) Number of days to add to the base timestamp to configure the rotation timestamp. When the current time has passed the rotation timestamp, the resource will trigger recreation. Conflicts with other `rotation_` arguments. +* `rotation_hours` - (Optional) Number of hours to add to the base timestamp to configure the rotation timestamp. When the current time has passed the rotation timestamp, the resource will trigger recreation. Conflicts with other `rotation_` arguments. +* `rotation_minutes` - (Optional) Number of minutes to add to the base timestamp to configure the rotation timestamp. When the current time has passed the rotation timestamp, the resource will trigger recreation. Conflicts with other `rotation_` arguments. +* `rotation_months` - (Optional) Number of months to add to the base timestamp to configure the rotation timestamp. When the current time has passed the rotation timestamp, the resource will trigger recreation. Conflicts with other `rotation_` arguments. +* `rotation_rfc3339` - (Optional) Configure the rotation timestamp with an UTC [RFC3339 time string](https://tools.ietf.org/html/rfc3339#section-5.8) (`YYYY-MM-DDTHH:MM:SSZ`). When the current time has passed the rotation timestamp, the resource will trigger recreation. Conflicts with other `rotation_` arguments. +* `rotation_years` - (Optional) Number of years to add to the base timestamp to configure the rotation timestamp. When the current time has passed the rotation timestamp, the resource will trigger recreation. Conflicts with other `rotation_` arguments. +* `triggers` - (Optional) Arbitrary map of values that, when changed, will trigger a new base timestamp value to be saved. These conditions recreate the resource in addition to other rotation arguments. See [the main provider documentation](../index.html) for more information. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `day` - Number day of timestamp. +* `hour` - Number hour of timestamp. +* `id` - UTC RFC3339 format of the base timestamp, e.g. `2020-02-12T06:36:13Z`. +* `minute` - Number minute of timestamp. +* `month` - Number month of timestamp. +* `second` - Number second of timestamp. +* `unix` - Number of seconds since epoch time, e.g. `1581489373`. +* `year` - Number year of timestamp. + +## Import + +This resource can be imported using the base UTC RFC3339 value and rotation years, months, days, hours, and minutes, separated by commas (`,`), e.g. for 30 days + +{{codefile "shell" "$WORK/examples/resources/rotating/import_1.sh"}} + +Otherwise, to import with the rotation RFC3339 value, the base UTC RFC3339 value and rotation UTC RFC3339 value, separated by commas (`,`), e.g. + +{{codefile "shell" "$WORK/examples/resources/rotating/import_2.sh"}} + +The `triggers` argument cannot be imported. +-- exp-templates/resources/sleep.md.tmpl -- +--- +layout: "time" +page_title: "Time: time_sleep" +description: |- + Manages a static time resource. +--- +# Resource: time_sleep + +Manages a resource that delays creation and/or destruction, typically for further resources. This prevents cross-platform compatibility and destroy-time issues with using the [`local-exec` provisioner](https://www.terraform.io/docs/provisioners/local-exec.html). + +-> In many cases, this resource should be considered a workaround for issues that should be reported and handled in downstream Terraform Provider logic. Downstream resources can usually introduce or adjust retries in their code to handle time delay issues for all Terraform configurations or upstream resources can be improved to better wait for a resource to be fully ready and available. + +## Example Usage + +### Delay Create Usage + +{{tffile "$WORK/examples/resources/sleep/example_1.tf"}} + +### Delay Destroy Usage + +{{tffile "$WORK/examples/resources/sleep/example_2.tf"}} + +### Triggers Usage + +{{tffile "$WORK/examples/resources/sleep/example_3.tf"}} + +## Argument Reference + +The following arguments are optional: + +* `create_duration` - (Optional) [Time duration](https://golang.org/pkg/time/#ParseDuration) to delay resource creation. For example, `30s` for 30 seconds or `5m` for 5 minutes. Updating this value by itself will not trigger a delay. +* `destroy_duration` - (Optional) [Time duration](https://golang.org/pkg/time/#ParseDuration) to delay resource destroy. For example, `30s` for 30 seconds or `5m` for 5 minutes. Updating this value by itself will not trigger a delay. This value or any updates to it must be successfully applied into the Terraform state before destroying this resource to take effect. +* `triggers` - (Optional) Arbitrary map of values that, when changed, will run any creation or destroy delays again. See [the main provider documentation](../index.html) for more information. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - UTC RFC3339 timestamp of the creation or import, e.g. `2020-02-12T06:36:13Z`. + +## Import + +This resource can be imported with the `create_duration` and `destroy_duration`, separated by a comma (`,`). + +e.g. For 30 seconds create duration with no destroy duration: + +{{codefile "shell" "$WORK/examples/resources/sleep/import_1.sh"}} + +e.g. For 30 seconds destroy duration with no create duration: + +{{codefile "shell" "$WORK/examples/resources/sleep/import_2.sh"}} + +The `triggers` argument cannot be imported. +-- exp-templates/resources/static.md.tmpl -- +--- +layout: "time" +page_title: "Time: time_static" +description: |- + Manages a static time resource. +--- +# Resource: time_static + +Manages a static time resource, which keeps a locally sourced UTC timestamp stored in the Terraform state. This prevents perpetual differences caused by using the [`timestamp()` function](https://www.terraform.io/docs/configuration/functions/timestamp.html). + +-> Further manipulation of incoming or outgoing values can be accomplished with the [`formatdate()` function](https://www.terraform.io/docs/configuration/functions/formatdate.html) and the [`timeadd()` function](https://www.terraform.io/docs/configuration/functions/timeadd.html). + +## Example Usage + +### Basic Usage + +{{tffile "$WORK/examples/resources/static/example_1.tf"}} + +### Triggers Usage + +{{tffile "$WORK/examples/resources/static/example_2.tf"}} + +## Argument Reference + +The following arguments are optional: + +* `triggers` - (Optional) Arbitrary map of values that, when changed, will trigger a new base timestamp value to be saved. See [the main provider documentation](../index.html) for more information. +* `rfc3339` - (Optional) Configure the base timestamp with an UTC [RFC3339 time string](https://tools.ietf.org/html/rfc3339#section-5.8) (`YYYY-MM-DDTHH:MM:SSZ`). Defaults to the current time. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `day` - Number day of timestamp. +* `hour` - Number hour of timestamp. +* `id` - UTC RFC3339 timestamp format, e.g. `2020-02-12T06:36:13Z`. +* `minute` - Number minute of timestamp. +* `month` - Number month of timestamp. +* `rfc3339` - UTC RFC3339 format of timestamp, e.g. `2020-02-12T06:36:13Z`. +* `second` - Number second of timestamp. +* `unix` - Number of seconds since epoch time, e.g. `1581489373`. +* `year` - Number year of timestamp. + +## Import + +This resource can be imported using the UTC RFC3339 value, e.g. + +{{codefile "shell" "$WORK/examples/resources/static/import_1.sh"}} + +The `triggers` argument cannot be imported. \ No newline at end of file diff --git a/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success.txtar b/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_legacy_website.txtar similarity index 99% rename from cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success.txtar rename to cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_legacy_website.txtar index 97d6d804..c8d86a42 100644 --- a/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success.txtar +++ b/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_legacy_website.txtar @@ -1,7 +1,7 @@ # Copyright (c) HashiCorp, Inc. # SPDX-License-Identifier: MPL-2.0 -# Successful run of tfplugindocs -migrate on the Time provider +# Successful run of tfplugindocs -migrate on the Time provider using the legacy website layout [!unix] skip # Run migrate command diff --git a/internal/cmd/migrate.go b/internal/cmd/migrate.go index 44218ac3..4af64c0a 100644 --- a/internal/cmd/migrate.go +++ b/internal/cmd/migrate.go @@ -21,7 +21,7 @@ type migrateCmd struct { } func (cmd *migrateCmd) Synopsis() string { - return "migrates website files from old directory structure (website/docs/r) to tfplugindocs supported structure (templates/)" + return "migrates website files from either the legacy rendered website directory (`website/docs/r`) or the docs rendered website directory (`docs/resources`) to the tfplugindocs supported structure (`templates/`)." } func (cmd *migrateCmd) Help() string { @@ -66,7 +66,6 @@ func (cmd *migrateCmd) Flags() *flag.FlagSet { fs := flag.NewFlagSet("migrate", flag.ExitOnError) 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.flagOldWebsiteSourceDir, "old-website-source-dir", "website", "old website directory based on provider-dir; files will be migrated from this directory") fs.StringVar(&cmd.flagTemplatesDir, "templates-dir", "templates", "new website templates directory based on provider-dir; files will be migrated to this directory") fs.StringVar(&cmd.flagExamplesDir, "examples-dir", "examples", "examples directory based on provider-dir") return fs @@ -87,7 +86,6 @@ func (cmd *migrateCmd) runInternal() error { err := provider.Migrate( cmd.ui, cmd.flagProviderDir, - cmd.flagOldWebsiteSourceDir, cmd.flagTemplatesDir, cmd.flagExamplesDir, ) diff --git a/internal/provider/migrate.go b/internal/provider/migrate.go index 4a867211..3dc142cd 100644 --- a/internal/provider/migrate.go +++ b/internal/provider/migrate.go @@ -26,9 +26,9 @@ type migrator struct { // providerDir is the absolute path to the root provider directory providerDir string - oldWebsiteDir string - templatesDir string - examplesDir string + websiteDir string + templatesDir string + examplesDir string ui cli.Ui } @@ -41,7 +41,7 @@ func (m *migrator) warnf(format string, a ...interface{}) { m.ui.Warn(fmt.Sprintf(format, a...)) } -func Migrate(ui cli.Ui, providerDir string, oldWebsiteDir string, templatesDir string, examplesDir string) error { +func Migrate(ui cli.Ui, providerDir string, templatesDir string, examplesDir string) error { // Ensure provider directory is resolved absolute path if providerDir == "" { wd, err := os.Getwd() @@ -72,31 +72,36 @@ func Migrate(ui cli.Ui, providerDir string, oldWebsiteDir string, templatesDir s return fmt.Errorf("expected %q to be a directory", providerDir) } - m := &migrator{ - providerDir: providerDir, - oldWebsiteDir: oldWebsiteDir, - templatesDir: templatesDir, - examplesDir: examplesDir, + // Determine website directory + websiteDir, err := determineWebsiteDir(providerDir) + if err != nil { + return err + } - ui: ui, + m := &migrator{ + providerDir: providerDir, + templatesDir: templatesDir, + examplesDir: examplesDir, + websiteDir: websiteDir, + ui: ui, } return m.Migrate() } func (m *migrator) Migrate() error { - m.infof("migrating website from %q to %q", m.OldProviderWebsiteDir(), m.ProviderTemplatesDir()) + m.infof("migrating website from %q to %q", m.ProviderWebsiteDir(), m.ProviderTemplatesDir()) - err := filepath.Walk(m.OldProviderWebsiteDir(), func(path string, info os.FileInfo, _ error) error { + err := filepath.Walk(m.ProviderWebsiteDir(), func(path string, info os.FileInfo, _ error) error { if info.IsDir() { // skip directories return nil } - rel, err := filepath.Rel(filepath.Join(m.OldProviderWebsiteDir()), path) + rel, err := filepath.Rel(filepath.Join(m.ProviderWebsiteDir()), path) if err != nil { return fmt.Errorf("unable to retrieve the relative path of basepath %q and targetpath %q: %w", - filepath.Join(m.OldProviderWebsiteDir()), path, err) + filepath.Join(m.ProviderWebsiteDir()), path, err) } relDir, relFile := filepath.Split(rel) @@ -108,7 +113,7 @@ func (m *migrator) Migrate() error { } switch relDir { - case "docs/d/": //data-sources + case "docs/d/", "data-sources/": //data-sources datasourceName := strings.TrimSuffix(relFile, ".html.markdown") m.infof("migrating data source %q", datasourceName) @@ -120,7 +125,7 @@ func (m *migrator) Migrate() error { return fmt.Errorf("unable to migrate template %q: %w", rel, err) } - case "docs/r/": //resources + case "docs/r/", "resources/": //resources resourceName := strings.TrimSuffix(relFile, ".html.markdown") m.infof("migrating resource %q", resourceName) @@ -141,10 +146,18 @@ func (m *migrator) Migrate() error { } } default: - m.infof("copying non-template file %q", rel) - err := cp(path, filepath.Join(m.ProviderTemplatesDir(), relFile)) - if err != nil { - return fmt.Errorf("unable to copy file %q: %w", rel, err) + if relFile == "index.html.markdown" { + m.infof("migrating provider index") + err := m.MigrateTemplate(data, "", "", "index.md.tmpl") + if err != nil { + return fmt.Errorf("unable to migrate template %q: %w", rel, err) + } + } else { + m.infof("copying non-template file %q", rel) + err := cp(path, filepath.Join(m.ProviderTemplatesDir(), relFile)) + if err != nil { + return fmt.Errorf("unable to copy file %q: %w", rel, err) + } } } if err != nil { @@ -311,10 +324,10 @@ func (m *migrator) ExtractCodeExamples(content []byte, newRelDir string, templat return nil } -// OldProviderWebsiteDir returns the absolute path to the joined provider and -// given old website directory, which defaults to "website". -func (m *migrator) OldProviderWebsiteDir() string { - return filepath.Join(m.providerDir, m.oldWebsiteDir) +// ProviderWebsiteDir returns the absolute path to the joined provider and +// the website directory that templates will be migrated from, which defaults to either "website" or "docs". +func (m *migrator) ProviderWebsiteDir() string { + return filepath.Join(m.providerDir, m.websiteDir) } // ProviderTemplatesDir returns the absolute path to the joined provider and @@ -328,3 +341,32 @@ func (m *migrator) ProviderTemplatesDir() string { func (m *migrator) ProviderExamplesDir() string { return filepath.Join(m.providerDir, m.examplesDir) } + +func determineWebsiteDir(providerDir string) (string, error) { + // Check for legacy website directory + providerWebsiteDirFileInfo, err := os.Stat(filepath.Join(providerDir, "website")) + + if err != nil { + if os.IsNotExist(err) { + // Legacy website directory does not exist, check for docs directory + } else { + return "", fmt.Errorf("error getting information for provider website directory %q: %w", providerDir, err) + } + } else if providerWebsiteDirFileInfo.IsDir() { + return "website", nil + } + + // Check for docs directory + providerDocsDirFileInfo, err := os.Stat(filepath.Join(providerDir, "docs")) + + if err != nil { + return "", fmt.Errorf("error getting information for provider docs directory %q: %w", providerDir, err) + } + + if providerDocsDirFileInfo.IsDir() { + return "docs", nil + } + + return "", fmt.Errorf("unable to determine website directory for provider %q", providerDir) + +} From ae8342852c10e9bdcc7dbb4d22813bd33a48b6c5 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Mon, 8 Jan 2024 17:21:48 -0500 Subject: [PATCH 09/17] Support converting files with any file extension to `.md.tmpl` --- README.md | 4 +++- internal/provider/migrate.go | 10 ++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 78b3b23e..c2a81a1a 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,9 @@ Docs website directory structure: | `docs/data-sources/.html.markdown` | Data source page | | `docs/resources/.html.markdown` | Resource page | -Files in these directories will be migrated to the `templates/` directory. +Files named `index` (before the first `.`) and files in the `website/docs/d/`, `website/docs/r/`, `docs/data-sources/`, +and `docs/resources/` subdirectories will be converted to `tfplugindocs` templates. All other files in the conventional +paths will be copied to the `--templates-dir` folder. ### Templates diff --git a/internal/provider/migrate.go b/internal/provider/migrate.go index 3dc142cd..6c0c7c3a 100644 --- a/internal/provider/migrate.go +++ b/internal/provider/migrate.go @@ -114,7 +114,7 @@ func (m *migrator) Migrate() error { switch relDir { case "docs/d/", "data-sources/": //data-sources - datasourceName := strings.TrimSuffix(relFile, ".html.markdown") + datasourceName, _, _ := strings.Cut(relFile, ".") m.infof("migrating data source %q", datasourceName) exampleRelDir := filepath.Join("data-sources", datasourceName) @@ -126,7 +126,7 @@ func (m *migrator) Migrate() error { } case "docs/r/", "resources/": //resources - resourceName := strings.TrimSuffix(relFile, ".html.markdown") + resourceName, _, _ := strings.Cut(relFile, ".") m.infof("migrating resource %q", resourceName) exampleRelDir := filepath.Join("resources", resourceName) @@ -138,7 +138,8 @@ func (m *migrator) Migrate() error { } case "docs/": // provider - if relFile == "index.html.markdown" { + fileName, _, _ := strings.Cut(relFile, ".") + if fileName == "index" { m.infof("migrating provider index") err := m.MigrateTemplate(data, "", "", "index.md.tmpl") if err != nil { @@ -146,7 +147,8 @@ func (m *migrator) Migrate() error { } } default: - if relFile == "index.html.markdown" { + fileName, _, _ := strings.Cut(relFile, ".") + if fileName == "index" { m.infof("migrating provider index") err := m.MigrateTemplate(data, "", "", "index.md.tmpl") if err != nil { From 32170f385597d6c1c23240bbfa7a6bb7fad3211f Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 9 Jan 2024 18:20:07 -0500 Subject: [PATCH 10/17] Refactor `Migrate()` method --- README.md | 35 ++-- .../time_provider_success_docs_website.txtar | 22 +-- ...time_provider_success_legacy_website.txtar | 62 ++----- internal/provider/migrate.go | 164 +++++++++--------- 4 files changed, 125 insertions(+), 158 deletions(-) diff --git a/README.md b/README.md index c2a81a1a..d60df7b5 100644 --- a/README.md +++ b/README.md @@ -164,25 +164,30 @@ The `migrate` subcommand assumes the following conventional paths for the render Legacy website directory structure: -| Path | Description | -|---------------------------------------------------|----------------------| -| `website/` | Root of website docs | -| `website/docs/index.html.markdown` | Docs index page | -| `website/docs/d/.html.markdown` | Data source page | -| `website/docs/r/.html.markdown` | Resource page | +| Path | Description | +|---------------------------------------------------|-----------------------------| +| `website/` | Root of website docs | +| `website/docs/guides` | Root of guides subdirectory | +| `website/docs/index.html.markdown` | Docs index page | +| `website/docs/d/.html.markdown` | Data source page | +| `website/docs/r/.html.markdown` | Resource page | Docs website directory structure: -| Path | Description | -|------------------------------------------------------|----------------------| -| `docs/` | Root of website docs | -| `docs/index.html.markdown` | Docs index page | -| `docs/data-sources/.html.markdown` | Data source page | -| `docs/resources/.html.markdown` | Resource page | +| Path | Description | +|------------------------------------------------------|-----------------------------| +| `docs/` | Root of website docs | +| `docs/guides` | Root of guides subdirectory | +| `docs/index.html.markdown` | Docs index page | +| `docs/data-sources/.html.markdown` | Data source page | +| `docs/resources/.html.markdown` | Resource page | -Files named `index` (before the first `.`) and files in the `website/docs/d/`, `website/docs/r/`, `docs/data-sources/`, -and `docs/resources/` subdirectories will be converted to `tfplugindocs` templates. All other files in the conventional -paths will be copied to the `--templates-dir` folder. +Files named `index` (before the first `.`) in the website docs root directory and files in the `website/docs/d/`, `website/docs/r/`, `docs/data-sources/`, +and `docs/resources/` subdirectories will be converted to `tfplugindocs` templates. + +The `website/docs/guides/` and `docs/guides/` subdirectories will be copied as-is to the `--templates-dir` folder. + +All other files in the conventional paths will be ignored. ### Templates diff --git a/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_docs_website.txtar b/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_docs_website.txtar index 84084ac8..dd98d63b 100644 --- a/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_docs_website.txtar +++ b/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_docs_website.txtar @@ -38,37 +38,39 @@ cmpenv examples/resources/static/import_1.sh examples/resources/static/import_1. -- expected-output.txt -- migrating website from "$WORK/docs" to "$WORK/templates" -migrating provider index +migrating provider index: index.html.markdown +migrating file "index.html.markdown" extracting YAML frontmatter to "$WORK/templates/index.md.tmpl" -extracting code examples from "index.md.tmpl" +extracting code examples from "index.html.markdown" creating example file "$WORK/examples/example_1.tf" finished creating template "$WORK/templates/index.md.tmpl" -migrating resource "offset" +migrating resources directory: resources +migrating file "offset.html.markdown" extracting YAML frontmatter to "$WORK/templates/resources/offset.md.tmpl" -extracting code examples from "offset.md.tmpl" +extracting code examples from "offset.html.markdown" creating example file "$WORK/examples/resources/offset/example_1.tf" creating example file "$WORK/examples/resources/offset/example_2.tf" creating import file "$WORK/examples/resources/offset/import_1.sh" finished creating template "$WORK/templates/resources/offset.md.tmpl" -migrating resource "rotating" +migrating file "rotating.html.markdown" extracting YAML frontmatter to "$WORK/templates/resources/rotating.md.tmpl" -extracting code examples from "rotating.md.tmpl" +extracting code examples from "rotating.html.markdown" creating example file "$WORK/examples/resources/rotating/example_1.tf" creating import file "$WORK/examples/resources/rotating/import_1.sh" creating import file "$WORK/examples/resources/rotating/import_2.sh" finished creating template "$WORK/templates/resources/rotating.md.tmpl" -migrating resource "sleep" +migrating file "sleep.html.markdown" extracting YAML frontmatter to "$WORK/templates/resources/sleep.md.tmpl" -extracting code examples from "sleep.md.tmpl" +extracting code examples from "sleep.html.markdown" creating example file "$WORK/examples/resources/sleep/example_1.tf" creating example file "$WORK/examples/resources/sleep/example_2.tf" creating example file "$WORK/examples/resources/sleep/example_3.tf" creating import file "$WORK/examples/resources/sleep/import_1.sh" creating import file "$WORK/examples/resources/sleep/import_2.sh" finished creating template "$WORK/templates/resources/sleep.md.tmpl" -migrating resource "static" +migrating file "static.html.markdown" extracting YAML frontmatter to "$WORK/templates/resources/static.md.tmpl" -extracting code examples from "static.md.tmpl" +extracting code examples from "static.html.markdown" creating example file "$WORK/examples/resources/static/example_1.tf" creating example file "$WORK/examples/resources/static/example_2.tf" creating import file "$WORK/examples/resources/static/import_1.sh" diff --git a/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_legacy_website.txtar b/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_legacy_website.txtar index c8d86a42..2d23a122 100644 --- a/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_legacy_website.txtar +++ b/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_legacy_website.txtar @@ -15,9 +15,6 @@ cmpenv templates/resources/rotating.md.tmpl exp-templates/resources/rotating.md. cmpenv templates/resources/sleep.md.tmpl exp-templates/resources/sleep.md.tmpl cmpenv templates/resources/static.md.tmpl exp-templates/resources/static.md.tmpl -# Check copied non-template files -cmpenv website/time.erb templates/time.erb - # Check generated example files cmpenv examples/example_1.tf examples/example_1.tf @@ -40,43 +37,44 @@ cmpenv examples/resources/static/example_2.tf examples/resources/static/example_ cmpenv examples/resources/static/import_1.sh examples/resources/static/import_1.sh -- expected-output.txt -- -migrating website from "$WORK/website" to "$WORK/templates" -migrating provider index +migrating website from "$WORK/website/docs" to "$WORK/templates" +migrating provider index: index.html.markdown +migrating file "index.html.markdown" extracting YAML frontmatter to "$WORK/templates/index.md.tmpl" -extracting code examples from "index.md.tmpl" +extracting code examples from "index.html.markdown" creating example file "$WORK/examples/example_1.tf" finished creating template "$WORK/templates/index.md.tmpl" -migrating resource "offset" +migrating resources directory: r +migrating file "offset.html.markdown" extracting YAML frontmatter to "$WORK/templates/resources/offset.md.tmpl" -extracting code examples from "offset.md.tmpl" +extracting code examples from "offset.html.markdown" creating example file "$WORK/examples/resources/offset/example_1.tf" creating example file "$WORK/examples/resources/offset/example_2.tf" creating import file "$WORK/examples/resources/offset/import_1.sh" finished creating template "$WORK/templates/resources/offset.md.tmpl" -migrating resource "rotating" +migrating file "rotating.html.markdown" extracting YAML frontmatter to "$WORK/templates/resources/rotating.md.tmpl" -extracting code examples from "rotating.md.tmpl" +extracting code examples from "rotating.html.markdown" creating example file "$WORK/examples/resources/rotating/example_1.tf" creating import file "$WORK/examples/resources/rotating/import_1.sh" creating import file "$WORK/examples/resources/rotating/import_2.sh" finished creating template "$WORK/templates/resources/rotating.md.tmpl" -migrating resource "sleep" +migrating file "sleep.html.markdown" extracting YAML frontmatter to "$WORK/templates/resources/sleep.md.tmpl" -extracting code examples from "sleep.md.tmpl" +extracting code examples from "sleep.html.markdown" creating example file "$WORK/examples/resources/sleep/example_1.tf" creating example file "$WORK/examples/resources/sleep/example_2.tf" creating example file "$WORK/examples/resources/sleep/example_3.tf" creating import file "$WORK/examples/resources/sleep/import_1.sh" creating import file "$WORK/examples/resources/sleep/import_2.sh" finished creating template "$WORK/templates/resources/sleep.md.tmpl" -migrating resource "static" +migrating file "static.html.markdown" extracting YAML frontmatter to "$WORK/templates/resources/static.md.tmpl" -extracting code examples from "static.md.tmpl" +extracting code examples from "static.html.markdown" creating example file "$WORK/examples/resources/static/example_1.tf" creating example file "$WORK/examples/resources/static/example_2.tf" creating import file "$WORK/examples/resources/static/import_1.sh" finished creating template "$WORK/templates/resources/static.md.tmpl" -copying non-template file "time.erb" -- website/docs/index.html.markdown -- --- layout: "time" @@ -478,40 +476,6 @@ $ terraform import time_static.example 2020-02-12T06:36:13Z ``` The `triggers` argument cannot be imported. --- website/time.erb -- -<% wrap_layout :inner do %> - <% content_for :sidebar do %> - - <% end %> - <%= yield %> -<% end %> -- exp-examples/example_1.tf -- resource "time_static" "ami_update" { diff --git a/internal/provider/migrate.go b/internal/provider/migrate.go index 6c0c7c3a..b1dcdf59 100644 --- a/internal/provider/migrate.go +++ b/internal/provider/migrate.go @@ -7,8 +7,10 @@ import ( "bufio" "bytes" "fmt" + "io/fs" "os" "path/filepath" + "regexp" "strconv" "strings" @@ -20,6 +22,7 @@ import ( var ( exampleImportCodeTemplate = "{{codefile \"shell\" \"%s\"}}" exampleTFCodeTemplate = "{{tffile \"%s\"}}" + indexFileNameRegex = regexp.MustCompile(`index.*`) ) type migrator struct { @@ -92,79 +95,47 @@ func Migrate(ui cli.Ui, providerDir string, templatesDir string, examplesDir str func (m *migrator) Migrate() error { m.infof("migrating website from %q to %q", m.ProviderWebsiteDir(), m.ProviderTemplatesDir()) - err := filepath.Walk(m.ProviderWebsiteDir(), func(path string, info os.FileInfo, _ error) error { - if info.IsDir() { - // skip directories - return nil - } - - rel, err := filepath.Rel(filepath.Join(m.ProviderWebsiteDir()), path) - if err != nil { - return fmt.Errorf("unable to retrieve the relative path of basepath %q and targetpath %q: %w", - filepath.Join(m.ProviderWebsiteDir()), path, err) - } - - relDir, relFile := filepath.Split(rel) - relDir = filepath.ToSlash(relDir) - - data, err := os.ReadFile(path) - if err != nil { - return fmt.Errorf("unable to read file %q: %w", rel, err) - } - - switch relDir { - case "docs/d/", "data-sources/": //data-sources - datasourceName, _, _ := strings.Cut(relFile, ".") - m.infof("migrating data source %q", datasourceName) - - exampleRelDir := filepath.Join("data-sources", datasourceName) - templateRelDir := "data-sources" - fileName := datasourceName + ".md.tmpl" - err := m.MigrateTemplate(data, templateRelDir, exampleRelDir, fileName) - if err != nil { - return fmt.Errorf("unable to migrate template %q: %w", rel, err) - } - - case "docs/r/", "resources/": //resources - resourceName, _, _ := strings.Cut(relFile, ".") - m.infof("migrating resource %q", resourceName) - - exampleRelDir := filepath.Join("resources", resourceName) - templateRelDir := "resources" - fileName := resourceName + ".md.tmpl" - err := m.MigrateTemplate(data, templateRelDir, exampleRelDir, fileName) - if err != nil { - return fmt.Errorf("unable to migrate template %q: %w", rel, err) - } - - case "docs/": // provider - fileName, _, _ := strings.Cut(relFile, ".") - if fileName == "index" { - m.infof("migrating provider index") - err := m.MigrateTemplate(data, "", "", "index.md.tmpl") + err := filepath.WalkDir(m.ProviderWebsiteDir(), func(path string, d os.DirEntry, _ error) error { + switch d.IsDir() { + case true: //directories + switch d.Name() { + case "d", "data-sources": //data-sources + m.infof("migrating data-sources directory: %s", d.Name()) + err := filepath.WalkDir(path, m.MigrateTemplate("data-sources")) if err != nil { - return fmt.Errorf("unable to migrate template %q: %w", rel, err) + return err } - } - default: - fileName, _, _ := strings.Cut(relFile, ".") - if fileName == "index" { - m.infof("migrating provider index") - err := m.MigrateTemplate(data, "", "", "index.md.tmpl") + return filepath.SkipDir + case "r", "resources": //resources + m.infof("migrating resources directory: %s", d.Name()) + err := filepath.WalkDir(path, m.MigrateTemplate("resources")) if err != nil { - return fmt.Errorf("unable to migrate template %q: %w", rel, err) + return err } - } else { - m.infof("copying non-template file %q", rel) - err := cp(path, filepath.Join(m.ProviderTemplatesDir(), relFile)) + return filepath.SkipDir + case "guides": + m.infof("copying guides directory: %s", d.Name()) + err := cp(path, filepath.Join(m.ProviderTemplatesDir(), "guides")) if err != nil { - return fmt.Errorf("unable to copy file %q: %w", rel, err) + return fmt.Errorf("unable to copy guides directory %q: %w", path, err) } + return filepath.SkipDir + } + case false: //files + switch { + case indexFileNameRegex.MatchString(d.Name()): //index file + m.infof("migrating provider index: %s", d.Name()) + err := filepath.WalkDir(path, m.MigrateTemplate("")) + if err != nil { + return err + } + return nil + default: + //skip non-index files + return nil } } - if err != nil { - return fmt.Errorf("unable to migrate template %q: %w", rel, err) - } + return nil }) if err != nil { @@ -174,24 +145,49 @@ func (m *migrator) Migrate() error { return nil } -func (m *migrator) MigrateTemplate(data []byte, templateRelDir string, exampleRelDir, filename string) error { - templateFilePath := filepath.Join(m.ProviderTemplatesDir(), templateRelDir, filename) +func (m *migrator) MigrateTemplate(relDir string) fs.WalkDirFunc { + return func(path string, d fs.DirEntry, err error) error { + if d.IsDir() { + //skip processing directories + return nil + } - err := os.MkdirAll(filepath.Dir(templateFilePath), 0755) - if err != nil { - return fmt.Errorf("unable to create directory %q: %w", templateFilePath, err) - } - m.infof("extracting YAML frontmatter to %q", templateFilePath) - err = m.ExtractFrontMatter(data, templateFilePath) - if err != nil { - return fmt.Errorf("unable to extract front matter to %q: %w", templateFilePath, err) - } - m.infof("extracting code examples from %q", filename) - err = m.ExtractCodeExamples(data, exampleRelDir, templateFilePath) - if err != nil { - return fmt.Errorf("unable to extract code examples from %q: %w", templateFilePath, err) + m.infof("migrating file %q", d.Name()) + data, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("unable to read file %q: %w", d.Name(), err) + } + + baseName, _, _ := strings.Cut(d.Name(), ".") + + var exampleRelDir string + if baseName == "index" { + exampleRelDir = relDir + } else { + exampleRelDir = filepath.Join(relDir, baseName) + } + templateFilePath := filepath.Join(m.ProviderTemplatesDir(), relDir, baseName+".md.tmpl") + + err = os.MkdirAll(filepath.Dir(templateFilePath), 0755) + if err != nil { + return fmt.Errorf("unable to create directory %q: %w", templateFilePath, err) + } + + m.infof("extracting YAML frontmatter to %q", templateFilePath) + err = m.ExtractFrontMatter(data, templateFilePath) + if err != nil { + return fmt.Errorf("unable to extract front matter to %q: %w", templateFilePath, err) + } + + m.infof("extracting code examples from %q", d.Name()) + err = m.ExtractCodeExamples(data, exampleRelDir, templateFilePath) + if err != nil { + return fmt.Errorf("unable to extract code examples from %q: %w", templateFilePath, err) + } + + return nil } - return nil + } func (m *migrator) ExtractFrontMatter(content []byte, templateFile string) error { @@ -327,7 +323,7 @@ func (m *migrator) ExtractCodeExamples(content []byte, newRelDir string, templat } // ProviderWebsiteDir returns the absolute path to the joined provider and -// the website directory that templates will be migrated from, which defaults to either "website" or "docs". +// the website directory that templates will be migrated from, which defaults to either "website/docs/" or "docs". func (m *migrator) ProviderWebsiteDir() string { return filepath.Join(m.providerDir, m.websiteDir) } @@ -346,7 +342,7 @@ func (m *migrator) ProviderExamplesDir() string { func determineWebsiteDir(providerDir string) (string, error) { // Check for legacy website directory - providerWebsiteDirFileInfo, err := os.Stat(filepath.Join(providerDir, "website")) + providerWebsiteDirFileInfo, err := os.Stat(filepath.Join(providerDir, "website/docs")) if err != nil { if os.IsNotExist(err) { @@ -355,7 +351,7 @@ func determineWebsiteDir(providerDir string) (string, error) { return "", fmt.Errorf("error getting information for provider website directory %q: %w", providerDir, err) } } else if providerWebsiteDirFileInfo.IsDir() { - return "website", nil + return "website/docs", nil } // Check for docs directory From fcc2b0bbb594a5815cb99f0ebda6c9d8d658ee55 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 9 Jan 2024 18:27:54 -0500 Subject: [PATCH 11/17] Skip `layout` front matter --- .../migrate/time_provider_success_docs_website.txtar | 5 ----- .../migrate/time_provider_success_legacy_website.txtar | 5 ----- internal/provider/migrate.go | 4 ++++ 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_docs_website.txtar b/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_docs_website.txtar index dd98d63b..b3e27200 100644 --- a/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_docs_website.txtar +++ b/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_docs_website.txtar @@ -620,7 +620,6 @@ resource "aws_instance" "server" { $ terraform import time_static.example 2020-02-12T06:36:13Z -- exp-templates/index.md.tmpl -- --- -layout: "time" page_title: "Provider: Time" description: |- The time provider is used to interact with time-based resources. @@ -650,7 +649,6 @@ For example: To force a these actions to reoccur without updating `triggers`, the [`terraform taint` command](https://www.terraform.io/docs/commands/taint.html) can be used to produce the action on the next run. -- exp-templates/resources/offset.md.tmpl -- --- -layout: "time" page_title: "Time: time_offset" description: |- Manages a offset time resource. @@ -709,7 +707,6 @@ This resource can be imported using the base UTC RFC3339 timestamp and offset ye The `triggers` argument cannot be imported. -- exp-templates/resources/rotating.md.tmpl -- --- -layout: "time" page_title: "Time: time_rotating" description: |- Manages a rotating time resource. @@ -767,7 +764,6 @@ Otherwise, to import with the rotation RFC3339 value, the base UTC RFC3339 value The `triggers` argument cannot be imported. -- exp-templates/resources/sleep.md.tmpl -- --- -layout: "time" page_title: "Time: time_sleep" description: |- Manages a static time resource. @@ -821,7 +817,6 @@ e.g. For 30 seconds destroy duration with no create duration: The `triggers` argument cannot be imported. -- exp-templates/resources/static.md.tmpl -- --- -layout: "time" page_title: "Time: time_static" description: |- Manages a static time resource. diff --git a/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_legacy_website.txtar b/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_legacy_website.txtar index 2d23a122..e0108c41 100644 --- a/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_legacy_website.txtar +++ b/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_legacy_website.txtar @@ -621,7 +621,6 @@ resource "aws_instance" "server" { $ terraform import time_static.example 2020-02-12T06:36:13Z -- exp-templates/index.md.tmpl -- --- -layout: "time" page_title: "Provider: Time" description: |- The time provider is used to interact with time-based resources. @@ -651,7 +650,6 @@ For example: To force a these actions to reoccur without updating `triggers`, the [`terraform taint` command](https://www.terraform.io/docs/commands/taint.html) can be used to produce the action on the next run. -- exp-templates/resources/offset.md.tmpl -- --- -layout: "time" page_title: "Time: time_offset" description: |- Manages a offset time resource. @@ -710,7 +708,6 @@ This resource can be imported using the base UTC RFC3339 timestamp and offset ye The `triggers` argument cannot be imported. -- exp-templates/resources/rotating.md.tmpl -- --- -layout: "time" page_title: "Time: time_rotating" description: |- Manages a rotating time resource. @@ -768,7 +765,6 @@ Otherwise, to import with the rotation RFC3339 value, the base UTC RFC3339 value The `triggers` argument cannot be imported. -- exp-templates/resources/sleep.md.tmpl -- --- -layout: "time" page_title: "Time: time_sleep" description: |- Manages a static time resource. @@ -822,7 +818,6 @@ e.g. For 30 seconds destroy duration with no create duration: The `triggers` argument cannot be imported. -- exp-templates/resources/static.md.tmpl -- --- -layout: "time" page_title: "Time: time_static" description: |- Manages a static time resource. diff --git a/internal/provider/migrate.go b/internal/provider/migrate.go index b1dcdf59..620ea801 100644 --- a/internal/provider/migrate.go +++ b/internal/provider/migrate.go @@ -205,6 +205,10 @@ func (m *migrator) ExtractFrontMatter(content []byte, templateFile string) error } exited := false for fileScanner.Scan() { + if strings.Contains(fileScanner.Text(), "layout:") { + // skip layout front matter + continue + } err = appendFile(templateFile, []byte(fileScanner.Text()+"\n")) if err != nil { return fmt.Errorf("unable to append frontmatter to %q: %w", templateFile, err) From c608fd9b12f7decb20e5c1ee20e43b952676c23d Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 9 Jan 2024 18:35:36 -0500 Subject: [PATCH 12/17] Fix linting errors --- internal/cmd/migrate.go | 7 +++---- internal/provider/migrate.go | 3 +++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/internal/cmd/migrate.go b/internal/cmd/migrate.go index 4af64c0a..1c5eacc5 100644 --- a/internal/cmd/migrate.go +++ b/internal/cmd/migrate.go @@ -14,10 +14,9 @@ import ( type migrateCmd struct { commonCmd - flagProviderDir string - flagOldWebsiteSourceDir string - flagTemplatesDir string - flagExamplesDir string + flagProviderDir string + flagTemplatesDir string + flagExamplesDir string } func (cmd *migrateCmd) Synopsis() string { diff --git a/internal/provider/migrate.go b/internal/provider/migrate.go index 620ea801..eb0f1d74 100644 --- a/internal/provider/migrate.go +++ b/internal/provider/migrate.go @@ -147,6 +147,9 @@ func (m *migrator) Migrate() error { func (m *migrator) MigrateTemplate(relDir string) fs.WalkDirFunc { return func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } if d.IsDir() { //skip processing directories return nil From ecbdd11dad04976f5a76bd3e3e4badd7368f075d Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Wed, 10 Jan 2024 16:26:44 -0500 Subject: [PATCH 13/17] Add comment to generated templates explaining functionality --- .../time_provider_success_docs_website.txtar | 25 +++++++++++++++++++ ...time_provider_success_legacy_website.txtar | 25 +++++++++++++++++++ internal/provider/migrate.go | 3 +++ internal/provider/template.go | 6 +++++ 4 files changed, 59 insertions(+) diff --git a/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_docs_website.txtar b/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_docs_website.txtar index b3e27200..15b8c754 100644 --- a/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_docs_website.txtar +++ b/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_docs_website.txtar @@ -624,6 +624,11 @@ page_title: "Provider: Time" description: |- The time provider is used to interact with time-based resources. --- + +{{/* This template serves as a starting point for documentation generation, and can be customized with hardcoded values and/or doc gen templates. + +For example, the {{ SchemaMarkdown }} template can be used to replace manual schema documentation if descriptions of schema attributes are added in the provider source code. */ -}} + # Time Provider The time provider is used to interact with time-based resources. The provider itself has no configuration options. @@ -653,6 +658,11 @@ page_title: "Time: time_offset" description: |- Manages a offset time resource. --- + +{{/* This template serves as a starting point for documentation generation, and can be customized with hardcoded values and/or doc gen templates. + +For example, the {{ SchemaMarkdown }} template can be used to replace manual schema documentation if descriptions of schema attributes are added in the provider source code. */ -}} + # Resource: time_offset Manages an offset time resource, which keeps an UTC timestamp stored in the Terraform state that is offset from a locally sourced base timestamp. This prevents perpetual differences caused by using the [`timestamp()` function](https://www.terraform.io/docs/configuration/functions/timestamp.html). @@ -711,6 +721,11 @@ page_title: "Time: time_rotating" description: |- Manages a rotating time resource. --- + +{{/* This template serves as a starting point for documentation generation, and can be customized with hardcoded values and/or doc gen templates. + +For example, the {{ SchemaMarkdown }} template can be used to replace manual schema documentation if descriptions of schema attributes are added in the provider source code. */ -}} + # Resource: time_rotating Manages a rotating time resource, which keeps a rotating UTC timestamp stored in the Terraform state and proposes resource recreation when the locally sourced current time is beyond the rotation time. This rotation only occurs when Terraform is executed, meaning there will be drift between the rotation timestamp and actual rotation. The new rotation timestamp offset includes this drift. This prevents perpetual differences caused by using the [`timestamp()` function](https://www.terraform.io/docs/configuration/functions/timestamp.html) by only forcing a new value on the set cadence. @@ -768,6 +783,11 @@ page_title: "Time: time_sleep" description: |- Manages a static time resource. --- + +{{/* This template serves as a starting point for documentation generation, and can be customized with hardcoded values and/or doc gen templates. + +For example, the {{ SchemaMarkdown }} template can be used to replace manual schema documentation if descriptions of schema attributes are added in the provider source code. */ -}} + # Resource: time_sleep Manages a resource that delays creation and/or destruction, typically for further resources. This prevents cross-platform compatibility and destroy-time issues with using the [`local-exec` provisioner](https://www.terraform.io/docs/provisioners/local-exec.html). @@ -821,6 +841,11 @@ page_title: "Time: time_static" description: |- Manages a static time resource. --- + +{{/* This template serves as a starting point for documentation generation, and can be customized with hardcoded values and/or doc gen templates. + +For example, the {{ SchemaMarkdown }} template can be used to replace manual schema documentation if descriptions of schema attributes are added in the provider source code. */ -}} + # Resource: time_static Manages a static time resource, which keeps a locally sourced UTC timestamp stored in the Terraform state. This prevents perpetual differences caused by using the [`timestamp()` function](https://www.terraform.io/docs/configuration/functions/timestamp.html). diff --git a/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_legacy_website.txtar b/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_legacy_website.txtar index e0108c41..3d609955 100644 --- a/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_legacy_website.txtar +++ b/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_legacy_website.txtar @@ -625,6 +625,11 @@ page_title: "Provider: Time" description: |- The time provider is used to interact with time-based resources. --- + +{{/* This template serves as a starting point for documentation generation, and can be customized with hardcoded values and/or doc gen templates. + +For example, the {{ SchemaMarkdown }} template can be used to replace manual schema documentation if descriptions of schema attributes are added in the provider source code. */ -}} + # Time Provider The time provider is used to interact with time-based resources. The provider itself has no configuration options. @@ -654,6 +659,11 @@ page_title: "Time: time_offset" description: |- Manages a offset time resource. --- + +{{/* This template serves as a starting point for documentation generation, and can be customized with hardcoded values and/or doc gen templates. + +For example, the {{ SchemaMarkdown }} template can be used to replace manual schema documentation if descriptions of schema attributes are added in the provider source code. */ -}} + # Resource: time_offset Manages an offset time resource, which keeps an UTC timestamp stored in the Terraform state that is offset from a locally sourced base timestamp. This prevents perpetual differences caused by using the [`timestamp()` function](https://www.terraform.io/docs/configuration/functions/timestamp.html). @@ -712,6 +722,11 @@ page_title: "Time: time_rotating" description: |- Manages a rotating time resource. --- + +{{/* This template serves as a starting point for documentation generation, and can be customized with hardcoded values and/or doc gen templates. + +For example, the {{ SchemaMarkdown }} template can be used to replace manual schema documentation if descriptions of schema attributes are added in the provider source code. */ -}} + # Resource: time_rotating Manages a rotating time resource, which keeps a rotating UTC timestamp stored in the Terraform state and proposes resource recreation when the locally sourced current time is beyond the rotation time. This rotation only occurs when Terraform is executed, meaning there will be drift between the rotation timestamp and actual rotation. The new rotation timestamp offset includes this drift. This prevents perpetual differences caused by using the [`timestamp()` function](https://www.terraform.io/docs/configuration/functions/timestamp.html) by only forcing a new value on the set cadence. @@ -769,6 +784,11 @@ page_title: "Time: time_sleep" description: |- Manages a static time resource. --- + +{{/* This template serves as a starting point for documentation generation, and can be customized with hardcoded values and/or doc gen templates. + +For example, the {{ SchemaMarkdown }} template can be used to replace manual schema documentation if descriptions of schema attributes are added in the provider source code. */ -}} + # Resource: time_sleep Manages a resource that delays creation and/or destruction, typically for further resources. This prevents cross-platform compatibility and destroy-time issues with using the [`local-exec` provisioner](https://www.terraform.io/docs/provisioners/local-exec.html). @@ -822,6 +842,11 @@ page_title: "Time: time_static" description: |- Manages a static time resource. --- + +{{/* This template serves as a starting point for documentation generation, and can be customized with hardcoded values and/or doc gen templates. + +For example, the {{ SchemaMarkdown }} template can be used to replace manual schema documentation if descriptions of schema attributes are added in the provider source code. */ -}} + # Resource: time_static Manages a static time resource, which keeps a locally sourced UTC timestamp stored in the Terraform state. This prevents perpetual differences caused by using the [`timestamp()` function](https://www.terraform.io/docs/configuration/functions/timestamp.html). diff --git a/internal/provider/migrate.go b/internal/provider/migrate.go index eb0f1d74..f1064d36 100644 --- a/internal/provider/migrate.go +++ b/internal/provider/migrate.go @@ -226,6 +226,9 @@ func (m *migrator) ExtractFrontMatter(content []byte, templateFile string) error return fmt.Errorf("cannot find ending of frontmatter block in %q", templateFile) } + // add comment to end of front matter briefly explaining template functionality + err = appendFile(templateFile, []byte(migrateProviderTemplateComment+"\n")) + return nil } diff --git a/internal/provider/template.go b/internal/provider/template.go index f2a24cbc..06c85d48 100644 --- a/internal/provider/template.go +++ b/internal/provider/template.go @@ -249,3 +249,9 @@ description: |- {{ .SchemaMarkdown | trimspace }} ` + +const migrateProviderTemplateComment providerTemplate = ` +{{/* This template serves as a starting point for documentation generation, and can be customized with hardcoded values and/or doc gen templates. + +For example, the {{ SchemaMarkdown }} template can be used to replace manual schema documentation if descriptions of schema attributes are added in the provider source code. */ -}} +` From 02f970ea357662d295475ff04c817325184ca5b3 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Wed, 10 Jan 2024 16:39:16 -0500 Subject: [PATCH 14/17] Remove `website/` directory at the end of migration --- README.md | 4 +++- .../migrate/time_provider_success_legacy_website.txtar | 3 +++ internal/provider/migrate.go | 6 ++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d60df7b5..e5b804a1 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,8 @@ Otherwise, the provider developer can set an arbitrary description like this: The `migrate` subcommand can be used to migrate website files from either the legacy rendered website directory (`website/docs/r`) or the docs rendered website directory (`docs/resources`) to the `tfplugindocs` supported structure (`templates/`). Markdown files in the rendered website -directory will be converted to `tfplugindocs` templates. +directory will be converted to `tfplugindocs` templates. The legacy `website/` directory will be removed after migration to avoid Terraform Registry +ingress issues. The `migrate` subcommand takes the following actions: - Determines the rendered website directory based on the `--provider-dir` argument @@ -127,6 +128,7 @@ The `migrate` subcommand takes the following actions: - Extracts code blocks from website docs to create individual example files in `--examples-dir` (will create this folder if it doesn't exist) - replace extracted example code in website templates with `tfplugindocs` template code referring to example files. - Copies non-template files to `--templates-dir` folder +- Removes the `website/` directory ### Conventional Paths diff --git a/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_legacy_website.txtar b/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_legacy_website.txtar index 3d609955..ec1af20e 100644 --- a/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_legacy_website.txtar +++ b/cmd/tfplugindocs/testdata/scripts/schema-json/migrate/time_provider_success_legacy_website.txtar @@ -36,6 +36,9 @@ cmpenv examples/resources/static/example_1.tf examples/resources/static/example_ cmpenv examples/resources/static/example_2.tf examples/resources/static/example_2.tf cmpenv examples/resources/static/import_1.sh examples/resources/static/import_1.sh +# Verify legacy website directory is removed +! exists website/ + -- expected-output.txt -- migrating website from "$WORK/website/docs" to "$WORK/templates" migrating provider index: index.html.markdown diff --git a/internal/provider/migrate.go b/internal/provider/migrate.go index f1064d36..49092392 100644 --- a/internal/provider/migrate.go +++ b/internal/provider/migrate.go @@ -142,6 +142,12 @@ func (m *migrator) Migrate() error { return fmt.Errorf("unable to migrate website: %w", err) } + //remove legacy website directory + err = os.RemoveAll(filepath.Join(m.providerDir, "website")) + if err != nil { + return fmt.Errorf("unable to remove legacy website directory: %w", err) + } + return nil } From d687f35bfcb27ad089323279f28b1cfd8be47344 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Wed, 10 Jan 2024 17:02:47 -0500 Subject: [PATCH 15/17] Fix `ineffassign` lint error --- internal/provider/migrate.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/provider/migrate.go b/internal/provider/migrate.go index 49092392..211df890 100644 --- a/internal/provider/migrate.go +++ b/internal/provider/migrate.go @@ -234,6 +234,9 @@ func (m *migrator) ExtractFrontMatter(content []byte, templateFile string) error // add comment to end of front matter briefly explaining template functionality err = appendFile(templateFile, []byte(migrateProviderTemplateComment+"\n")) + if err != nil { + return fmt.Errorf("unable to append template comment to %q: %w", templateFile, err) + } return nil } From 84a9af41cdc591faa965453717bb8c6ff27b77c9 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Thu, 11 Jan 2024 12:20:10 -0500 Subject: [PATCH 16/17] Update README.md Co-authored-by: Austin Valle --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e5b804a1..94bde54f 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ ingress issues. The `migrate` subcommand takes the following actions: - Determines the rendered website directory based on the `--provider-dir` argument -- Copies the contents of the rendered website directory to the `--tempates-dir` folder (will create this folder if it doesn't exist) +- Copies the contents of the rendered website directory to the `--templates-dir` folder (will create this folder if it doesn't exist) - (if the rendered website is using legacy format) Renames `docs/d/` and `docs/r/` subdirectories to `data-sources/` and `resources/` respectively - Change file suffixes for Markdown files to `.md.tmpl` to create website templates - Extracts code blocks from website docs to create individual example files in `--examples-dir` (will create this folder if it doesn't exist) From b9d1018948fe9449e1fa97f867ec12745f720ceb Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Thu, 11 Jan 2024 13:08:16 -0500 Subject: [PATCH 17/17] Resolve comments from code review --- README.md | 16 +++++------ internal/cmd/migrate.go | 4 +-- internal/provider/migrate.go | 52 ++++++++++++++++++++--------------- internal/provider/template.go | 2 +- internal/provider/util.go | 25 ----------------- 5 files changed, 41 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 94bde54f..ac96c2e7 100644 --- a/README.md +++ b/README.md @@ -121,14 +121,14 @@ directory will be converted to `tfplugindocs` templates. The legacy `website/` d ingress issues. The `migrate` subcommand takes the following actions: -- Determines the rendered website directory based on the `--provider-dir` argument -- Copies the contents of the rendered website directory to the `--templates-dir` folder (will create this folder if it doesn't exist) -- (if the rendered website is using legacy format) Renames `docs/d/` and `docs/r/` subdirectories to `data-sources/` and `resources/` respectively -- Change file suffixes for Markdown files to `.md.tmpl` to create website templates -- Extracts code blocks from website docs to create individual example files in `--examples-dir` (will create this folder if it doesn't exist) -- replace extracted example code in website templates with `tfplugindocs` template code referring to example files. -- Copies non-template files to `--templates-dir` folder -- Removes the `website/` directory +1. Determines the rendered website directory based on the `--provider-dir` argument +2. Copies the contents of the rendered website directory to the `--templates-dir` folder (will create this folder if it doesn't exist) +3. (if the rendered website is using legacy format) Renames `docs/d/` and `docs/r/` subdirectories to `data-sources/` and `resources/` respectively +4. Change file suffixes for Markdown files to `.md.tmpl` to create website templates +5. Extracts code blocks from website docs to create individual example files in `--examples-dir` (will create this folder if it doesn't exist) +6. Replace extracted example code in website templates with `codefile`/`tffile` template functions referencing the example files. +7. Copies non-template files to `--templates-dir` folder +8. Removes the `website/` directory ### Conventional Paths diff --git a/internal/cmd/migrate.go b/internal/cmd/migrate.go index 1c5eacc5..8248b9ec 100644 --- a/internal/cmd/migrate.go +++ b/internal/cmd/migrate.go @@ -64,9 +64,9 @@ func (cmd *migrateCmd) Help() string { func (cmd *migrateCmd) Flags() *flag.FlagSet { fs := flag.NewFlagSet("migrate", flag.ExitOnError) - 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.flagProviderDir, "provider-dir", "", "relative or absolute path to the root provider code directory; this will default to the current working directory if not set") fs.StringVar(&cmd.flagTemplatesDir, "templates-dir", "templates", "new website templates directory based on provider-dir; files will be migrated to this directory") - fs.StringVar(&cmd.flagExamplesDir, "examples-dir", "examples", "examples directory based on provider-dir") + fs.StringVar(&cmd.flagExamplesDir, "examples-dir", "examples", "examples directory based on provider-dir; extracted code examples will be migrated to this directory") return fs } diff --git a/internal/provider/migrate.go b/internal/provider/migrate.go index 211df890..4e9159d7 100644 --- a/internal/provider/migrate.go +++ b/internal/provider/migrate.go @@ -19,12 +19,6 @@ import ( "github.com/yuin/goldmark/text" ) -var ( - exampleImportCodeTemplate = "{{codefile \"shell\" \"%s\"}}" - exampleTFCodeTemplate = "{{tffile \"%s\"}}" - indexFileNameRegex = regexp.MustCompile(`index.*`) -) - type migrator struct { // providerDir is the absolute path to the root provider directory providerDir string @@ -95,9 +89,12 @@ func Migrate(ui cli.Ui, providerDir string, templatesDir string, examplesDir str func (m *migrator) Migrate() error { m.infof("migrating website from %q to %q", m.ProviderWebsiteDir(), m.ProviderTemplatesDir()) - err := filepath.WalkDir(m.ProviderWebsiteDir(), func(path string, d os.DirEntry, _ error) error { - switch d.IsDir() { - case true: //directories + err := filepath.WalkDir(m.ProviderWebsiteDir(), func(path string, d os.DirEntry, err error) error { + if err != nil { + return fmt.Errorf("unable to walk path %q: %w", path, err) + } + + if d.IsDir() { switch d.Name() { case "d", "data-sources": //data-sources m.infof("migrating data-sources directory: %s", d.Name()) @@ -121,9 +118,9 @@ func (m *migrator) Migrate() error { } return filepath.SkipDir } - case false: //files + } else { switch { - case indexFileNameRegex.MatchString(d.Name()): //index file + case regexp.MustCompile(`index.*`).MatchString(d.Name()): //index file m.infof("migrating provider index: %s", d.Name()) err := filepath.WalkDir(path, m.MigrateTemplate("")) if err != nil { @@ -199,18 +196,29 @@ func (m *migrator) MigrateTemplate(relDir string) fs.WalkDirFunc { } -func (m *migrator) ExtractFrontMatter(content []byte, templateFile string) error { +func (m *migrator) ExtractFrontMatter(content []byte, templateFilePath string) error { + templateFile, err := os.OpenFile(templateFilePath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) + if err != nil { + return fmt.Errorf("unable to open file %q: %w", templateFilePath, err) + } + defer func(f *os.File) { + err := f.Close() + if err != nil { + m.warnf("unable to close file %q: %q", templateFilePath, err) + } + }(templateFile) + fileScanner := bufio.NewScanner(bytes.NewReader(content)) fileScanner.Split(bufio.ScanLines) hasFirstLine := fileScanner.Scan() if !hasFirstLine || fileScanner.Text() != "---" { - m.warnf("no frontmatter found in %q", templateFile) + m.warnf("no frontmatter found in %q", templateFilePath) return nil } - err := appendFile(templateFile, []byte(fileScanner.Text()+"\n")) + _, err = templateFile.WriteString(fileScanner.Text() + "\n") if err != nil { - return fmt.Errorf("unable to append frontmatter to %q: %w", templateFile, err) + return fmt.Errorf("unable to append frontmatter to %q: %w", templateFilePath, err) } exited := false for fileScanner.Scan() { @@ -218,9 +226,9 @@ func (m *migrator) ExtractFrontMatter(content []byte, templateFile string) error // skip layout front matter continue } - err = appendFile(templateFile, []byte(fileScanner.Text()+"\n")) + _, err = templateFile.WriteString(fileScanner.Text() + "\n") if err != nil { - return fmt.Errorf("unable to append frontmatter to %q: %w", templateFile, err) + return fmt.Errorf("unable to append frontmatter to %q: %w", templateFilePath, err) } if fileScanner.Text() == "---" { exited = true @@ -229,13 +237,13 @@ func (m *migrator) ExtractFrontMatter(content []byte, templateFile string) error } if !exited { - return fmt.Errorf("cannot find ending of frontmatter block in %q", templateFile) + return fmt.Errorf("cannot find ending of frontmatter block in %q", templateFilePath) } // add comment to end of front matter briefly explaining template functionality - err = appendFile(templateFile, []byte(migrateProviderTemplateComment+"\n")) + _, err = templateFile.WriteString(migrateProviderTemplateComment + "\n") if err != nil { - return fmt.Errorf("unable to append template comment to %q: %w", templateFile, err) + return fmt.Errorf("unable to append template comment to %q: %w", templateFilePath, err) } return nil @@ -276,14 +284,14 @@ func (m *migrator) ExtractCodeExamples(content []byte, newRelDir string, templat ext = ".tf" exampleName = "example_" + strconv.Itoa(exampleCount) + ext examplePath = filepath.Join(m.ProviderExamplesDir(), newRelDir, exampleName) - template = fmt.Sprintf(exampleTFCodeTemplate, examplePath) + template = fmt.Sprintf("{{tffile \"%s\"}}", examplePath) m.infof("creating example file %q", examplePath) case "console": importCount++ ext = ".sh" exampleName = "import_" + strconv.Itoa(importCount) + ext examplePath = filepath.Join(m.ProviderExamplesDir(), newRelDir, exampleName) - template = fmt.Sprintf(exampleImportCodeTemplate, examplePath) + template = fmt.Sprintf("{{codefile \"shell\" \"%s\"}}", examplePath) m.infof("creating import file %q", examplePath) default: // Render node as is diff --git a/internal/provider/template.go b/internal/provider/template.go index 06c85d48..809d7411 100644 --- a/internal/provider/template.go +++ b/internal/provider/template.go @@ -250,7 +250,7 @@ description: |- {{ .SchemaMarkdown | trimspace }} ` -const migrateProviderTemplateComment providerTemplate = ` +const migrateProviderTemplateComment string = ` {{/* This template serves as a starting point for documentation generation, and can be customized with hardcoded values and/or doc gen templates. For example, the {{ SchemaMarkdown }} template can be used to replace manual schema documentation if descriptions of schema attributes are added in the provider source code. */ -}} diff --git a/internal/provider/util.go b/internal/provider/util.go index 047583f8..21063dc8 100644 --- a/internal/provider/util.go +++ b/internal/provider/util.go @@ -99,31 +99,6 @@ func writeFile(path string, data string) error { return nil } -// appendFile appends data to the specified file or creates the file if it doesn't exist. -func appendFile(path string, data []byte) error { - dir, _ := filepath.Split(path) - err := os.MkdirAll(dir, 0755) - if err != nil { - return fmt.Errorf("unable to make dir %q: %w", dir, err) - } - - f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644) - if err != nil { - return fmt.Errorf("unable to open file %q: %w", path, err) - } - - _, err = f.Write(data) - if err != nil { - return fmt.Errorf("unable to write file %q: %w", path, err) - } - err = f.Close() - if err != nil { - return fmt.Errorf("unable to close file %q: %w", path, err) - } - - return nil -} - func runCmd(cmd *exec.Cmd) ([]byte, error) { output, err := cmd.CombinedOutput() if err != nil {