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("| ID | Code | Type | Description | Constraints |
\n\n")
- } else {
- b.WriteString("| ID | Code | Type | Default | Description | Constraints |
\n\n")
- }
- for _, p := range params {
- b.WriteString("")
- b.WriteString(fmt.Sprintf("| %s | ", escapeText(formatID(p.ID))))
- b.WriteString(fmt.Sprintf("%s | ", formatCode(p.Code)))
- b.WriteString(fmt.Sprintf("%s | ", formatTypeCell(p)))
- if !noDefault {
- b.WriteString(fmt.Sprintf("%s | ", defaultCell(p.Default)))
- }
- b.WriteString(fmt.Sprintf("%s | ", escapeText(pickText(p))))
- b.WriteString(fmt.Sprintf("%s | ", escapeText(collectConstraints(p))))
- b.WriteString("
\n")
- }
- b.WriteString("
\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("| ID | Code | Type | Description | Constraints |
\n\n")
- for _, p := range params {
- b.WriteString("")
- b.WriteString(fmt.Sprintf("| %s | ", escapeText(formatID(p.ID))))
- b.WriteString(fmt.Sprintf("%s | ", formatCode(p.Code)))
- b.WriteString(fmt.Sprintf("%s | ", formatTypeCell(p)))
- b.WriteString(fmt.Sprintf("%s | ", escapeText(pickText(p))))
- b.WriteString(fmt.Sprintf("%s | ", escapeText(collectConstraints(p))))
- b.WriteString("
\n")
- }
- b.WriteString("
\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
-}