// docs-generator — генератор Markdown-документации для ресурсов провайдера. package main import ( "flag" "fmt" "io/fs" "os" "path/filepath" "regexp" "sort" "strconv" "strings" "gopkg.in/yaml.v3" "docs-generator/internal/ops" "docs-generator/internal/types" "docs-generator/internal/writers" ) func main() { resourcesDirFlag := flag.String("resources", "", "Path to resources_yaml") docsDirFlag := flag.String("docs", "", "Path to docs/30_registry/resources") servicesListFlag := flag.String("services", "", "Path to devops/config/services_list.txt") excludeFlag := flag.String("exclude", "", "Comma-separated resource names to skip") versionFlag := flag.String("version", "", "Provider version for example block") apiEndpointFlag := flag.String("api-endpoint", "https://deck-api.ngcloud.ru/api/v1/index.cfm", "API endpoint for example block") opsFlag := flag.Bool("ops", false, "Generate per-service operations docs (resources_ops_yaml → docs/.../operations)") flag.Parse() root := detectRoot() // --ops mode: generate per-service operations documentation if *opsFlag { runOpsMode(root) return } resourcesDir := pickPath(*resourcesDirFlag, filepath.Join(root, "provider", "resources_yaml")) docsDir := pickPath(*docsDirFlag, filepath.Join(root, "docs", "30_registry", "resources")) servicesList := pickPath(*servicesListFlag, filepath.Join(root, "devops", "config", "services_list.txt")) writers.CloudOutputByServiceID = writers.LoadCloudOutputSnapshot(root) excludeSet := toSet(*excludeFlag) version := *versionFlag if version == "" { version = detectVersion(filepath.Join(root, "provider", "main.go")) } apiEndpoint := *apiEndpointFlag servicesOrder := loadServicesList(servicesList) specs := loadSpecs(resourcesDir, servicesOrder) if len(specs) == 0 { fmt.Fprintf(os.Stderr, "No specs found in %s\n", resourcesDir) os.Exit(1) } if err := os.MkdirAll(docsDir, 0o755); err != nil { fmt.Fprintf(os.Stderr, "ERROR: cannot create docs dir %s: %v\n", docsDir, err) os.Exit(1) } var processedSpecs []types.ServiceSpec for _, spec := range specs { if excludeSet[spec.Name] { continue } writers.ResourceDocs(docsDir, spec, version, apiEndpoint) processedSpecs = append(processedSpecs, spec) } writers.IndexMD(docsDir, processedSpecs) } func detectRoot() string { cwd, err := os.Getwd() if err != nil { panic(err) } if filepath.Base(cwd) == "provider" { return filepath.Dir(cwd) } return cwd } func pickPath(value, fallback string) string { if value != "" { return value } return fallback } func toSet(csv string) map[string]bool { out := map[string]bool{} for _, item := range strings.Split(csv, ",") { trimmed := strings.TrimSpace(item) if trimmed == "" { continue } out[trimmed] = true } return out } func detectVersion(mainPath string) string { b, err := os.ReadFile(mainPath) if err != nil { return "2.x" } re := regexp.MustCompile(`version string\s*=\s*"([0-9.]+)"`) match := re.FindStringSubmatch(string(b)) if len(match) > 1 { return match[1] } return "2.x" } func loadServicesList(path string) []types.ServiceMeta { b, err := os.ReadFile(path) if err != nil { return nil } lines := strings.Split(string(b), "\n") out := []types.ServiceMeta{} for _, line := range lines { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") { continue } head := strings.SplitN(line, "#", 2)[0] parts := strings.Fields(head) if len(parts) < 2 { continue } id, err := strconv.Atoi(parts[0]) if err != nil { continue } out = append(out, types.ServiceMeta{ID: id, Name: parts[1]}) } return out } func loadSpecs(dir string, ordered []types.ServiceMeta) []types.ServiceSpec { specsByID := map[int]types.ServiceSpec{} filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() || !strings.HasSuffix(d.Name(), ".yaml") { return nil } b, err := os.ReadFile(path) if err != nil { return err } var spec types.ServiceSpec if err := yaml.Unmarshal(b, &spec); err != nil { return err } specsByID[spec.ServiceID] = spec return nil }) if len(ordered) == 0 { specs := make([]types.ServiceSpec, 0, len(specsByID)) for _, spec := range specsByID { specs = append(specs, spec) } sort.Slice(specs, func(i, j int) bool { return specs[i].ServiceID < specs[j].ServiceID }) return specs } specs := []types.ServiceSpec{} for _, meta := range ordered { spec, ok := specsByID[meta.ID] if !ok { continue } specs = append(specs, spec) } return specs } // runOpsMode генерирует per-service operations документацию. func runOpsMode(root string) { yamlDir := pickPath(os.Getenv("NUBES_OPS_YAML_DIR"), filepath.Join(root, "provider", "resources_ops_yaml")) docsDir := pickPath(os.Getenv("NUBES_OPS_DOCS_DIR"), filepath.Join(root, "docs", "30_registry", "resources", "operations")) if err := os.MkdirAll(docsDir, 0o755); err != nil { panic(err) } entries := []ops.Entry{} err := filepath.WalkDir(yamlDir, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() || !strings.HasSuffix(d.Name(), ".yaml") { return nil } b, err := os.ReadFile(path) if err != nil { return err } var spec ops.ServiceOpsSpec if err := yaml.Unmarshal(b, &spec); err != nil { return err } if spec.ServiceID == 0 || spec.Name == "" { return nil } fileName := fmt.Sprintf("%d_%s.md", spec.ServiceID, ops.Name(spec.Name)) outPath := filepath.Join(docsDir, fileName) if err := ops.ServiceDoc(outPath, spec); err != nil { return err } entries = append(entries, ops.Entry{ ServiceID: spec.ServiceID, Name: spec.Name, File: fileName, Title: ops.Title(spec), OpCount: len(spec.Operations), }) return nil }) if err != nil { panic(err) } sort.Slice(entries, func(i, j int) bool { return entries[i].ServiceID < entries[j].ServiceID }) if err := ops.Index(filepath.Join(docsDir, "index.md"), entries); err != nil { panic(err) } fmt.Printf("Generated %d operations docs in %s\n", len(entries), docsDir) }