diff --git a/go.mod b/go.mod index 222a73091..ed1934a58 100644 --- a/go.mod +++ b/go.mod @@ -46,6 +46,7 @@ require ( github.com/spf13/afero v1.9.5 github.com/spf13/cobra v1.8.0 github.com/stretchr/testify v1.9.0 + github.com/yuin/goldmark v1.7.4 github.com/zclconf/go-cty v1.14.2 golang.org/x/crypto v0.24.0 golang.org/x/mod v0.18.0 diff --git a/go.sum b/go.sum index 77e78a00c..d1955a0ce 100644 --- a/go.sum +++ b/go.sum @@ -2032,6 +2032,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg= +github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/zclconf/go-cty v1.0.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8= diff --git a/pkg/tfgen/docs.go b/pkg/tfgen/docs.go index 5562854c2..278e9477a 100644 --- a/pkg/tfgen/docs.go +++ b/pkg/tfgen/docs.go @@ -36,6 +36,10 @@ import ( "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" bf "github.com/russross/blackfriday/v2" "github.com/spf13/afero" + "github.com/yuin/goldmark" + gmast "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/extension" + gmtext "github.com/yuin/goldmark/text" "golang.org/x/text/cases" "golang.org/x/text/language" @@ -401,70 +405,33 @@ func trimFrontMatter(text []byte) []byte { return body[idx+3:] } +func gmWalkNodes(node gmast.Node, f func(gmast.Node)) { + f(node) + for child := node.FirstChild(); child != nil; child = child.NextSibling() { + gmWalkNodes(child, f) + } +} + func splitByMdHeaders(text string, level int) [][]string { bytes := trimFrontMatter([]byte(text)) - idx := 0 - - // nodeLiteralIdx returns a pointer to the first .Literal field on a node or its - // children. - var nodeLiteralIdx func(*bf.Node, *byte) bool - nodeLiteralIdx = func(node *bf.Node, target *byte) bool { - if node == nil { - return false - } - if len(node.Literal) > 0 { - if target == &node.Literal[0] { - return true - } - } - for child := node.FirstChild; child != nil; child = child.Next { - if nodeLiteralIdx(child, target) { - return true - } - } - return false - } - - // Here is where we actually find the set of headers (of the right level). headers := []int{} - parseDoc(bytes).Walk(func(node *bf.Node, entering bool) bf.WalkStatus { - if !entering { - return bf.GoToNext - } - - if node.Type != bf.Heading || node.HeadingData.Level != level || node.HeadingData.IsTitleblock { - return bf.GoToNext - } - var foundHeader bool - for ; idx < len(bytes); idx++ { - // Here we take advantage of the fact that the .Literal field on - // leaf nodes is a view into the same byte array that was passed - // into `parseDoc` to recover the index of of .Literal[0] in the - // original array. - if nodeLiteralIdx(node, &bytes[idx]) { - // We have found in `bytes` the location of a header text, - // but we want the start of the line. We need to walk - // back. - for i := idx; i > 0; i-- { - if bytes[i] == '\n' { - headers = append(headers, i+1) - break - } - } - foundHeader = true - break + + gm := goldmark.New(goldmark.WithExtensions(extension.GFM)) + gmWalkNodes(gm.Parser().Parse(gmtext.NewReader(bytes)), func(node gmast.Node) { + heading, ok := node.(*gmast.Heading) + if !ok || heading.Level != level { + return + } + if heading.Lines().Len() == 0 { + return + } + for i := heading.Lines().At(0).Start; i > 0; i-- { + if bytes[i] == '\n' { + headers = append(headers, i+1) + return } } - // Markdown's alternative headers[1] are undiscovered by this method. That's - // tollerable, since we didn't parse them correctly before either. - // - // [^1]: Alternative headers look like this: - // - // h2 - // --- - contract.Ignore(foundHeader) - return bf.GoToNext }) // headers now contains the index into `bytes` that represent the start of each diff --git a/pkg/tfgen/docs_test.go b/pkg/tfgen/docs_test.go index 07b520157..4508dd2b3 100644 --- a/pkg/tfgen/docs_test.go +++ b/pkg/tfgen/docs_test.go @@ -775,7 +775,7 @@ content }), }, { - input: readTestFile(t, "split_file.md"), + input: readTestFile(t, "alternative_header.md"), level: 2, expected: autogold.Expect([][]string{ { @@ -798,13 +798,141 @@ content "", "* `default_action` - (Optional) Specifies the default action for the account access. Possible values are `Allow` and `Deny`. Defaults to `Deny`.", "", - "* alternative rule", + "* alternative rule (we don't parse this correctly now, but we should)", "---", "", "", }, }), }, + { + input: readTestFile(t, "container_app_environment_custom_domain.md"), + level: 2, + expected: autogold.Expect([][]string{ + { + "---", + `subcategory: "Container Apps"`, + `layout: "azurerm"`, + `page_title: "Azure Resource Manager: azurerm_container_app_environment_custom_domain"`, + "description: |-", + " Manages a Container App Environment Custom Domain.", + "---", + "", + "# azurerm_container_app_environment_custom_domain", + "", + "Manages a Container App Environment Custom Domain Suffix.", + "", + }, + { + "## Example Usage", + "", + "```hcl", + `resource "azurerm_resource_group" "example" {`, + ` name = "example-resources"`, + ` location = "West Europe"`, + "}", + "", + `resource "azurerm_log_analytics_workspace" "example" {`, + ` name = "acctest-01"`, + " location = azurerm_resource_group.example.location", + " resource_group_name = azurerm_resource_group.example.name", + ` sku = "PerGB2018"`, + " retention_in_days = 30", + "}", + "", + `resource "azurerm_container_app_environment" "example" {`, + ` name = "my-environment"`, + " location = azurerm_resource_group.example.location", + " resource_group_name = azurerm_resource_group.example.name", + " log_analytics_workspace_id = azurerm_log_analytics_workspace.example.id", + "}", + "", + `resource "azurerm_container_app_environment_custom_domain" "example" {`, + " container_app_environment_id = azurerm_container_app_environment.example.id", + ` certificate_blob_base64 = filebase64("testacc.pfx")`, + ` certificate_password = "TestAcc"`, + ` dns_suffix = "acceptancetest.contoso.com"`, + "}", + "```", + "", + }, + { + "## Arguments Reference", + "", + "The following arguments are supported:", + "", + "* `container_app_environment_id` - (Required) The ID of the Container Apps Managed Environment. Changing this forces a new resource to be created.", + "", + "* `certificate_blob_base64` - (Required) The bundle of Private Key and Certificate for the Custom DNS Suffix as a base64 encoded PFX or PEM.", + "", + "* `certificate_password` - (Required) The password for the Certificate bundle.", + "", + "* `dns_suffix` - (Required) Custom DNS Suffix for the Container App Environment.", + }, + { + "## Timeouts", + "", + "The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/docs/configuration/resources.html#timeouts) for certain actions:", + "", + "* `create` - (Defaults to 30 minutes) Used when creating the Container App Environment.", + "* `update` - (Defaults to 30 minutes) Used when updating the Container App Environment.", + "* `read` - (Defaults to 5 minutes) Used when retrieving the Container App Environment.", + "* `delete` - (Defaults to 30 minutes) Used when deleting the Container App Environment.", + "", + }, + { + + "## Import", + "", + "A Container App Environment Custom Domain Suffix can be imported using the `resource id` of its parent container ontainer App Environment , e.g.", + "", + "```shell", + `terraform import azurerm_container_app_environment_custom_domain.example "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.App/managedEnvironments/myEnvironment"`, + "```", + "", + }, + }), + }, + { + input: `## Header 1 +content 1 +## +content 2 +## header 3 +content 3 +`, + level: 2, + expected: autogold.Expect([][]string{ + { + "## Header 1", + "content 1", + "## ", + "content 2", + }, + { + "## header 3", + "content 3", + "", + }, + }), + }, + { + level: 2, + input: `## Header 1 +content +## Header 2 +`, + expected: autogold.Expect([][]string{ + { + "## Header 1", + "content", + }, + { + "## Header 2", + "", + }, + }), + }, } for _, tt := range tests { diff --git a/pkg/tfgen/test_data/split_file.md b/pkg/tfgen/test_data/alternative_header.md similarity index 85% rename from pkg/tfgen/test_data/split_file.md rename to pkg/tfgen/test_data/alternative_header.md index 2b0403c57..1fc3ad160 100644 --- a/pkg/tfgen/test_data/split_file.md +++ b/pkg/tfgen/test_data/alternative_header.md @@ -15,6 +15,6 @@ An `account_access` block supports the following: * `default_action` - (Optional) Specifies the default action for the account access. Possible values are `Allow` and `Deny`. Defaults to `Deny`. -* alternative rule +* alternative rule (we don't parse this correctly now, but we should) --- diff --git a/pkg/tfgen/test_data/container_app_environment_custom_domain.md b/pkg/tfgen/test_data/container_app_environment_custom_domain.md new file mode 100644 index 000000000..464750ac5 --- /dev/null +++ b/pkg/tfgen/test_data/container_app_environment_custom_domain.md @@ -0,0 +1,70 @@ +--- +subcategory: "Container Apps" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_container_app_environment_custom_domain" +description: |- + Manages a Container App Environment Custom Domain. +--- + +# azurerm_container_app_environment_custom_domain + +Manages a Container App Environment Custom Domain Suffix. + +## Example Usage + +```hcl +resource "azurerm_resource_group" "example" { + name = "example-resources" + location = "West Europe" +} + +resource "azurerm_log_analytics_workspace" "example" { + name = "acctest-01" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + sku = "PerGB2018" + retention_in_days = 30 +} + +resource "azurerm_container_app_environment" "example" { + name = "my-environment" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + log_analytics_workspace_id = azurerm_log_analytics_workspace.example.id +} + +resource "azurerm_container_app_environment_custom_domain" "example" { + container_app_environment_id = azurerm_container_app_environment.example.id + certificate_blob_base64 = filebase64("testacc.pfx") + certificate_password = "TestAcc" + dns_suffix = "acceptancetest.contoso.com" +} +``` + +## Arguments Reference + +The following arguments are supported: + +* `container_app_environment_id` - (Required) The ID of the Container Apps Managed Environment. Changing this forces a new resource to be created. + +* `certificate_blob_base64` - (Required) The bundle of Private Key and Certificate for the Custom DNS Suffix as a base64 encoded PFX or PEM. + +* `certificate_password` - (Required) The password for the Certificate bundle. + +* `dns_suffix` - (Required) Custom DNS Suffix for the Container App Environment. +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/docs/configuration/resources.html#timeouts) for certain actions: + +* `create` - (Defaults to 30 minutes) Used when creating the Container App Environment. +* `update` - (Defaults to 30 minutes) Used when updating the Container App Environment. +* `read` - (Defaults to 5 minutes) Used when retrieving the Container App Environment. +* `delete` - (Defaults to 30 minutes) Used when deleting the Container App Environment. + +## Import + +A Container App Environment Custom Domain Suffix can be imported using the `resource id` of its parent container ontainer App Environment , e.g. + +```shell +terraform import azurerm_container_app_environment_custom_domain.example "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.App/managedEnvironments/myEnvironment" +```