- renderParamTable/renderModifyTable/renderNestedParams: убрать колонку ID - collectConstraints: value_list → Допустимые значения - buildExamplePage: минимальный пример + полный в <details> - minimalExampleBlock: только required без default - WriteNavFragment: генерация _nav_fragment.yml с категориями - mkdocs.yml: navigation.path, navigation.footer, navigation.indexes
225 lines
6.0 KiB
Go
225 lines
6.0 KiB
Go
// docs-generator — генератор Markdown-документации для ресурсов провайдера.
|
|
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"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 TOOLS/config/{stand}/services_list.txt")
|
|
excludeFlag := flag.String("exclude", "", "Comma-separated resource names to skip")
|
|
versionFlag := flag.String("version", "", "Provider version for example block")
|
|
// ⛔ LEGACY DEFAULT (index.cfm) — переопределяется через NUBES_API_ENDPOINT в profile.env.
|
|
// Никогда не использовать deck-api.ngcloud.ru напрямую.
|
|
apiEndpointFlag := flag.String("api-endpoint", "https://lk-api-gateway.ngcloud.ru/api/v1/svc", "API endpoint for example block")
|
|
providerSourceFlag := flag.String("provider-source", "registry.kube5s.ru/nubes-dev/nubes", "Provider source for example block")
|
|
opsFlag := flag.Bool("ops", false, "Generate per-service operations docs (resources_ops_yaml → docs/.../operations)")
|
|
flag.Parse()
|
|
|
|
// --ops mode: generate per-service operations documentation
|
|
if *opsFlag {
|
|
runOpsMode()
|
|
return
|
|
}
|
|
resourcesDir := *resourcesDirFlag
|
|
if resourcesDir == "" {
|
|
panic("--resources is required")
|
|
}
|
|
docsDir := *docsDirFlag
|
|
if docsDir == "" {
|
|
panic("--docs is required")
|
|
}
|
|
servicesList := *servicesListFlag
|
|
writers.CloudOutputByServiceID = writers.LoadCloudOutputSnapshot(*docsDirFlag)
|
|
excludeSet := toSet(*excludeFlag)
|
|
version := *versionFlag
|
|
if version == "" {
|
|
panic("--version is required")
|
|
}
|
|
apiEndpoint := *apiEndpointFlag
|
|
providerSource := *providerSourceFlag
|
|
|
|
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, providerSource)
|
|
processedSpecs = append(processedSpecs, spec)
|
|
}
|
|
writers.IndexMD(docsDir, processedSpecs)
|
|
writers.WriteNavFragment(docsDir, processedSpecs)
|
|
}
|
|
|
|
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 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
|
|
}
|
|
for i := range spec.Operations {
|
|
types.FixupSubParams(spec.Operations[i].Params)
|
|
}
|
|
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() {
|
|
yamlDir := os.Getenv("NUBES_OPS_YAML_DIR")
|
|
if yamlDir == "" {
|
|
panic("NUBES_OPS_YAML_DIR is required for --ops mode")
|
|
}
|
|
docsDir := os.Getenv("NUBES_OPS_DOCS_DIR")
|
|
if docsDir == "" {
|
|
panic("NUBES_OPS_DOCS_DIR is required for --ops mode")
|
|
}
|
|
|
|
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)
|
|
}
|