diff --git a/tools/diff-processor/cmd/detect_missing_docs.go b/tools/diff-processor/cmd/detect_missing_docs.go new file mode 100644 index 000000000000..60a5a427dac6 --- /dev/null +++ b/tools/diff-processor/cmd/detect_missing_docs.go @@ -0,0 +1,79 @@ +package cmd + +import ( + newProvider "google/provider/new/google/provider" + oldProvider "google/provider/old/google/provider" + "slices" + "sort" + + "encoding/json" + "fmt" + "io" + "os" + + "github.com/GoogleCloudPlatform/magic-modules/tools/diff-processor/detector" + "github.com/GoogleCloudPlatform/magic-modules/tools/diff-processor/diff" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/spf13/cobra" + "golang.org/x/exp/maps" +) + +const detectMissingDocDesc = `Compute list of fields missing documents` + +type MissingDocsInfo struct { + Name string + FilePath string + Fields []string +} + +type detectMissingDocsOptions struct { + rootOptions *rootOptions + computeSchemaDiff func() diff.SchemaDiff + newResourceSchema map[string]*schema.Resource + stdout io.Writer +} + +func newDetectMissingDocsCmd(rootOptions *rootOptions) *cobra.Command { + o := &detectMissingDocsOptions{ + rootOptions: rootOptions, + computeSchemaDiff: func() diff.SchemaDiff { + return diff.ComputeSchemaDiff(oldProvider.ResourceMap(), newProvider.ResourceMap()) + }, + stdout: os.Stdout, + } + cmd := &cobra.Command{ + Use: "detect-missing-docs", + Short: detectMissingDocDesc, + Long: detectMissingDocDesc, + Args: cobra.ExactArgs(1), + RunE: func(c *cobra.Command, args []string) error { + return o.run(args) + }, + } + return cmd +} +func (o *detectMissingDocsOptions) run(args []string) error { + schemaDiff := o.computeSchemaDiff() + detectedResources, err := detector.DetectMissingDocs(schemaDiff, args[0], o.newResourceSchema) + if err != nil { + return err + } + resources := maps.Keys(detectedResources) + slices.Sort(resources) + info := []MissingDocsInfo{} + for _, r := range resources { + details := detectedResources[r] + sort.Strings(details.Fields) + info = append(info, MissingDocsInfo{ + Name: r, + FilePath: details.FilePath, + Fields: details.Fields, + }) + } + + if err := json.NewEncoder(o.stdout).Encode(info); err != nil { + return fmt.Errorf("error encoding json: %w", err) + } + + return nil +} diff --git a/tools/diff-processor/cmd/detect_missing_docs_test.go b/tools/diff-processor/cmd/detect_missing_docs_test.go new file mode 100644 index 000000000000..e385488c04ba --- /dev/null +++ b/tools/diff-processor/cmd/detect_missing_docs_test.go @@ -0,0 +1,91 @@ +package cmd + +import ( + "bytes" + "encoding/json" + "testing" + + "github.com/GoogleCloudPlatform/magic-modules/tools/diff-processor/diff" + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func TestDetectMissingDocs(t *testing.T) { + cases := []struct { + name string + oldResourceMap map[string]*schema.Resource + newResourceMap map[string]*schema.Resource + want []MissingDocsInfo + }{ + { + name: "no new fields", + oldResourceMap: map[string]*schema.Resource{ + "google_x": { + Schema: map[string]*schema.Schema{ + "field-a": {Description: "beep", Computed: true, Optional: true}, + "field-b": {Description: "beep", Computed: true}, + }, + }, + }, + newResourceMap: map[string]*schema.Resource{ + "google_x": { + Schema: map[string]*schema.Schema{ + "field-a": {Description: "beep", Computed: true, Optional: true}, + "field-b": {Description: "beep", Computed: true}, + }, + }, + }, + want: []MissingDocsInfo{}, + }, + { + name: "multiple new fields missing doc", + oldResourceMap: map[string]*schema.Resource{}, + newResourceMap: map[string]*schema.Resource{ + "google_x": { + Schema: map[string]*schema.Schema{ + "field-a": {Description: "beep", Computed: true, Optional: true}, + "field-b": {Description: "beep", Computed: true}, + }, + }, + }, + want: []MissingDocsInfo{ + { + Name: "google_x", + FilePath: "/website/docs/r/x.html.markdown", + Fields: []string{"field-a", "field-b"}, + }, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var buf bytes.Buffer + o := detectMissingDocsOptions{ + computeSchemaDiff: func() diff.SchemaDiff { + return diff.ComputeSchemaDiff(tc.oldResourceMap, tc.newResourceMap) + }, + newResourceSchema: tc.newResourceMap, + stdout: &buf, + } + + err := o.run([]string{t.TempDir()}) + if err != nil { + t.Fatalf("Error running command: %s", err) + } + + out := make([]byte, buf.Len()) + buf.Read(out) + + var got []MissingDocsInfo + if err = json.Unmarshal(out, &got); err != nil { + t.Fatalf("Failed to unmarshall output: %s", err) + } + + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("Unexpected result. Want %+v, got %+v. ", tc.want, got) + } + }) + } +} diff --git a/tools/diff-processor/detector/detector.go b/tools/diff-processor/detector/detector.go index 8305aaac1407..05e0720f085f 100644 --- a/tools/diff-processor/detector/detector.go +++ b/tools/diff-processor/detector/detector.go @@ -1,10 +1,14 @@ package detector import ( + "fmt" + "os" + "path/filepath" "sort" "strings" "github.com/GoogleCloudPlatform/magic-modules/tools/diff-processor/diff" + "github.com/GoogleCloudPlatform/magic-modules/tools/diff-processor/documentparser" "github.com/GoogleCloudPlatform/magic-modules/tools/test-reader/reader" "github.com/hashicorp/hcl/v2/hclwrite" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -33,6 +37,12 @@ type Field struct { Tested bool } +// MissingDocDetails denotes the doc file path and the fields that are not shown up in the corresponding doc. +type MissingDocDetails struct { + FilePath string + Fields []string +} + // Detect missing tests for the given resource changes map in the given slice of tests. // Return a map of resource names to missing test info about that resource. func DetectMissingTests(schemaDiff diff.SchemaDiff, allTests []*reader.Test) (map[string]*MissingTestInfo, error) { @@ -152,3 +162,91 @@ func suggestedTest(resourceName string, untested []string) string { } return strings.ReplaceAll(string(f.Bytes()), `"VALUE"`, "# value needed") } + +// DetectMissingDocs detect new fields that are missing docs given the schema diffs. +// Return a map of resource names to missing doc info. +func DetectMissingDocs(schemaDiff diff.SchemaDiff, repoPath string, resourceMap map[string]*schema.Resource) (map[string]MissingDocDetails, error) { + ret := make(map[string]MissingDocDetails) + for resource, resourceDiff := range schemaDiff { + fieldsInDoc := make(map[string]bool) + + docFilePath, err := resourceToDocFile(resource, repoPath) + if err != nil { + fmt.Printf("Warning: %s.\n", err) + } else { + content, err := os.ReadFile(docFilePath) + if err != nil { + return nil, fmt.Errorf("failed to read resource doc %s: %w", docFilePath, err) + } + parser := documentparser.NewParser() + err = parser.Parse(content) + if err != nil { + return nil, fmt.Errorf("failed to parse document %s: %w", docFilePath, err) + } + + argumentsInDoc := listToMap(parser.Arguments()) + attributesInDoc := listToMap(parser.Attributes()) + for _, m := range []map[string]bool{argumentsInDoc, attributesInDoc} { + for k, v := range m { + fieldsInDoc[k] = v + } + } + // for iam resource + if v, ok := fieldsInDoc["member/members"]; ok { + fieldsInDoc["member"] = v + fieldsInDoc["members"] = v + } + } + details := MissingDocDetails{ + FilePath: strings.ReplaceAll(docFilePath, repoPath, ""), + } + + for field, fieldDiff := range resourceDiff.Fields { + if !isNewField(fieldDiff) { + continue + } + if !fieldsInDoc[field] { + details.Fields = append(details.Fields, field) + } + } + if len(details.Fields) > 0 { + ret[resource] = details + } + } + return ret, nil +} + +func isNewField(fieldDiff diff.FieldDiff) bool { + return fieldDiff.Old == nil && fieldDiff.New != nil +} + +func resourceToDocFile(resource string, repoPath string) (string, error) { + baseNameOptions := []string{ + strings.TrimPrefix(resource, "google_") + ".html.markdown", + resource + ".html.markdown", + } + suffix := []string{"_policy", "_binding", "_member"} + for _, s := range suffix { + if strings.HasSuffix(resource, "_iam"+s) { + iamName := strings.TrimSuffix(resource, s) + baseNameOptions = append(baseNameOptions, iamName+".html.markdown") + baseNameOptions = append(baseNameOptions, strings.TrimPrefix(iamName, "google_")+".html.markdown") + } + } + for _, baseName := range baseNameOptions { + fullPath := filepath.Join(repoPath, "website", "docs", "r", baseName) + _, err := os.ReadFile(fullPath) + if !os.IsNotExist(err) { + return fullPath, nil + } + } + return filepath.Join(repoPath, "website", "docs", "r", baseNameOptions[0]), fmt.Errorf("no document files found in %s for resource %q", baseNameOptions, resource) +} + +func listToMap(items []string) map[string]bool { + m := make(map[string]bool) + for _, item := range items { + m[item] = true + } + return m +} diff --git a/tools/diff-processor/detector/detector_test.go b/tools/diff-processor/detector/detector_test.go index 60ad7739bc7f..30f0dcc5813a 100644 --- a/tools/diff-processor/detector/detector_test.go +++ b/tools/diff-processor/detector/detector_test.go @@ -2,10 +2,12 @@ package detector import ( "reflect" + "sort" "testing" "github.com/GoogleCloudPlatform/magic-modules/tools/diff-processor/diff" "github.com/GoogleCloudPlatform/magic-modules/tools/test-reader/reader" + "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -201,3 +203,206 @@ func TestGetMissingTestsForChanges(t *testing.T) { } } } + +func TestDetectMissingDocs(t *testing.T) { + // top level field_one is argument, field_two is attribute. + resourceSchema := map[string]*schema.Resource{ + "a_resource": { + Schema: map[string]*schema.Schema{ + "field_one": { + Computed: true, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "a": { + Computed: true, + Optional: true, + }, + "b": { + Computed: true, + Optional: false, + }, + "c": { + Computed: true, + Optional: false, + }, + }, + }, + }, + "field_two": { + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "a": { + Computed: true, + Optional: false, + }, + "b": { + Computed: true, + Optional: false, + }, + "c": { + Computed: true, + Optional: false, + }, + }, + }, + }, + "field_three": { + Computed: true, + Optional: true, + }, + "field_four": { + Computed: true, + }, + }, + }, + } + + // If repo is not temp dir, then the doc file points to tools/diff-processor/testdata/website/docs/r/a_resource.html.markdown. + for _, test := range []struct { + name string + schemaDiff diff.SchemaDiff + repo string + want map[string]MissingDocDetails + }{ + { + name: "doc file not exist", + schemaDiff: diff.SchemaDiff{ + "a_resource": diff.ResourceDiff{ + Fields: map[string]diff.FieldDiff{ + "field_one": { + New: &schema.Schema{}, + }, + "field_one.a": { + New: &schema.Schema{}, + }, + "field_one.b": { + New: &schema.Schema{}, + }, + "field_two.a": { + New: &schema.Schema{}, + Old: &schema.Schema{}, + }, + "field_two.b": { + New: &schema.Schema{}, + }, + "field_three": { + New: &schema.Schema{ + Computed: true, + Optional: true, + }, + }, + "field_four": { + New: &schema.Schema{ + Computed: true, + }, + }, + }, + }, + }, + repo: t.TempDir(), + want: map[string]MissingDocDetails{ + "a_resource": { + FilePath: "/website/docs/r/a_resource.html.markdown", + Fields: []string{"field_one", "field_one.a", "field_one.b", "field_two.b", "field_three", "field_four"}, + }, + }, + }, + { + name: "doc file exist", + schemaDiff: diff.SchemaDiff{ + "a_resource": diff.ResourceDiff{ + Fields: map[string]diff.FieldDiff{ + "field_one": { + New: &schema.Schema{}, + }, + "field_one.a": { + New: &schema.Schema{}, + }, + "field_one.b": { + New: &schema.Schema{}, + }, + "field_two.a": { + New: &schema.Schema{}, + Old: &schema.Schema{}, + }, + "field_two.b": { + New: &schema.Schema{}, + }, + "field_three": { + New: &schema.Schema{ + Computed: true, + Optional: true, + }, + }, + "field_four": { + New: &schema.Schema{ + Computed: true, + }, + }, + }, + }, + }, + repo: "../testdata", + want: map[string]MissingDocDetails{ + "a_resource": { + FilePath: "/website/docs/r/a_resource.html.markdown", + Fields: []string{"field_one.b", "field_two.b", "field_three", "field_four"}, + }, + }, + }, + { + name: "nested new field missing doc", + schemaDiff: diff.SchemaDiff{ + "a_resource": diff.ResourceDiff{ + Fields: map[string]diff.FieldDiff{ + "field_one.c": { + New: &schema.Schema{}, + }, + }, + }, + }, + repo: "../testdata", + want: map[string]MissingDocDetails{ + "a_resource": { + FilePath: "/website/docs/r/a_resource.html.markdown", + Fields: []string{"field_one.c"}, + }, + }, + }, + { + name: "member and members is member/members in doc", + schemaDiff: diff.SchemaDiff{ + "a_resource": diff.ResourceDiff{ + Fields: map[string]diff.FieldDiff{ + "member": { + New: &schema.Schema{}, + }, + "members": { + New: &schema.Schema{}, + }, + }, + }, + }, + repo: "../testdata", + want: map[string]MissingDocDetails{}, + }, + } { + t.Run(test.name, func(t *testing.T) { + got, err := DetectMissingDocs(test.schemaDiff, test.repo, resourceSchema) + if err != nil { + t.Fatalf("DetectMissingDocs = %v, want = nil", err) + } + for r := range test.want { + sort.Strings(test.want[r].Fields) + } + for r := range got { + sort.Strings(got[r].Fields) + } + if diff := cmp.Diff(test.want, got); diff != "" { + t.Errorf("DetectMissingDocs = %v, want = %v", got, test.want) + } + }) + } +} diff --git a/tools/diff-processor/documentparser/document_parser.go b/tools/diff-processor/documentparser/document_parser.go new file mode 100644 index 000000000000..28969906c665 --- /dev/null +++ b/tools/diff-processor/documentparser/document_parser.go @@ -0,0 +1,202 @@ +package documentparser + +import ( + "fmt" + "regexp" + "sort" + "strings" +) + +const ( + nestedNamePattern = `\(#(nested_[a-z0-9_]+)\)` + + itemNamePattern = "\\* `([a-z0-9_\\./]+)`" + nestedLinkPattern = `` + + sectionSeparator = "## " + nestedObjectSeparator = ` 0 { + l := len(queue) + for _, cur := range queue { + // the separator should always at the beginning of the line + items := strings.Split(cur.text, "\n"+listItemSeparator) + for _, item := range items[1:] { + text := listItemSeparator + item + itemName, err := findItemName(text) + if err != nil { + return err + } + // There is a special case in some hand written resource eg. in compute_instance, where its attributes is in a.0.b.0.c format. + itemName = strings.ReplaceAll(itemName, ".0.", ".") + nestedName, err := findNestedName(text) + if err != nil { + return err + } + newNode := &node{ + name: itemName, + } + cur.children = append(cur.children, newNode) + if text, ok := nestedBlock[nestedName]; ok { + newNode.text = text + queue = append(queue, newNode) + } + } + + } + queue = queue[l:] + } + return nil +} + +func findItemName(text string) (name string, err error) { + name, err = findPattern(text, itemNamePattern) + if err != nil { + return "", err + } + if name == "" { + return "", fmt.Errorf("cannot find item name from %s", text) + } + return +} + +func findPattern(text string, pattern string) (string, error) { + re, err := regexp.Compile(pattern) + if err != nil { + return "", err + } + match := re.FindStringSubmatch(text) + + if match != nil { + return match[1], nil + } + return "", nil +} + +func findNestedName(text string) (string, error) { + s := strings.ReplaceAll(text, "\n", "") + return findPattern(s, nestedNamePattern) +} diff --git a/tools/diff-processor/documentparser/document_parser_test.go b/tools/diff-processor/documentparser/document_parser_test.go new file mode 100644 index 000000000000..d48df5f184e8 --- /dev/null +++ b/tools/diff-processor/documentparser/document_parser_test.go @@ -0,0 +1,116 @@ +package documentparser + +import ( + "os" + "sort" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestParse(t *testing.T) { + b, err := os.ReadFile("../testdata/resource.html.markdown") + if err != nil { + t.Fatal(err) + } + parser := NewParser() + if err := parser.Parse(b); err != nil { + t.Fatal(err) + } + wantArguments := []string{ + "boot_disk", + "boot_disk.auto_delete", + "boot_disk.device_name", + "boot_disk.disk_encryption_key_raw", + "boot_disk.initialize_params", + "boot_disk.initialize_params.enable_confidential_compute", + "boot_disk.initialize_params.image", + "boot_disk.initialize_params.labels", + "boot_disk.initialize_params.provisioned_iops", + "boot_disk.initialize_params.provisioned_throughput", + "boot_disk.initialize_params.resource_manager_tags", + "boot_disk.initialize_params.size", + "boot_disk.initialize_params.storage_pool", + "boot_disk.initialize_params.type", + "boot_disk.kms_key_self_link", + "boot_disk.mode", + "boot_disk.source", + "name", + "network_interface", + "network_interface.access_config", + "network_interface.access_config.nat_ip", + "network_interface.access_config.network_tier", + "network_interface.access_config.public_ptr_domain_name", + "network_interface.alias_ip_range", + "network_interface.alias_ip_range.ip_cidr_range", + "network_interface.alias_ip_range.subnetwork_range_name", + "network_interface.ipv6_access_config", + "network_interface.ipv6_access_config.external_ipv6", + "network_interface.ipv6_access_config.external_ipv6_prefix_length", + "network_interface.ipv6_access_config.name", + "network_interface.ipv6_access_config.network_tier", + "network_interface.ipv6_access_config.public_ptr_domain_name", + "network_interface.network", + "network_interface.network_attachment", + "network_interface.network_ip", + "network_interface.nic_type", + "network_interface.queue_count", + "network_interface.security_policy", + "network_interface.stack_type", + "params", + // "params.resource_manager_tags", // params text does not include a nested tag + "zone", + "labels", + "description", + "traffic_port_selector", + "traffic_port_selector.ports", + "project", + } + wantAttributes := []string{ + "id", + "network_interface.access_config.nat_ip", + "workload_identity_config", + "errors", + "workload_identity_config.identity_provider", + "workload_identity_config.issuer_uri", + "workload_identity_config.workload_pool", + "errors.message", + } + gotArguments := parser.Arguments() + gotAttributes := parser.Attributes() + for _, arr := range [][]string{gotArguments, wantArguments, gotAttributes, wantAttributes} { + sort.Strings(arr) + } + if diff := cmp.Diff(wantArguments, gotArguments); diff != "" { + t.Errorf("Parse returned diff in arguments(-want, +got): %s", diff) + } + if diff := cmp.Diff(wantAttributes, gotAttributes); diff != "" { + t.Errorf("Parse returned diff in attributes(-want, +got): %s", diff) + } +} + +func TestTraverse(t *testing.T) { + n1 := &node{name: "n1"} + n2 := &node{name: "n2"} + n3 := &node{name: "n3"} + n4 := &node{name: "n4"} + root := &node{ + children: []*node{n1, n2, n3}, + } + n1.children = []*node{n4} + n2.children = []*node{n4} + + var paths []string + traverse(&paths, "", root) + + wantPaths := []string{ + "n1", + "n1.n4", + "n2", + "n2.n4", + "n3", + } + if diff := cmp.Diff(wantPaths, paths); diff != "" { + t.Errorf("traverse returned diff(-want, +got): %s", diff) + } +} diff --git a/tools/diff-processor/testdata/resource.html.markdown b/tools/diff-processor/testdata/resource.html.markdown new file mode 100644 index 000000000000..b06fc5b13984 --- /dev/null +++ b/tools/diff-processor/testdata/resource.html.markdown @@ -0,0 +1,285 @@ +--- +subcategory: "Compute Engine" +description: |- + Manages abcdefg. +--- + +# google_test_resource + +This resource combines some sections in google_compute_instance, google_container_attached_cluster, network_services_endpoint_policy and irrelvant parts are trimmed. + +## Example Usage + +Lorem ipsum + +## Example usage - Confidential Computing + +Lorem ipsum + + +## Argument Reference + +The following arguments are supported: + +* `boot_disk` - (Required) The boot disk for the instance. + Structure is [documented below](#nested_boot_disk). + +* `name` - (Required) A unique name for the resource, required by GCE. + Changing this forces a new resource to be created. + +* `zone` - (Optional) The zone that the machine should be created in. If it is not provided, the provider zone is used. + +* `network_interface` - (Required) Networks to attach to the instance. This can + be specified multiple times. Structure is [documented below](#nested_network_interface). + +* `params` - (Optional) Additional instance parameters. + +--- + +The `boot_disk` block supports: + +* `auto_delete` - (Optional) Whether the disk will be auto-deleted when the instance + is deleted. Defaults to true. + +* `device_name` - (Optional) Name with which attached disk will be accessible. + On the instance, this device will be `/dev/disk/by-id/google-{{device_name}}`. + +* `mode` - (Optional) The mode in which to attach this disk, either `READ_WRITE` + or `READ_ONLY`. If not specified, the default is to attach the disk in `READ_WRITE` mode. + +* `disk_encryption_key_raw` - (Optional) A 256-bit [customer-supplied encryption key] + (https://cloud.google.com/compute/docs/disks/customer-supplied-encryption), + encoded in [RFC 4648 base64](https://tools.ietf.org/html/rfc4648#section-4) + to encrypt this disk. Only one of `kms_key_self_link` and `disk_encryption_key_raw` + may be set. + +* `kms_key_self_link` - (Optional) The self_link of the encryption key that is + stored in Google Cloud KMS to encrypt this disk. Only one of `kms_key_self_link` + and `disk_encryption_key_raw` may be set. + +* `initialize_params` - (Optional) Parameters for a new disk that will be created + alongside the new instance. Either `initialize_params` or `source` must be set. + Structure is [documented below](#nested_initialize_params). + +* `source` - (Optional) The name or self_link of the existing disk (such as those managed by + `google_compute_disk`) or disk image. To create an instance from a snapshot, first create a + `google_compute_disk` from a snapshot and reference it here. + +The `initialize_params` block supports: + +* `size` - (Optional) The size of the image in gigabytes. If not specified, it + will inherit the size of its base image. + +* `type` - (Optional) The GCE disk type. Such as pd-standard, pd-balanced or pd-ssd. + +* `image` - (Optional) The image from which to initialize this disk. This can be + one of: the image's `self_link`, `projects/{project}/global/images/{image}`, + `projects/{project}/global/images/family/{family}`, `global/images/{image}`, + `global/images/family/{family}`, `family/{family}`, `{project}/{family}`, + `{project}/{image}`, `{family}`, or `{image}`. If referred by family, the + images names must include the family name. If they don't, use the + [google_compute_image data source](/docs/providers/google/d/compute_image.html). + For instance, the image `centos-6-v20180104` includes its family name `centos-6`. + These images can be referred by family name here. + +* `labels` - (Optional) A set of key/value label pairs assigned to the disk. This + field is only applicable for persistent disks. + +* `resource_manager_tags` - (Optional) A tag is a key-value pair that can be attached to a Google Cloud resource. You can use tags to conditionally allow or deny policies based on whether a resource has a specific tag. This value is not returned by the API. In Terraform, this value cannot be updated and changing it will recreate the resource. + +* `provisioned_iops` - (Optional) Indicates how many IOPS to provision for the disk. + This sets the number of I/O operations per second that the disk can handle. + For more details,see the [Hyperdisk documentation](https://cloud.google.com/compute/docs/disks/hyperdisks). + Note: Updating currently is only supported for hyperdisk skus via disk update + api/gcloud without the need to delete and recreate the disk, hyperdisk allows + for an update of IOPS every 4 hours. To update your hyperdisk more frequently, + you'll need to manually delete and recreate it. + +* `provisioned_throughput` - (Optional) Indicates how much throughput to provision for the disk. + This sets the number of throughput mb per second that the disk can handle. + For more details,see the [Hyperdisk documentation](https://cloud.google.com/compute/docs/disks/hyperdisks). + Note: Updating currently is only supported for hyperdisk skus via disk update + api/gcloud without the need to delete and recreate the disk, hyperdisk allows + for an update of throughput every 4 hours. To update your hyperdisk more + frequently, you'll need to manually delete and recreate it. + +* `enable_confidential_compute` - (Optional) Whether this disk is using confidential compute mode. + Note: Only supported on hyperdisk skus, disk_encryption_key is required when setting to true. + +* `storage_pool` - (Optional) The URL of the storage pool in which the new disk is created. + For example: + * https://www.googleapis.com/compute/v1/projects/{project}/zones/{zone}/storagePools/{storagePool} + * /projects/{project}/zones/{zone}/storagePools/{storagePool} + + +The `network_interface` block supports: + +* `network` - (Optional) The name or self_link of the network to attach this interface to. + Either `network` or `subnetwork` must be provided. If network isn't provided it will + be inferred from the subnetwork. + +* `subnetwork` - (Optional) The name or self_link of the subnetwork to attach this + interface to. Either `network` or `subnetwork` must be provided. If network isn't provided + it will be inferred from the subnetwork. The subnetwork must exist in the same region this + instance will be created in. If the network resource is in + [legacy](https://cloud.google.com/vpc/docs/legacy) mode, do not specify this field. If the + network is in auto subnet mode, specifying the subnetwork is optional. If the network is + in custom subnet mode, specifying the subnetwork is required. + + +* `subnetwork_project` - (Optional) The project in which the subnetwork belongs. + If the `subnetwork` is a self_link, this field is ignored in favor of the project + defined in the subnetwork self_link. If the `subnetwork` is a name and this + field is not provided, the provider project is used. + +* `network_ip` - (Optional) The private IP address to assign to the instance. If + empty, the address will be automatically assigned. + +* `access_config` - (Optional) Access configurations, i.e. IPs via which this + instance can be accessed via the Internet. Omit to ensure that the instance + is not accessible from the Internet. If omitted, ssh provisioners will not + work unless Terraform can send traffic to the instance's network (e.g. via + tunnel or because it is running on another cloud instance on that network). + This block can be repeated multiple times. Structure [documented below](#nested_access_config). + +* `alias_ip_range` - (Optional) An + array of alias IP ranges for this network interface. Can only be specified for network + interfaces on subnet-mode networks. Structure [documented below](#nested_alias_ip_range). + +* `nic_type` - (Optional) The type of vNIC to be used on this interface. Possible values: GVNIC, VIRTIO_NET. + +* `network_attachment` - (Optional) [Beta](https://terraform.io/docs/providers/google/guides/provider_versions.html) The URL of the network attachment that this interface should connect to in the following format: `projects/{projectNumber}/regions/{region_name}/networkAttachments/{network_attachment_name}`. + +* `stack_type` - (Optional) The stack type for this network interface to identify whether the IPv6 feature is enabled or not. Values are IPV4_IPV6 or IPV4_ONLY. If not specified, IPV4_ONLY will be used. + +* `ipv6_access_config` - (Optional) An array of IPv6 access configurations for this interface. +Currently, only one IPv6 access config, DIRECT_IPV6, is supported. If there is no ipv6AccessConfig +specified, then this instance will have no external IPv6 Internet access. Structure [documented below](#nested_ipv6_access_config). + +* `queue_count` - (Optional) The networking queue count that's specified by users for the network interface. Both Rx and Tx queues will be set to this number. It will be empty if not specified. + +* `security_policy` - (Optional) [Beta](https://terraform.io/docs/providers/google/guides/provider_versions.html) A full or partial URL to a security policy to add to this instance. If this field is set to an empty string it will remove the associated security policy. + +The `access_config` block supports: + +* `nat_ip` - (Optional) The IP address that will be 1:1 mapped to the instance's + network ip. If not given, one will be generated. + +* `public_ptr_domain_name` - (Optional) The DNS domain name for the public PTR record. + To set this field on an instance, you must be verified as the owner of the domain. + See [the docs](https://cloud.google.com/compute/docs/instances/create-ptr-record) for how + to become verified as a domain owner. + +* `network_tier` - (Optional) The [networking tier](https://cloud.google.com/network-tiers/docs/overview) used for configuring this instance. + This field can take the following values: PREMIUM, FIXED_STANDARD or STANDARD. If this field is + not specified, it is assumed to be PREMIUM. + +The `ipv6_access_config` block supports: + +* `external_ipv6` - (Optional) The first IPv6 address of the external IPv6 range associated + with this instance, prefix length is stored in externalIpv6PrefixLength in ipv6AccessConfig. + To use a static external IP address, it must be unused and in the same region as the instance's zone. + If not specified, Google Cloud will automatically assign an external IPv6 address from the instance's subnetwork. + +* `external_ipv6_prefix_length` - (Optional) The prefix length of the external IPv6 range. + +* `name` - (Optional) The name of this access configuration. In ipv6AccessConfigs, the recommended name + is "External IPv6". + +* `network_tier` - (Optional) The service-level to be provided for IPv6 traffic when the + subnet has an external subnet. Only PREMIUM or STANDARD tier is valid for IPv6. + +* `public_ptr_domain_name` - (Optional) The domain name to be used when creating DNSv6 + records for the external IPv6 ranges.. + +The `alias_ip_range` block supports: + +* `ip_cidr_range` - The IP CIDR range represented by this alias IP range. This IP CIDR range + must belong to the specified subnetwork and cannot contain IP addresses reserved by + system or used by other network interfaces. This range may be a single IP address + (e.g. 10.2.3.4), a netmask (e.g. /24) or a CIDR format string (e.g. 10.1.2.0/24). + +* `subnetwork_range_name` - (Optional) The subnetwork secondary range name specifying + the secondary range from which to allocate the IP CIDR range for this alias IP + range. If left unspecified, the primary range of the subnetwork will be used. + +The `params` block supports: + +* `resource_manager_tags` (Optional) - A tag is a key-value pair that can be attached to a Google Cloud resource. You can use tags to conditionally allow or deny policies based on whether a resource has a specific tag. This value is not returned by the API. In Terraform, this value cannot be updated and changing it will recreate the resource. + +- - - + + +* `labels` - + (Optional) + Set of label tags associated with the TcpRoute resource. + **Note**: This field is non-authoritative, and will only manage the labels present in your configuration. + Please refer to the field `effective_labels` for all of the labels present on the resource. + +* `description` - + (Optional) + A free-text description of the resource. Max length 1024 characters. + +* `traffic_port_selector` - + (Optional) + Port selector for the (matched) endpoints. If no port selector is provided, the matched config is applied to all ports. + Structure is [documented below](#nested_traffic_port_selector). + +* `project` - (Optional) The ID of the project in which the resource belongs. + If it is not provided, the provider project is used. + + +The `traffic_port_selector` block supports: + +* `ports` - + (Required) + List of ports. Can be port numbers or port range (example, [80-90] specifies all ports from 80 to 90, including 80 and 90) or named ports or * to specify all ports. If the list is empty, all ports are selected. + + +## Attributes Reference + +In addition to the arguments listed above, the following computed attributes are +exported: + +* `id` - an identifier for the resource with format `projects/{{project}}/zones/{{zone}}/instances/{{name}}` + +* `network_interface.0.access_config.0.nat_ip` - If the instance has an access config, either the given external ip (in the `nat_ip` field) or the ephemeral (generated) ip (if you didn't provide one). + +* `workload_identity_config` - + Workload Identity settings. + Structure is [documented below](#nested_workload_identity_config). + +* `errors` - + A set of errors found in the cluster. + Structure is [documented below](#nested_errors). + + +The `workload_identity_config` block contains: + +* `identity_provider` - + (Optional) + The ID of the OIDC Identity Provider (IdP) associated to + the Workload Identity Pool. + +* `issuer_uri` - + (Optional) + The OIDC issuer URL for this cluster. + +* `workload_pool` - + (Optional) + The Workload Identity Pool associated to the cluster. + +The `errors` block contains: + +* `message` - + (Optional) + Human-friendly description of the error. + +## Timeouts + +Lorem ipsum + +## Import + +Lorem ipsum + diff --git a/tools/diff-processor/testdata/website/docs/r/a_resource.html.markdown b/tools/diff-processor/testdata/website/docs/r/a_resource.html.markdown new file mode 100644 index 000000000000..9d3f5a0cde76 --- /dev/null +++ b/tools/diff-processor/testdata/website/docs/r/a_resource.html.markdown @@ -0,0 +1,18 @@ +## Some resource description + +## Argument Reference + +* `field_one` lorem ipsum. Structure is [documented below](#nested_field_one). +* `member/members` - (Required) lorem ipsum. + +The `field_one` block supports: + +* `a` - (Optional) lorem ipsum. + +## Attributes Reference + +* `field_two` lorem ipsum. Structure is [documented below](#nested_field_two). + +The `field_two` block supports: + +* `a` - (Optional) lorem ipsum. \ No newline at end of file