diff --git a/.gitignore b/.gitignore index b2c5717..434329c 100644 --- a/.gitignore +++ b/.gitignore @@ -88,3 +88,4 @@ universal_rebuild/service_params_gen # ── Build artifacts ── terraform-provider-mycloud artifacts/api-meta/*/errors.log +TOOLS/bin/ diff --git a/TOOLS/docs-generator/go.mod b/TOOLS/docs-generator/go.mod new file mode 100644 index 0000000..2923b06 --- /dev/null +++ b/TOOLS/docs-generator/go.mod @@ -0,0 +1,5 @@ +module docs-generator + +go 1.24 + +require gopkg.in/yaml.v3 v3.0.1 diff --git a/TOOLS/docs-generator/go.sum b/TOOLS/docs-generator/go.sum new file mode 100644 index 0000000..a62c313 --- /dev/null +++ b/TOOLS/docs-generator/go.sum @@ -0,0 +1,4 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/universal_rebuild/tools/docs_template_gen_v2/main.go b/TOOLS/docs-generator/main.go similarity index 100% rename from universal_rebuild/tools/docs_template_gen_v2/main.go rename to TOOLS/docs-generator/main.go diff --git a/universal_rebuild/tools/ops_docs_gen/generate_ops_docs.go b/TOOLS/ops-generator/generate_ops_docs.go similarity index 100% rename from universal_rebuild/tools/ops_docs_gen/generate_ops_docs.go rename to TOOLS/ops-generator/generate_ops_docs.go diff --git a/TOOLS/ops-generator/go.mod b/TOOLS/ops-generator/go.mod new file mode 100644 index 0000000..2fa9b63 --- /dev/null +++ b/TOOLS/ops-generator/go.mod @@ -0,0 +1,5 @@ +module ops-generator + +go 1.24 + +require gopkg.in/yaml.v3 v3.0.1 diff --git a/TOOLS/ops-generator/go.sum b/TOOLS/ops-generator/go.sum new file mode 100644 index 0000000..a62c313 --- /dev/null +++ b/TOOLS/ops-generator/go.sum @@ -0,0 +1,4 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/TOOLS/resource-generator/go.mod b/TOOLS/resource-generator/go.mod new file mode 100644 index 0000000..36e8443 --- /dev/null +++ b/TOOLS/resource-generator/go.mod @@ -0,0 +1,5 @@ +module resource-generator + +go 1.24 + +require gopkg.in/yaml.v3 v3.0.1 diff --git a/TOOLS/resource-generator/go.sum b/TOOLS/resource-generator/go.sum new file mode 100644 index 0000000..a62c313 --- /dev/null +++ b/TOOLS/resource-generator/go.sum @@ -0,0 +1,4 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/universal_rebuild/tools/gen_v2/helpers/helpers.go b/TOOLS/resource-generator/helpers/helpers.go similarity index 99% rename from universal_rebuild/tools/gen_v2/helpers/helpers.go rename to TOOLS/resource-generator/helpers/helpers.go index 28d1991..0a12819 100644 --- a/universal_rebuild/tools/gen_v2/helpers/helpers.go +++ b/TOOLS/resource-generator/helpers/helpers.go @@ -8,7 +8,7 @@ import ( "strings" "unicode" - "terraform-provider-nubes/tools/gen_v2/types" + "resource-generator/types" ) // ToCamel конвертирует snake_case → CamelCase. diff --git a/universal_rebuild/tools/gen_v2/loader/loader.go b/TOOLS/resource-generator/loader/loader.go similarity index 99% rename from universal_rebuild/tools/gen_v2/loader/loader.go rename to TOOLS/resource-generator/loader/loader.go index 0d65c15..771b64c 100644 --- a/universal_rebuild/tools/gen_v2/loader/loader.go +++ b/TOOLS/resource-generator/loader/loader.go @@ -11,8 +11,8 @@ import ( "gopkg.in/yaml.v3" - "terraform-provider-nubes/tools/gen_v2/params" - "terraform-provider-nubes/tools/gen_v2/types" + "resource-generator/params" + "resource-generator/types" ) // LoadSpecs загружает все YAML-спеки из директории и строит GenResource/GenSubresource/GenAction. diff --git a/universal_rebuild/tools/gen_v2/main.go b/TOOLS/resource-generator/main.go similarity index 95% rename from universal_rebuild/tools/gen_v2/main.go rename to TOOLS/resource-generator/main.go index 9791b91..e247b37 100644 --- a/universal_rebuild/tools/gen_v2/main.go +++ b/TOOLS/resource-generator/main.go @@ -20,8 +20,8 @@ import ( "path/filepath" "strings" - "terraform-provider-nubes/tools/gen_v2/loader" - "terraform-provider-nubes/tools/gen_v2/writers" + "resource-generator/loader" + "resource-generator/writers" ) func main() { diff --git a/universal_rebuild/tools/gen_v2/params/params.go b/TOOLS/resource-generator/params/params.go similarity index 99% rename from universal_rebuild/tools/gen_v2/params/params.go rename to TOOLS/resource-generator/params/params.go index 97c2e24..4974bfb 100644 --- a/universal_rebuild/tools/gen_v2/params/params.go +++ b/TOOLS/resource-generator/params/params.go @@ -5,7 +5,7 @@ import ( "sort" "strings" - "terraform-provider-nubes/tools/gen_v2/types" + "resource-generator/types" ) // Merge объединяет несколько наборов параметров, убирая дубликаты по Code. diff --git a/universal_rebuild/tools/gen_v2/templates/templates.go b/TOOLS/resource-generator/templates/templates.go similarity index 100% rename from universal_rebuild/tools/gen_v2/templates/templates.go rename to TOOLS/resource-generator/templates/templates.go diff --git a/universal_rebuild/tools/gen_v2/types/types.go b/TOOLS/resource-generator/types/types.go similarity index 100% rename from universal_rebuild/tools/gen_v2/types/types.go rename to TOOLS/resource-generator/types/types.go diff --git a/universal_rebuild/tools/gen_v2/writers/writers.go b/TOOLS/resource-generator/writers/writers.go similarity index 96% rename from universal_rebuild/tools/gen_v2/writers/writers.go rename to TOOLS/resource-generator/writers/writers.go index 77ecf70..21713c5 100644 --- a/universal_rebuild/tools/gen_v2/writers/writers.go +++ b/TOOLS/resource-generator/writers/writers.go @@ -8,9 +8,9 @@ import ( "path/filepath" "text/template" - "terraform-provider-nubes/tools/gen_v2/helpers" - "terraform-provider-nubes/tools/gen_v2/templates" - "terraform-provider-nubes/tools/gen_v2/types" + "resource-generator/helpers" + "resource-generator/templates" + "resource-generator/types" ) // WriteInstanceResource генерирует nubes_{service} (CRUD инстанса). diff --git a/universal_rebuild/tools/service_spec_gen/client/client.go b/TOOLS/yaml-generator/client/client.go similarity index 98% rename from universal_rebuild/tools/service_spec_gen/client/client.go rename to TOOLS/yaml-generator/client/client.go index 4b91479..a505886 100644 --- a/universal_rebuild/tools/service_spec_gen/client/client.go +++ b/TOOLS/yaml-generator/client/client.go @@ -10,8 +10,8 @@ import ( "strings" "time" - "terraform-provider-nubes/tools/service_spec_gen/normalize" - "terraform-provider-nubes/tools/service_spec_gen/types" + "yaml-generator/normalize" + "yaml-generator/types" ) // Client — HTTP-клиент для Nubes API. diff --git a/universal_rebuild/tools/service_spec_gen/config/config.go b/TOOLS/yaml-generator/config/config.go similarity index 98% rename from universal_rebuild/tools/service_spec_gen/config/config.go rename to TOOLS/yaml-generator/config/config.go index 4182cdb..8baa04e 100644 --- a/universal_rebuild/tools/service_spec_gen/config/config.go +++ b/TOOLS/yaml-generator/config/config.go @@ -9,7 +9,7 @@ import ( "strconv" "strings" - "terraform-provider-nubes/tools/service_spec_gen/normalize" + "yaml-generator/normalize" ) // Config — конфигурация генератора. diff --git a/TOOLS/yaml-generator/go.mod b/TOOLS/yaml-generator/go.mod new file mode 100644 index 0000000..389413a --- /dev/null +++ b/TOOLS/yaml-generator/go.mod @@ -0,0 +1,5 @@ +module yaml-generator + +go 1.24 + +require gopkg.in/yaml.v3 v3.0.1 diff --git a/TOOLS/yaml-generator/go.sum b/TOOLS/yaml-generator/go.sum new file mode 100644 index 0000000..a62c313 --- /dev/null +++ b/TOOLS/yaml-generator/go.sum @@ -0,0 +1,4 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/universal_rebuild/tools/service_spec_gen/main.go b/TOOLS/yaml-generator/main.go similarity index 89% rename from universal_rebuild/tools/service_spec_gen/main.go rename to TOOLS/yaml-generator/main.go index 473658a..f254633 100644 --- a/universal_rebuild/tools/service_spec_gen/main.go +++ b/TOOLS/yaml-generator/main.go @@ -12,11 +12,11 @@ import ( "gopkg.in/yaml.v3" - "terraform-provider-nubes/tools/service_spec_gen/client" - "terraform-provider-nubes/tools/service_spec_gen/config" - "terraform-provider-nubes/tools/service_spec_gen/normalize" - "terraform-provider-nubes/tools/service_spec_gen/spec" - "terraform-provider-nubes/tools/service_spec_gen/types" + "yaml-generator/client" + "yaml-generator/config" + "yaml-generator/normalize" + "yaml-generator/spec" + "yaml-generator/types" ) func main() { diff --git a/universal_rebuild/tools/service_spec_gen/normalize/normalize.go b/TOOLS/yaml-generator/normalize/normalize.go similarity index 100% rename from universal_rebuild/tools/service_spec_gen/normalize/normalize.go rename to TOOLS/yaml-generator/normalize/normalize.go diff --git a/universal_rebuild/tools/service_spec_gen/spec/spec.go b/TOOLS/yaml-generator/spec/spec.go similarity index 94% rename from universal_rebuild/tools/service_spec_gen/spec/spec.go rename to TOOLS/yaml-generator/spec/spec.go index 4327dd7..193dc7d 100644 --- a/universal_rebuild/tools/service_spec_gen/spec/spec.go +++ b/TOOLS/yaml-generator/spec/spec.go @@ -6,7 +6,7 @@ import ( "path/filepath" "strings" - "terraform-provider-nubes/tools/service_spec_gen/types" + "yaml-generator/types" ) // DefaultOutputParams возвращает стандартный набор выходных параметров. diff --git a/universal_rebuild/tools/service_spec_gen/types/types.go b/TOOLS/yaml-generator/types/types.go similarity index 100% rename from universal_rebuild/tools/service_spec_gen/types/types.go rename to TOOLS/yaml-generator/types/types.go diff --git a/devops/01_generate_yamls.sh b/devops/01_generate_yamls.sh index 41a1d43..7712569 100755 --- a/devops/01_generate_yamls.sh +++ b/devops/01_generate_yamls.sh @@ -138,9 +138,9 @@ fi rm -f "$FAILURES_FILE" # Build service_spec_gen binary if missing. -if [[ ! -x "${PROVIDER_DIR}/bin/service_spec_gen" ]]; then +if [[ ! -x "${ROOT_DIR}/TOOLS/bin/yaml-generator" ]]; then echo "Building service_spec_gen..." - (cd "$PROVIDER_DIR" && go build -o bin/service_spec_gen ./tools/service_spec_gen/) + # Binary pre-built in TOOLS/bin/yaml-generator fi # Проверка что список сервисов не пустой @@ -235,7 +235,7 @@ PY NUBES_SERVICE_NAME="$svc_name" \ NUBES_API_ENDPOINT="$API_ENDPOINT" \ NUBES_OUTPUT_DIR="$YAML_OUTPUT_DIR" \ - "${PROVIDER_DIR}/bin/service_spec_gen" + "${ROOT_DIR}/TOOLS/bin/yaml-generator" ) && success=1 || success=0 if [[ "$success" -eq 1 ]]; then diff --git a/devops/02_generate_resources_and_docs.sh b/devops/02_generate_resources_and_docs.sh index 7afcece..f3ded27 100755 --- a/devops/02_generate_resources_and_docs.sh +++ b/devops/02_generate_resources_and_docs.sh @@ -15,7 +15,7 @@ rm -f "${PROVIDER_DIR}/internal/resources_gen/registry.go" # Step 1: generate Go resources from unified YAML. echo "Generating Go resources from unified YAML..." -go run ./tools/gen_v2 +${ROOT_DIR}/TOOLS/bin/resource-generator # Step 2: generate documentation from unified YAML (new template). echo "Generating documentation using new template generator..." diff --git a/devops/02_generate_resources_and_docs_template.sh b/devops/02_generate_resources_and_docs_template.sh index 4be385b..777600a 100755 --- a/devops/02_generate_resources_and_docs_template.sh +++ b/devops/02_generate_resources_and_docs_template.sh @@ -13,7 +13,7 @@ SERVICES_LIST_PATH="${ROOT_DIR}/devops/config/services_list.txt" cd "$PROVIDER_DIR" -go run ./tools/docs_template_gen \ +${ROOT_DIR}/TOOLS/bin/docs-generator \ -resources "$RESOURCES_YAML_DIR" \ -docs "$DOCS_DIR" \ -services "$SERVICES_LIST_PATH" \ diff --git a/devops/02_generate_resources_and_docs_template_v2.sh b/devops/02_generate_resources_and_docs_template_v2.sh index c5e4f28..26d19fe 100755 --- a/devops/02_generate_resources_and_docs_template_v2.sh +++ b/devops/02_generate_resources_and_docs_template_v2.sh @@ -13,7 +13,7 @@ SERVICES_LIST_PATH="${ROOT_DIR}/devops/config/services_list.txt" cd "$PROVIDER_DIR" -go run ./tools/docs_template_gen_v2 \ +${ROOT_DIR}/TOOLS/bin/docs-generator \ -resources "$RESOURCES_YAML_DIR" \ -docs "$DOCS_DIR" \ -services "$SERVICES_LIST_PATH" \ diff --git a/devops/02_generate_resources_and_docs_v2.sh b/devops/02_generate_resources_and_docs_v2.sh index 94046e9..2a0c2c6 100755 --- a/devops/02_generate_resources_and_docs_v2.sh +++ b/devops/02_generate_resources_and_docs_v2.sh @@ -81,7 +81,7 @@ trap 'rm -rf "$TMP_GEN_DIR"' EXIT NUBES_RESOURCES_DIR="$RESOURCES_YAML_DIR" \ NUBES_RESOURCES_GEN_DIR="$TMP_GEN_DIR" \ -go run ./tools/gen_v2 +${ROOT_DIR}/TOOLS/bin/resource-generator mkdir -p "$GO_OUTPUT_DIR" rm -rf "${GO_OUTPUT_DIR}"/* @@ -99,7 +99,7 @@ if [[ "$DOCS_API_ENDPOINT" != *"/index.cfm"* ]] && [[ "$DOCS_API_ENDPOINT" != *" DOCS_API_ENDPOINT="${DOCS_API_ENDPOINT%/}/index.cfm" fi -go run ./tools/docs_template_gen_v2 \ +${ROOT_DIR}/TOOLS/bin/docs-generator \ -resources "$RESOURCES_YAML_DIR" \ -docs "$DOCS_DIR" \ -services "$SERVICES_LIST_PATH" \ diff --git a/universal_rebuild/tools/docs_template_gen/main.go b/universal_rebuild/tools/docs_template_gen/main.go deleted file mode 100644 index 118b7da..0000000 --- a/universal_rebuild/tools/docs_template_gen/main.go +++ /dev/null @@ -1,746 +0,0 @@ -package main - -import ( - "bytes" - "flag" - "fmt" - "html" - "io/fs" - "os" - "path/filepath" - "regexp" - "sort" - "strconv" - "strings" - - "gopkg.in/yaml.v3" -) - -type ParamSpec struct { - ID int `yaml:"id"` - Code string `yaml:"code"` - DataType string `yaml:"data_type"` - Type string `yaml:"type"` - Required bool `yaml:"required"` - Default interface{} `yaml:"default"` - Descr string `yaml:"descr"` - Man string `yaml:"man"` - RefSvcID *int `yaml:"ref_svc_id"` - Func string `yaml:"func"` - MinValue *int `yaml:"minvalue"` - MaxValue *int `yaml:"maxvalue"` - Regex string `yaml:"regex"` - ValueList []string `yaml:"value_list"` - Unique string `yaml:"unique_scope"` - MaxLength *int `yaml:"maxlength"` - MinLength *int `yaml:"minlength"` -} - -type OperationSpec struct { - Name string `yaml:"name"` - ID int `yaml:"id"` - Kind string `yaml:"kind"` - Action string `yaml:"action"` - Subresource string `yaml:"subresource"` - Man string `yaml:"man"` - Params []ParamSpec `yaml:"params"` -} - -type OutputParam struct { - Code string `yaml:"code"` - Type string `yaml:"type"` - Sensitive bool `yaml:"sensitive"` -} - -type ServiceSpec struct { - Name string `yaml:"name"` - ServiceID int `yaml:"service_id"` - ServiceDisplayName string `yaml:"service_display_name"` - ServiceMan string `yaml:"service_man"` - Lifecycle struct { - DeleteModeDefault string `yaml:"delete_mode_default"` - ResumeIfExistsDefault bool `yaml:"resume_if_exists_default"` - } `yaml:"lifecycle"` - Outputs struct { - Params []OutputParam `yaml:"params"` - } `yaml:"outputs"` - Operations []OperationSpec `yaml:"operations"` -} - -type ServiceMeta struct { - ID int - Name string -} - -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", "clickhouse", "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") - flag.Parse() - - root := detectRoot() - resourcesDir := pickPath(*resourcesDirFlag, filepath.Join(root, "universal_rebuild", "resources_yaml")) - docsDir := pickPath(*docsDirFlag, filepath.Join(root, "docs", "30_registry", "resources")) - servicesList := pickPath(*servicesListFlag, filepath.Join(root, "devops", "config", "services_list.txt")) - excludeSet := toSet(*excludeFlag) - version := *versionFlag - if version == "" { - version = detectVersion(filepath.Join(root, "universal_rebuild", "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 { - panic(err) - } - - for _, spec := range specs { - if excludeSet[spec.Name] { - continue - } - writeResourceDocs(docsDir, spec, version, apiEndpoint) - } -} - -func detectRoot() string { - cwd, err := os.Getwd() - if err != nil { - panic(err) - } - if filepath.Base(cwd) == "universal_rebuild" { - 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) []ServiceMeta { - b, err := os.ReadFile(path) - if err != nil { - return nil - } - lines := strings.Split(string(b), "\n") - out := []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, ServiceMeta{ID: id, Name: parts[1]}) - } - return out -} - -func loadSpecs(dir string, ordered []ServiceMeta) []ServiceSpec { - specsByID := map[int]ServiceSpec{} - walkErr := 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 ServiceSpec - if err := yaml.Unmarshal(b, &spec); err != nil { - return err - } - specsByID[spec.ServiceID] = spec - return nil - }) - if walkErr != nil { - panic(walkErr) - } - - if len(ordered) == 0 { - specs := make([]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 := []ServiceSpec{} - for _, meta := range ordered { - spec, ok := specsByID[meta.ID] - if !ok { - continue - } - specs = append(specs, spec) - } - return specs -} - -func writeResourceDocs(docsDir string, spec ServiceSpec, version string, apiEndpoint string) { - base := spec.Name - nav := fmt.Sprintf("[Manual](%s.md) | [Create params](%s_params_create.md) | [Modify params](%s_params_modify.md) | [Output params](%s_outputs.md) | [Operations](%s_ops.md) | [Example](%s_example.md)", base, base, base, base, base, base) - - writeFile(filepath.Join(docsDir, base+".md"), buildManualPage(spec, nav)) - writeFile(filepath.Join(docsDir, base+"_example.md"), buildExamplePage(spec, nav, version, apiEndpoint)) - writeFile(filepath.Join(docsDir, base+"_params_create.md"), buildCreateParamsPage(spec, nav)) - writeFile(filepath.Join(docsDir, base+"_params_modify.md"), buildModifyParamsPage(spec, nav)) - writeFile(filepath.Join(docsDir, base+"_outputs.md"), buildOutputsPage(spec, nav)) - writeFile(filepath.Join(docsDir, base+"_ops.md"), buildOpsPage(spec, nav, docsDir)) - writeFile(filepath.Join(docsDir, base+"_params.md"), buildParamsLandingPage(spec, nav)) -} - -func writeFile(path, content string) { - if err := os.WriteFile(path, []byte(content), 0o644); err != nil { - panic(err) - } -} - -func buildHeader(spec ServiceSpec, nav string) string { - name := spec.ServiceDisplayName - if name == "" { - name = spec.Name - } - return fmt.Sprintf("# Resource nubes_%s\n\nService ID: `%d`\n\nService Name: %s\n\n%s\n\n", spec.Name, spec.ServiceID, name, nav) -} - -func buildManualPage(spec ServiceSpec, nav string) string { - var b strings.Builder - b.WriteString(buildHeader(spec, nav)) - b.WriteString("## MAN\n\n") - man := strings.TrimSpace(spec.ServiceMan) - if man == "" { - man = "Manual not available." - } - b.WriteString("
\n") - b.WriteString(man) - b.WriteString("\n
\n") - return b.String() -} - -func buildExamplePage(spec ServiceSpec, nav, version string, apiEndpoint string) string { - var b strings.Builder - b.WriteString(buildHeader(spec, nav)) - b.WriteString(fmt.Sprintf("## Copy-ready manifest (`%s_main.tf`)\n\n", spec.Name)) - b.WriteString("
")
-	b.WriteString(exampleBlock(spec, version, apiEndpoint))
-	b.WriteString("
\n\n") - b.WriteString("## Outputs usage\n\n") - b.WriteString("
")
-	b.WriteString("# Выходные параметры можно использовать как\n")
-	b.WriteString(fmt.Sprintf("nubes_%s.baza.state_params[\"имя_ключа\"]\n", spec.Name))
-	b.WriteString(fmt.Sprintf("nubes_%s.baza.state_out[\"имя_ключа\"]\n", spec.Name))
-	b.WriteString(fmt.Sprintf("nubes_%s.baza.state_params_flat[\"имя_ключа\"]\n", spec.Name))
-	b.WriteString(fmt.Sprintf("nubes_%s.baza.state_out_flat[\"имя_ключа\"]\n", spec.Name))
-	b.WriteString(fmt.Sprintf("nubes_%s.baza.vault_secrets[\"имя_ключа\"]\n", spec.Name))
-	b.WriteString(fmt.Sprintf("nubes_%s.baza.vault_url\n", spec.Name))
-	b.WriteString(fmt.Sprintf("nubes_%s.baza.vault_user_path\n", spec.Name))
-	b.WriteString(fmt.Sprintf("nubes_%s.baza.vault_fields\n", spec.Name))
-	b.WriteString("
\n") - return b.String() -} - -func exampleBlock(spec ServiceSpec, version string, apiEndpoint string) string { - createParams := findParams(spec.Operations, "create") - requiredParams, defaultParams := splitParams(createParams) - - var b strings.Builder - b.WriteString("terraform {\n") - b.WriteString(" required_providers {\n") - b.WriteString(" nubes = {\n") - b.WriteString(" source = \"terra.k8c.ru/nubes/nubes\"\n") - b.WriteString(fmt.Sprintf(" version = \"%s\"\n", version)) - b.WriteString(" }\n") - b.WriteString(" }\n") - b.WriteString("}\n\n") - b.WriteString("# Токен доступа api_token к Nubes API — замените на реальный.\n") - b.WriteString("provider \"nubes\" {\n") - b.WriteString(" api_token = \"token_***\"\n") - b.WriteString(fmt.Sprintf(" api_endpoint = \"%s\"\n", apiEndpoint)) - b.WriteString("}\n") - b.WriteString(fmt.Sprintf("resource \"nubes_%s\" \"baza\" {\n", spec.Name)) - - if len(requiredParams) > 0 { - b.WriteString(" # TODO - обязательный параметр, задаваемый пользователем\n") - } - for _, p := range requiredParams { - b.WriteString(formatParamLine(p, " ")) - } - - if len(defaultParams) > 0 { - b.WriteString("\n # Параметры, имеющие значение по умолчанию, если не меняете - эти параметры не обязательно прописывать в манифесте\n") - for _, p := range defaultParams { - b.WriteString(formatParamLine(p, " ")) - } - } - - b.WriteString("}\n") - return escapeCode(b.String()) -} - -func buildCreateParamsPage(spec ServiceSpec, nav string) string { - var b strings.Builder - b.WriteString(buildHeader(spec, nav)) - b.WriteString("## Create params\n\n") - createParams := findParams(spec.Operations, "create") - requiredParams, defaultParams := splitParams(createParams) - - b.WriteString("**Обязательные параметры (вводимые пользователем)**\n\n") - b.WriteString(renderParamTable(requiredParams, true, true)) - - b.WriteString("\n**Параметры, имеющие значение по умолчанию, если не меняете - эти параметры не обязательно прописывать в манифесте**\n") - b.WriteString(renderParamTable(defaultParams, false, false)) - - lifecycle := spec.Lifecycle - if lifecycle.DeleteModeDefault != "" || lifecycle.ResumeIfExistsDefault { - b.WriteString("\n
\n") - b.WriteString("Параметры жизненного цикла (Lifecycle defaults)
\n") - b.WriteString(fmt.Sprintf("По умолчанию: `delete_mode_default = %s`, `resume_if_exists_default = %v`.
\n", lifecycle.DeleteModeDefault, lifecycle.ResumeIfExistsDefault)) - b.WriteString("Если для сервиса доступны операции `suspend` и `resume`, то `delete` выполняет приостановку (без удаления данных),
\n") - b.WriteString("а `apply` при необходимости возобновляет работу существующего ресурса.
\n") - b.WriteString("Менять эти значения стоит только в продвинутых сценариях — через параметры `delete_mode` и `resume_if_exists` в манифесте.\n") - b.WriteString("
\n") - } - - return b.String() -} - -func buildModifyParamsPage(spec ServiceSpec, nav string) string { - var b strings.Builder - b.WriteString(buildHeader(spec, nav)) - b.WriteString("## Modify params\n\n") - modifyParams := findParams(spec.Operations, "modify") - b.WriteString(renderModifyTable(modifyParams)) - return b.String() -} - -func buildOutputsPage(spec ServiceSpec, nav string) string { - var b strings.Builder - b.WriteString(buildHeader(spec, nav)) - b.WriteString("## Output params\n\n") - if len(spec.Outputs.Params) == 0 { - b.WriteString("None.\n") - return b.String() - } - for _, p := range spec.Outputs.Params { - b.WriteString(fmt.Sprintf("- `%s` (%s)\n", p.Code, p.Type)) - } - return b.String() -} - -func buildOpsPage(spec ServiceSpec, nav, docsDir string) string { - var b strings.Builder - b.WriteString(buildHeader(spec, nav)) - b.WriteString("## Operations\n\n") - for _, op := range spec.Operations { - name := op.Name - if name == "" { - name = op.Action - } - descr := strings.TrimSpace(stripHTML(op.Man)) - if descr != "" { - descr = ": " + descr - } - switch op.Action { - case "create": - b.WriteString(fmt.Sprintf("- `%s` — Создание кластера. Параметры: [%s params](%s_params_create.md)\n", name, "Create", spec.Name)) - continue - case "modify": - b.WriteString(fmt.Sprintf("- `%s` — Изменение кластера. Параметры: [%s params](%s_params_modify.md)\n", name, "Modify", spec.Name)) - continue - } - - if op.Kind == "subresource" && op.Subresource != "" { - subFile := fmt.Sprintf("%s_%s.md", spec.Name, slug(op.Subresource)) - if fileExists(filepath.Join(docsDir, subFile)) { - b.WriteString(fmt.Sprintf("- `%s` — %s. См. [nubes_%s_%s](%s)\n", name, strings.TrimPrefix(descr, ": "), spec.Name, slug(op.Subresource), subFile)) - continue - } - } - if descr != "" { - b.WriteString(fmt.Sprintf("- `%s` — %s\n", name, strings.TrimPrefix(descr, ": "))) - } else { - b.WriteString(fmt.Sprintf("- `%s`\n", name)) - } - } - return b.String() -} - -func buildParamsLandingPage(spec ServiceSpec, nav string) string { - var b strings.Builder - b.WriteString(buildHeader(spec, nav)) - b.WriteString("## Parameters\n\n") - b.WriteString(fmt.Sprintf("- [Create params](%s_params_create.md)\n", spec.Name)) - b.WriteString(fmt.Sprintf("- [Modify params](%s_params_modify.md)\n", spec.Name)) - return b.String() -} - -func findParams(ops []OperationSpec, action string) []ParamSpec { - for _, op := range ops { - if op.Kind == "instance" && op.Action == action { - return op.Params - } - } - return nil -} - -func splitParams(params []ParamSpec) ([]ParamSpec, []ParamSpec) { - required := []ParamSpec{} - defaults := []ParamSpec{} - for _, p := range params { - if hasDefault(p.Default) { - defaults = append(defaults, p) - continue - } - required = append(required, p) - } - return required, defaults -} - -func hasDefault(value interface{}) bool { - if value == nil { - return false - } - if s, ok := value.(string); ok { - return strings.TrimSpace(s) != "" - } - return true -} - -func renderParamTable(params []ParamSpec, requiredTable, noDefault bool) string { - if len(params) == 0 { - return "None.\n" - } - var b strings.Builder - tableClass := "resource-table resource-table-compact" - if requiredTable { - tableClass += " resource-table-required" - } - b.WriteString(fmt.Sprintf("\n", tableClass)) - if noDefault { - b.WriteString("\n\n") - } else { - b.WriteString("\n\n") - } - for _, p := range params { - b.WriteString("") - b.WriteString(fmt.Sprintf("", escapeText(formatID(p.ID)))) - b.WriteString(fmt.Sprintf("", formatCode(p.Code))) - b.WriteString(fmt.Sprintf("", formatTypeCell(p))) - if !noDefault { - b.WriteString(fmt.Sprintf("", defaultCell(p.Default))) - } - b.WriteString(fmt.Sprintf("", escapeText(pickText(p)))) - b.WriteString(fmt.Sprintf("", escapeText(collectConstraints(p)))) - b.WriteString("\n") - } - b.WriteString("
IDCodeTypeDescriptionConstraints
IDCodeTypeDefaultDescriptionConstraints
%s%s%s%s%s%s
\n") - return b.String() -} - -func renderModifyTable(params []ParamSpec) string { - if len(params) == 0 { - return "None.\n" - } - var b strings.Builder - b.WriteString("\n") - b.WriteString("\n\n") - for _, p := range params { - b.WriteString("") - b.WriteString(fmt.Sprintf("", escapeText(formatID(p.ID)))) - b.WriteString(fmt.Sprintf("", formatCode(p.Code))) - b.WriteString(fmt.Sprintf("", formatTypeCell(p))) - b.WriteString(fmt.Sprintf("", escapeText(pickText(p)))) - b.WriteString(fmt.Sprintf("", escapeText(collectConstraints(p)))) - b.WriteString("\n") - } - b.WriteString("
IDCodeTypeDescriptionConstraints
%s%s%s%s%s
\n") - return b.String() -} - -func formatParamLine(p ParamSpec, indent string) string { - value := sampleValue(p) - comment := strings.TrimSpace(stripHTML(p.Man)) - if comment == "" { - comment = strings.TrimSpace(stripHTML(p.Descr)) - } - if comment != "" { - return fmt.Sprintf("%s%s = %s # %s\n", indent, p.Code, value, comment) - } - return fmt.Sprintf("%s%s = %s\n", indent, p.Code, value) -} - -func sampleValue(p ParamSpec) string { - if hasDefault(p.Default) { - return formatLiteral(p.Default) - } - if len(p.ValueList) > 0 { - return formatLiteral(p.ValueList[0]) - } - dtype := strings.ToLower(firstType(p)) - switch { - case strings.Contains(dtype, "bool"): - return "false" - case strings.Contains(dtype, "int") || strings.Contains(dtype, "number"): - return "1" - case strings.Contains(dtype, "json"): - return "{}" - } - return "\"TODO\"" -} - -func formatLiteral(value interface{}) string { - switch v := value.(type) { - case bool: - if v { - return "true" - } - return "false" - case int: - return strconv.Itoa(v) - case int64: - return strconv.FormatInt(v, 10) - case float64: - return strconv.FormatFloat(v, 'f', -1, 64) - case string: - if strings.HasPrefix(v, "{") || strings.HasPrefix(v, "[") { - return v - } - if strings.HasPrefix(v, "\"") && strings.HasSuffix(v, "\"") { - return v - } - if looksLikeNumber(v) || v == "true" || v == "false" { - return v - } - return fmt.Sprintf("\"%s\"", v) - default: - return fmt.Sprintf("\"%v\"", v) - } -} - -func looksLikeNumber(value string) bool { - if value == "" { - return false - } - if value[0] == '-' { - value = value[1:] - } - for _, ch := range value { - if ch == '.' { - continue - } - if ch < '0' || ch > '9' { - return false - } - } - return true -} - -func formatID(id int) string { - if id == 0 { - return "" - } - return strconv.Itoa(id) -} - -func formatTypeCell(p ParamSpec) string { - dtype := firstType(p) - if dtype == "" { - return "" - } - return "" + escapeText(dtype) + "" -} - -func firstType(p ParamSpec) string { - if p.DataType != "" { - return normalizeType(p.DataType) - } - return normalizeType(p.Type) -} - -func normalizeType(dtype string) string { - cleaned := strings.ReplaceAll(dtype, ">", ">") - if idx := strings.Index(cleaned, ">"); idx >= 0 { - cleaned = cleaned[:idx] - } - return strings.TrimSpace(cleaned) -} - -func defaultCell(value interface{}) string { - if !hasDefault(value) { - return "" - } - return "" + escapeText(formatLiteral(value)) + "" -} - -func pickText(p ParamSpec) string { - if p.Descr == "" && p.Man == "" { - return "" - } - if p.Man == "" { - return p.Descr - } - if p.Descr == "" { - return p.Man - } - if len(p.Man) > len(p.Descr) { - return p.Man - } - return p.Descr -} - -func collectConstraints(p ParamSpec) string { - parts := []string{} - if p.MinValue != nil { - parts = append(parts, fmt.Sprintf("minvalue=%d", *p.MinValue)) - } - if p.MaxValue != nil { - parts = append(parts, fmt.Sprintf("maxvalue=%d", *p.MaxValue)) - } - if p.Regex != "" { - parts = append(parts, fmt.Sprintf("regex=%s", p.Regex)) - } - if len(p.ValueList) > 0 { - parts = append(parts, fmt.Sprintf("value_list=%s", strings.Join(p.ValueList, ", "))) - } - if p.Func != "" { - parts = append(parts, fmt.Sprintf("func=%s", p.Func)) - } - if p.RefSvcID != nil { - parts = append(parts, fmt.Sprintf("ref_svc_id=%d", *p.RefSvcID)) - } - if p.Unique != "" { - parts = append(parts, fmt.Sprintf("unique_scope=%s", p.Unique)) - } - if p.MaxLength != nil { - parts = append(parts, fmt.Sprintf("maxlength=%d", *p.MaxLength)) - } - if p.MinLength != nil { - parts = append(parts, fmt.Sprintf("minlength=%d", *p.MinLength)) - } - return strings.Join(parts, "; ") -} - -func escapeText(value string) string { - if value == "" { - return "" - } - text := html.EscapeString(value) - text = strings.ReplaceAll(text, "<br/>", "
") - text = strings.ReplaceAll(text, "<br />", "
") - text = strings.ReplaceAll(text, "<br>", "
") - return text -} - -func escapeCode(value string) string { - buf := bytes.NewBuffer(nil) - for _, line := range strings.Split(value, "\n") { - buf.WriteString(html.EscapeString(line)) - buf.WriteString("\n") - } - return buf.String() -} - -func formatCode(code string) string { - if code == "" { - return "" - } - if len(code) <= 40 { - return "" + code + "" - } - out := []string{} - prev := rune(0) - for _, ch := range code { - if ch == '_' { - out = append(out, "_") - prev = ch - continue - } - if prev != 0 && (unicodeIsLower(prev) || unicodeIsDigit(prev)) && unicodeIsUpper(ch) { - out = append(out, "") - } - out = append(out, string(ch)) - prev = ch - } - return "" + strings.Join(out, "") + "" -} - -func unicodeIsLower(r rune) bool { return r >= 'a' && r <= 'z' } -func unicodeIsUpper(r rune) bool { return r >= 'A' && r <= 'Z' } -func unicodeIsDigit(r rune) bool { return r >= '0' && r <= '9' } - -func stripHTML(value string) string { - if value == "" { - return "" - } - re := regexp.MustCompile("<[^>]+>") - cleaned := re.ReplaceAllString(value, " ") - cleaned = strings.Join(strings.Fields(cleaned), " ") - return html.UnescapeString(cleaned) -} - -func slug(value string) string { - out := []rune{} - for _, ch := range value { - if (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '_' || ch == '-' { - out = append(out, ch) - } else if ch >= 'A' && ch <= 'Z' { - out = append(out, ch+'a'-'A') - } else { - out = append(out, '_') - } - } - return strings.Trim(string(out), "_") -} - -func fileExists(path string) bool { - _, err := os.Stat(path) - return err == nil -}