coverage/index.go (261 lines of code) (raw):

package coverage import ( "archive/zip" "bytes" "encoding/json" "fmt" "io" "net/http" "net/url" "os" "path/filepath" "strings" openapispec "github.com/go-openapi/spec" "github.com/magodo/azure-rest-api-index/azidx" "github.com/sirupsen/logrus" ) const ( indexFileURL = "https://raw.githubusercontent.com/teowa/azure-rest-api-index-file/main/index.json.zip" azureRepoURL = "https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/" ) var indexCache *azidx.Index func GetIndexFromLocalDir(swaggerRepo, indexFilePath string) (*azidx.Index, error) { if indexCache != nil { return indexCache, nil } if indexFilePath != "" { if _, err := os.Stat(indexFilePath); err == nil { byteValue, _ := os.ReadFile(indexFilePath) var index azidx.Index if err := json.Unmarshal(byteValue, &index); err != nil { return nil, fmt.Errorf("unmarshal index file: %+v", err) } indexCache = &index logrus.Infof("load index from cache file %s", indexFilePath) return indexCache, nil } } logrus.Infof("building index from from local swagger %s, it might take several minutes", swaggerRepo) index, err := azidx.BuildIndex(swaggerRepo, "") if err != nil { logrus.Error(fmt.Sprintf("failed to build index: %+v", err)) return nil, err } logrus.Infof("index successfully built on commit %+v", index.Commit) indexCache = index if indexFilePath != "" { jsonBytes, err := json.Marshal(&index) if err != nil { logrus.Warningf("failed to marshal index: %+v", err) return index, nil } err = os.WriteFile(indexFilePath, jsonBytes, 0644) if err != nil { logrus.Warningf("failed to write index cache file %s: %+v", indexFilePath, err) return index, nil } logrus.Infof("index successfully saved to cache file %s", indexFilePath) } return index, nil } func GetIndex(indexFilePath string) (*azidx.Index, error) { if indexCache != nil { return indexCache, nil } if indexFilePath != "" { if _, err := os.Stat(indexFilePath); err == nil { byteValue, _ := os.ReadFile(indexFilePath) var index azidx.Index if err := json.Unmarshal(byteValue, &index); err != nil { return nil, fmt.Errorf("unmarshal index file: %+v", err) } indexCache = &index logrus.Infof("load index from cache file %s", indexFilePath) return indexCache, nil } } resp, err := http.Get(indexFileURL) if err != nil { return nil, fmt.Errorf("get index file from %v: %+v", indexFileURL, err) } logrus.Infof("downloading index file from %s", indexFileURL) defer resp.Body.Close() b, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("download index file zip: %+v", err) } zipReader, err := zip.NewReader(bytes.NewReader(b), int64(len(b))) if err != nil { return nil, fmt.Errorf("read index file zip: %+v", err) } var unzippedIndexBytes []byte for _, zipFile := range zipReader.File { if strings.EqualFold(zipFile.Name, "index.json") { unzippedIndexBytes, err = readZipFile(zipFile) if err != nil { return nil, fmt.Errorf("unzip index file: %+v", err) } break } } if len(unzippedIndexBytes) == 0 { return nil, fmt.Errorf("index file not found in zip") } var index azidx.Index if err := json.Unmarshal(unzippedIndexBytes, &index); err != nil { return nil, fmt.Errorf("unmarshal index file: %+v", err) } indexCache = &index logrus.Infof("load index based commit: https://github.com/Azure/azure-rest-api-specs/tree/%s", index.Commit) if indexFilePath != "" { jsonBytes, err := json.Marshal(&index) if err != nil { logrus.Warningf("failed to marshal index: %+v", err) return indexCache, nil } err = os.WriteFile(indexFilePath, jsonBytes, 0644) if err != nil { logrus.Warningf("failed to write index cache file %s: %+v", indexFilePath, err) return indexCache, nil } logrus.Infof("index successfully saved to cache file %s", indexFilePath) } return indexCache, nil } type SwaggerModel struct { ApiPath string ModelName string SwaggerPath string OperationID string } // GetModelInfoFromIndex will try to download online index from https://github.com/teowa/azure-rest-api-index-file, and get model info from it // if the index is already downloaded as in {indexFilePath}, it will use the cached index func GetModelInfoFromIndex(resourceId, apiVersion, method, indexFilePath string) (*SwaggerModel, error) { index, err := GetIndex(indexFilePath) if err != nil { return nil, err } resourceURL := fmt.Sprintf("https://management.azure.com%s?api-version=%s", resourceId, apiVersion) uRL, err := url.Parse(resourceURL) if err != nil { return nil, fmt.Errorf("parsing URL %s: %+v", resourceURL, err) } ref, err := index.Lookup(method, *uRL) if err != nil { return nil, fmt.Errorf("lookup %s URL %s in index: %+v", method, resourceURL, err) } model, err := GetModelInfoFromIndexRef(openapispec.Ref{Ref: *ref}, azureRepoURL) if err != nil { return nil, fmt.Errorf("get model %s: %+v", ref, err) } if model.ModelName == "" { return nil, fmt.Errorf("PUT model not found for %s", ref.String()) } return model, nil } // GetModelInfoFromLocalIndex tries to build index from local swagger repo and get model info from it func GetModelInfoFromLocalIndex(resourceId, apiVersion, method, swaggerRepo, indexCacheFile string) (*SwaggerModel, error) { swaggerRepo, err := filepath.Abs(swaggerRepo) if err != nil { return nil, fmt.Errorf("swagger repo path %q is invalid: %+v", swaggerRepo, err) } if _, err := os.Stat(swaggerRepo); os.IsNotExist(err) { return nil, fmt.Errorf("swagger repo path %q is invalid: path does not exist", swaggerRepo) } swaggerRepo = strings.TrimSuffix(swaggerRepo, "/") if !strings.HasSuffix(swaggerRepo, "specification") { return nil, fmt.Errorf("swagger repo path %q is invalid: must point to \"specification\", e.g., /home/projects/azure-rest-api-specs/specification", swaggerRepo) } swaggerRepo += "/" index, err := GetIndexFromLocalDir(swaggerRepo, indexCacheFile) if err != nil { return nil, fmt.Errorf("build index from local dir %s: %+v", swaggerRepo, err) } resourceURL := fmt.Sprintf("https://management.azure.com%s?api-version=%s", resourceId, apiVersion) uRL, err := url.Parse(resourceURL) if err != nil { return nil, fmt.Errorf("parsing URL %s: %+v", resourceURL, err) } ref, err := index.Lookup(method, *uRL) if err != nil { return nil, fmt.Errorf("lookup %s URL %s in index: %+v", method, resourceURL, err) } model, err := GetModelInfoFromIndexRef(openapispec.Ref{Ref: *ref}, swaggerRepo) if err != nil { return nil, fmt.Errorf("get model %s: %+v", ref, err) } if model.ModelName == "" { return nil, fmt.Errorf("PUT model not found for %s", ref.String()) } return model, nil } func GetModelInfoFromIndexRef(ref openapispec.Ref, swaggerRepo string) (*SwaggerModel, error) { _, swaggerPath := SchemaNamePathFromRef(swaggerRepo, ref) seperator := "/" // in windows the ref might use backslashes if strings.Contains(ref.GetURL().Path, string(os.PathSeparator)) { seperator = string(os.PathSeparator) } relativeBase := swaggerRepo + strings.Split(ref.GetURL().Path, seperator)[0] operation, err := openapispec.ResolvePathItemWithBase(nil, ref, &openapispec.ExpandOptions{RelativeBase: relativeBase}) if err != nil { return nil, err } pointerTokens := ref.GetPointer().DecodedTokens() apiPath := pointerTokens[1] var modelName string for _, param := range operation.Parameters { paramRef := param.Ref if paramRef.String() != "" { refParam, err := openapispec.ResolveParameterWithBase(nil, param.Ref, &openapispec.ExpandOptions{RelativeBase: swaggerPath}) if err != nil { return nil, fmt.Errorf("resolve param ref %q: %+v", param.Ref.String(), err) } // Update the param param = *refParam } if param.In == "body" { if paramRef.String() != "" { modelName, swaggerPath = SchemaNamePathFromRef(swaggerPath, paramRef) } if param.Schema.Ref.String() != "" { modelName, swaggerPath = SchemaNamePathFromRef(swaggerPath, param.Schema.Ref) } break } } return &SwaggerModel{ ApiPath: apiPath, ModelName: modelName, SwaggerPath: swaggerPath, }, nil } func readZipFile(zf *zip.File) ([]byte, error) { f, err := zf.Open() if err != nil { return nil, err } defer f.Close() return io.ReadAll(f) } func MockResourceIDFromType(azapiResourceType string) (string, string) { const ( managementGroupId = "/providers/Microsoft.Management" subscritionSeg = "/subscriptions/00000000-0000-0000-0000-000000000000" resourceGroupSeg = "resourceGroups/rg" ) resourceType := strings.Split(azapiResourceType, "@")[0] apiVersion := strings.Split(azapiResourceType, "@")[1] resourceProvider := strings.Split(resourceType, "/")[0] rTypes := strings.Split(resourceType, "/")[1:] typeIds := strings.Join(rTypes, "/xxx/") + "/xxx" if strings.HasPrefix(strings.ToLower(resourceType), strings.ToLower("Microsoft.Management/managementGroups")) { return fmt.Sprintf("%s/%s", managementGroupId, typeIds), apiVersion } if strings.EqualFold(resourceType, "Microsoft.Resources/subscriptions") { return subscritionSeg, apiVersion } if strings.EqualFold(resourceType, "Microsoft.Resources/resourceGroups") { return fmt.Sprintf("%s/%s", subscritionSeg, resourceGroupSeg), apiVersion } return fmt.Sprintf("%s/%s/providers/%s/%s", subscritionSeg, resourceGroupSeg, resourceProvider, typeIds), apiVersion } func GetModelInfoFromIndexWithType(azapiResourceType, method, indexCacheFile string) (*SwaggerModel, error) { resourceId, apiVersion := MockResourceIDFromType(azapiResourceType) return GetModelInfoFromIndex(resourceId, apiVersion, method, indexCacheFile) }