scripts/consistency/consistency_check.go (370 lines of code) (raw):

//nolint:all package main import ( "bufio" "flag" "fmt" "io/ioutil" "os" "path/filepath" "regexp" "strings" "github.com/aliyun/terraform-provider-alicloud/alicloud" set "github.com/deckarep/golang-set" "github.com/hashicorp/terraform-plugin-sdk/helper/schema" log "github.com/sirupsen/logrus" "github.com/waigani/diffparser" ) func init() { customFormatter := new(log.TextFormatter) customFormatter.FullTimestamp = true customFormatter.TimestampFormat = "2006-01-02 15:04:05" customFormatter.DisableTimestamp = false customFormatter.DisableColors = false customFormatter.ForceColors = true log.SetFormatter(customFormatter) log.SetOutput(os.Stdout) log.SetLevel(log.DebugLevel) } var ( resourceNames = flag.String("resourceNames", "", "the names of the terraform resources to diff") fileNames = flag.String("fileNames", "", "the files to check diff") filterList = map[string][]string{ "alicloud_amqp_instance": {"logistics"}, "alicloud_cms_alarm": {"notify_type"}, "alicloud_cs_serverless_kubernetes": {"private_zone", "create_v2_cluster"}, "alicloud_slb_listener": {"lb_protocol", "instance_port", "lb_port"}, "alicloud_kvstore_instance": {"connection_string"}, "alicloud_instance": {"subnet_id"}, "alicloud_hbr_ots_backup_plan": {"vault_id"}, "alicloud_nat_gateway": {"vswitch_id"}, "alicloud_ecs_disk": {"advanced_features", "encrypt_algorithm", "dedicated_block_storage_cluster_id"}, } skippedSchemaKeys = map[string]struct{}{ "page_size": {}, "page_number": {}, "total_count": {}, "max_results": {}, } ) type ResourceAttribute struct { Name string Type string Optional string Required string ForceNew bool Default string ElemType string Deprecated string DocsLineNum int Removed string } func main() { exitCode := 0 flag.Parse() if fileNames != nil && len(*fileNames) == 0 { log.Infof("the diff file is empty, shipped!") return } byt, _ := ioutil.ReadFile(*fileNames) diff, _ := diffparser.Parse(string(byt)) fileRegex := regexp.MustCompile("alicloud/(resource|data_source)[0-9a-zA-Z_]*.go") fileTestRegex := regexp.MustCompile("alicloud/(resource|data_source)[0-9a-zA-Z_]*_test.go") fileDocsRegex := regexp.MustCompile("website/docs/(r|d)/[0-9a-zA-Z_]*.html.markdown") resourceNameMap := make(map[string]struct{}) for _, file := range diff.Files { resourceName := "" isResource := true docsPath := "website/docs/r/" if fileRegex.MatchString(file.NewName) { if fileTestRegex.MatchString(file.NewName) { continue } resourceName = strings.TrimPrefix(strings.TrimSuffix(strings.Split(file.NewName, "/")[1], ".go"), "resource_") } else if fileDocsRegex.MatchString(file.NewName) { resourceName = "alicloud_" + strings.TrimSuffix(strings.Split(file.NewName, "/")[3], ".html.markdown") } else { continue } if _, ok := resourceNameMap[resourceName]; ok { continue } else { resourceNameMap[resourceName] = struct{}{} } log.Infof("==> Checking resource or data-source %s attributes consistency...", resourceName) resource, ok := alicloud.Provider().(*schema.Provider).ResourcesMap[resourceName] if !ok || resource == nil { resourceName = strings.TrimPrefix(resourceName, "data_source_") resource, ok = alicloud.Provider().(*schema.Provider).DataSourcesMap[resourceName] if !ok || resource == nil { log.Errorf("resource %s is not found in the provider ResourceMap\n\n", resourceName) exitCode = 1 continue } docsPath = "website/docs/d/" isResource = false } resourceSchema := resource.Schema resourceSchemaFromDocs := make(map[string]ResourceAttribute) if err := parseResourceDocs(resourceName, docsPath, isResource, resourceSchemaFromDocs); err != nil { log.Errorf("parsing the resource %s docs failed. error: %s", resourceName, err) continue } if consistencyCheck(resourceName, resourceSchemaFromDocs, resourceSchema) { log.Infof("--- PASS!\n\n") continue } log.Errorf("--- Failed!\n\n") exitCode = 1 } if exitCode > 0 { os.Exit(exitCode) } return } func parseResourceDocs(resourceName, docsPath string, isResource bool, resourceAttributes map[string]ResourceAttribute) error { splitRes := strings.Split(resourceName, "alicloud_") if len(splitRes) < 2 { log.Errorf("parsing resource name %s failed.", resourceName) return fmt.Errorf(fmt.Sprintf("parsing resource name %s failed.", resourceName)) } filePath := strings.Join([]string{docsPath, splitRes[1], ".html.markdown"}, "") file, err := os.Open(filePath) if err != nil { log.Errorf("open resource %s docs failed. Error: %s", filePath, err) return err } defer file.Close() argsRegex := regexp.MustCompile("## Argument Reference") attribRegex := regexp.MustCompile("## Attributes Reference") secondLevelRegex := regexp.MustCompile("^### `([a-zA-Z_\\.0-9-]*)`") argumentsFieldRegex := regexp.MustCompile("^\\* `([a-zA-Z_0-9]*)`[ ]*-? ?(\\(.*\\)) ?(.*)") attributeFieldRegex := regexp.MustCompile("^\\s*\\* `([a-zA-Z_0-9]*)`[ ]*-?(.*)") name := filepath.Base(filePath) re := regexp.MustCompile("[a-z0-9A-Z_]*") resourceName = "alicloud_" + re.FindString(name) scanner := bufio.NewScanner(file) phase := "Argument" record := false subAttributeName := "" line := 0 rootPrefixLen := 0 rootName := "" extraResourceAttribute := map[string]struct{}{} for scanner.Scan() { line += 1 text := scanner.Text() if strings.HasPrefix(text, "#") && (strings.HasSuffix(text, "Timeouts") || strings.HasSuffix(text, "Import")) { break } if argsRegex.MatchString(text) { record = true phase = "Argument" continue } if attribRegex.MatchString(text) { record = true phase = "Attribute" subAttributeName = "" continue } if secondLevelRegex.MatchString(strings.TrimSpace(text)) { record = true parts := strings.Split(strings.TrimSpace(text), " ") subAttributeName = strings.Replace(strings.Trim(parts[len(parts)-1], "`"), "-", ".", -1) continue } if record { var matched [][]string if phase == "Argument" { matched = argumentsFieldRegex.FindAllStringSubmatch(text, 1) } else if phase == "Attribute" { matched = attributeFieldRegex.FindAllStringSubmatch(text, 1) } for _, m := range matched { if phase == "Attribute" { thisLen := len(strings.Split(m[0], "*")[0]) if rootPrefixLen < thisLen { if subAttributeName != "" { subAttributeName += "." + rootName } else { subAttributeName = rootName } rootName = m[1] } else if rootPrefixLen > thisLen { parts := strings.Split(subAttributeName, ".") backIndex := rootPrefixLen - thisLen if backIndex%2 == 0 { backIndex /= 2 } else { return fmt.Errorf("resource %s docs %s have not been formatted.", resourceName, docsPath) } if len(parts) > 0 { for backIndex > 0 { backIndex-- if strings.Contains(subAttributeName, ".") { subAttributeName = strings.TrimSuffix(strings.TrimSuffix(subAttributeName, parts[len(parts)-1]), ".") parts = parts[:len(parts)-1] } else { subAttributeName = "" } rootName = m[1] } } } else { rootName = m[1] } rootPrefixLen = thisLen } attribute := parseMatchLine(m, phase, subAttributeName) if attribute == nil { continue } attribute.DocsLineNum = line if _, ok := resourceAttributes[attribute.Name]; !ok { resourceAttributes[attribute.Name] = *attribute } else if isResource { extraResourceAttribute[attribute.Name] = struct{}{} } if phase == "Attribute" { for key := range extraResourceAttribute { if strings.HasPrefix(attribute.Name, key+".") { delete(extraResourceAttribute, key) } } } } } } for key := range extraResourceAttribute { log.Errorf("'%v' has been set in the `## Argument Reference` and it should be removed from `## Attributes Reference`", key) } return nil } func parseMatchLine(words []string, phase, rootName string) *ResourceAttribute { result := ResourceAttribute{} if phase == "Argument" && len(words) >= 4 { if rootName != "" { result.Name = rootName + "." + words[1] } else { result.Name = words[1] } //result["Description"] = words[3] if strings.Contains(words[2], "Optional") { result.Optional = "true" } if strings.Contains(words[2], "Required") { result.Required = "true" } if strings.Contains(words[2], "ForceNew") { result.ForceNew = true } if strings.Contains(words[2], "Deprecated") { result.Deprecated = "Deprecated since" } return &result } if phase == "Attribute" && len(words) >= 3 { if rootName != "" { result.Name = rootName + "." + words[1] } else { result.Name = words[1] } if strings.Contains(words[2], "Deprecated") { result.Deprecated = "Deprecated since" } //result["Description"] = words[2] return &result } return nil } func consistencyCheck(resourceName string, resourceAttributeFromDocs map[string]ResourceAttribute, resourceSchemaDefined map[string]*schema.Schema) bool { isConsistent := true filteredList := set.NewSet() if val, ok := filterList[resourceName]; ok { for _, v := range val { filteredList.Add(v) } } // the number of the schema field + 1(id) should equal to the number defined in document resourceAttributes := make(map[string]ResourceAttribute) getResourceAttributes("", resourceAttributes, resourceSchemaDefined, "") for attributeKey, attributeValue := range resourceAttributes { if _, ok := skippedSchemaKeys[attributeKey]; ok { continue } attributeDocsValue, ok := resourceAttributeFromDocs[attributeKey] if attributeValue.Removed != "" { continue } if !ok { isConsistent = false log.Errorf("'%v' is not found in the docs", attributeKey) } if attributeValue.Deprecated != "" { if attributeDocsValue.Deprecated == "" { isConsistent = false log.Errorf("'%v' should be marked as Deprecated in the document description", attributeKey) } continue } if attributeValue.Optional == "true" && attributeDocsValue.Optional != attributeValue.Optional { isConsistent = false log.Errorf("'%v' should be marked as Optional in the document", attributeKey) } if attributeValue.Required == "true" && attributeDocsValue.Required != attributeValue.Required { isConsistent = false log.Errorf("'%v' should be marked as Required in the document", attributeKey) } if attributeValue.ForceNew && !attributeDocsValue.ForceNew { isConsistent = false log.Errorf("'%v' should be marked as ForceNew in the document description", attributeKey) } } for attributeKey, _ := range resourceAttributeFromDocs { if _, ok := resourceAttributes[attributeKey]; !ok && attributeKey != "id" { isConsistent = false log.Errorf("'%v' which described in the docs not found in the resource schema", attributeKey) } } return isConsistent } func getResourceAttributes(rootName string, resourceAttributeMap map[string]ResourceAttribute, resourceSchema map[string]*schema.Schema, rootRemoved string) { for key, value := range resourceSchema { if rootName != "" { key = rootName + "." + key } var thisRemoved = value.Removed if len(thisRemoved) == 0 && len(rootRemoved) != 0 { thisRemoved = rootRemoved } if _, ok := resourceAttributeMap[key]; !ok { resourceAttributeMap[key] = ResourceAttribute{ Name: key, Type: value.Type.String(), Optional: fmt.Sprint(value.Optional), Required: fmt.Sprint(value.Required), ForceNew: value.ForceNew, Default: fmt.Sprint(value.Default), Deprecated: value.Deprecated, Removed: thisRemoved, } } if value.Type == schema.TypeSet || value.Type == schema.TypeList { if v, ok := value.Elem.(*schema.Schema); ok { vv := resourceAttributeMap[key] vv.ElemType = v.Type.String() resourceAttributeMap[key] = vv } else { vv := resourceAttributeMap[key] vv.ElemType = "Object" resourceAttributeMap[key] = vv getResourceAttributes(key, resourceAttributeMap, value.Elem.(*schema.Resource).Schema, thisRemoved) } } if value.Type == schema.TypeMap { if _, ok := value.Elem.(*schema.Resource); ok { vv := resourceAttributeMap[key] vv.ElemType = "Object" resourceAttributeMap[key] = vv getResourceAttributes(key, resourceAttributeMap, value.Elem.(*schema.Resource).Schema, thisRemoved) } } } }