add: generator tools (sources)

This commit is contained in:
“Naeel”
2026-06-30 15:44:56 +04:00
parent d2c4ad2979
commit aac18970ae
9 changed files with 7022 additions and 0 deletions
@@ -0,0 +1,523 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"gopkg.in/yaml.v3"
)
// service_params_gen: builds resource YAML from /index.cfm?endpoint=/services/{svcId}
// and /index.cfm?endpoint=/serviceOperation/{svcOperationId}.
//
// Env:
// - NUBES_API_TOKEN (or read from test_universal/terraform.tfvars)
// - NUBES_API_ENDPOINT (default: https://deck-api.ngcloud.ru/api/v1/index.cfm)
// - NUBES_SERVICE_ID (required)
// - NUBES_SERVICE_NAME (optional override for YAML name)
// - NUBES_OUTPUT (optional output path)
func main() {
cfg, err := loadConfig()
if err != nil {
panic(err)
}
repoRoot, err := findRepoRoot()
if err != nil {
panic(err)
}
extraDir := filepath.Join(repoRoot, "extra-params")
if err := validateExtraParams(extraDir); err != nil {
fmt.Fprintf(os.Stderr, "extra-params validation failed: %v\n", err)
os.Exit(1)
}
client := &apiClient{
endpoint: cfg.ApiEndpoint,
token: cfg.ApiToken,
}
service, err := client.getService(cfg.ServiceID)
if err != nil {
panic(err)
}
name := cfg.ServiceName
if name == "" {
name = service.Name
}
if name == "" {
name = fmt.Sprintf("service_%d", cfg.ServiceID)
}
ops, createMan, hasSuspend, err := client.collectOperations(service.Operations)
if err != nil {
panic(err)
}
suspendOnDestroyDefault := hasSuspend
spec := ResourceSpec{
Name: name,
ServiceID: cfg.ServiceID,
Create: Operation{Params: ops["create"]},
Modify: Operation{Params: ops["modify"]},
Outputs: OutputSection{Params: defaultOutputParams()},
Lifecycle: Lifecycle{SuspendOnDestroyDefault: suspendOnDestroyDefault, AdoptExistingOnCreateDefault: false},
}
out, err := yaml.Marshal(spec)
if err != nil {
panic(err)
}
if cfg.OutputPath != "" {
if err := os.WriteFile(cfg.OutputPath, out, 0o644); err != nil {
panic(err)
}
fmt.Printf("written %s\n", cfg.OutputPath)
if err := writeInstruction(cfg.InstructionPath, name, service.Man, createMan); err != nil {
panic(err)
}
return
}
fmt.Print(string(out))
}
func writeInstruction(path string, name string, serviceMan string, createMan string) error {
man := strings.TrimSpace(serviceMan)
if man == "" {
man = strings.TrimSpace(createMan)
}
if man == "" {
return nil
}
if path == "" {
return nil
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
content := fmt.Sprintf("# Инструкция для %s\n\n%s\n", name, man)
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
return err
}
fmt.Printf("written %s\n", path)
return nil
}
// ===== config =====
type config struct {
ApiEndpoint string
ApiToken string
ServiceID int
ServiceName string
OutputPath string
InstructionPath string
}
func loadConfig() (config, error) {
apiEndpoint := getenvDefault("NUBES_API_ENDPOINT", "https://deck-api.ngcloud.ru/api/v1/index.cfm")
apiToken := strings.TrimSpace(os.Getenv("NUBES_API_TOKEN"))
if apiToken == "" {
apiToken = strings.TrimSpace(readTokenFromTfvars("/home/naeel/terra/universal_rebuild/test_universal/terraform.tfvars"))
}
if apiToken == "" {
return config{}, errors.New("NUBES_API_TOKEN is required")
}
serviceIDStr := strings.TrimSpace(os.Getenv("NUBES_SERVICE_ID"))
if serviceIDStr == "" {
return config{}, errors.New("NUBES_SERVICE_ID is required")
}
serviceID, err := strconv.Atoi(serviceIDStr)
if err != nil {
return config{}, fmt.Errorf("invalid NUBES_SERVICE_ID: %s", serviceIDStr)
}
name := strings.TrimSpace(os.Getenv("NUBES_SERVICE_NAME"))
output := strings.TrimSpace(os.Getenv("NUBES_OUTPUT"))
if output == "" {
output = filepath.Join("/home/naeel/terra/universal_rebuild/resources_yaml", fmt.Sprintf("%s.yaml", nameOrDefault(name, serviceID)))
}
instructionOutput := strings.TrimSpace(os.Getenv("NUBES_INSTRUCTION_OUTPUT"))
if instructionOutput == "" {
instructionOutput = filepath.Join("/home/naeel/terra/docs/30_registry/resources", fmt.Sprintf("%s_instruction.md", nameOrDefault(name, serviceID)))
}
return config{
ApiEndpoint: apiEndpoint,
ApiToken: apiToken,
ServiceID: serviceID,
ServiceName: name,
OutputPath: output,
InstructionPath: instructionOutput,
}, nil
}
func nameOrDefault(name string, serviceID int) string {
if strings.TrimSpace(name) != "" {
return name
}
return fmt.Sprintf("service_%d", serviceID)
}
func getenvDefault(key, def string) string {
val := strings.TrimSpace(os.Getenv(key))
if val == "" {
return def
}
return val
}
func readTokenFromTfvars(path string) string {
b, err := os.ReadFile(path)
if err != nil {
return ""
}
re := regexp.MustCompile(`(?m)^\s*api_token\s*=\s*"([^"]+)"`)
m := re.FindStringSubmatch(string(b))
if len(m) < 2 {
return ""
}
return m[1]
}
// ===== api client =====
type apiClient struct {
endpoint string
token string
}
type serviceResponse struct {
Service serviceInfo `json:"svc"`
}
type serviceInfo struct {
ID int `json:"svcId"`
Name string `json:"svcShort"`
Man string `json:"man"`
Operations []operationInfo `json:"operations"`
}
type operationInfo struct {
SvcOperationId int `json:"svcOperationId"`
Operation string `json:"operation"`
}
type serviceOperationResponse struct {
ServiceOperation serviceOperationInfo `json:"svcOperation"`
}
type serviceOperationInfo struct {
SvcOperationId int `json:"svcOperationId"`
Operation string `json:"operation"`
Man string `json:"man"`
CfsParams []cfsParam `json:"cfsParams"`
}
type cfsParam struct {
ID int `json:"svcOperationCfsParamId"`
Code string `json:"svcOperationCfsParam"`
DataType string `json:"dataType"`
IsRequired bool `json:"isRequired"`
DefaultValue interface{} `json:"defaultValue"`
// NOTE(name-to-uuid): refSvcId marks params referencing another service instance.
RefSvcId *int `json:"refSvcId"`
}
func (c *apiClient) getService(serviceID int) (serviceInfo, error) {
endpoint := fmt.Sprintf("/services/%d", serviceID)
var res serviceResponse
if err := c.getViaProxy(endpoint, &res); err != nil {
return serviceInfo{}, err
}
return res.Service, nil
}
func (c *apiClient) getServiceOperation(svcOperationId int) (serviceOperationInfo, error) {
endpoint := fmt.Sprintf("/serviceOperation/%d", svcOperationId)
var res serviceOperationResponse
if err := c.getViaProxy(endpoint, &res); err != nil {
return serviceOperationInfo{}, err
}
return res.ServiceOperation, nil
}
func (c *apiClient) getViaProxy(endpoint string, out interface{}) error {
req, err := http.NewRequest("GET", c.endpoint, nil)
if err != nil {
return err
}
q := req.URL.Query()
q.Set("endpoint", endpoint)
req.URL.RawQuery = q.Encode()
if c.token != "" {
req.Header.Set("Authorization", "Bearer "+c.token)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("status %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
dec := json.NewDecoder(resp.Body)
return dec.Decode(out)
}
func (c *apiClient) collectOperations(ops []operationInfo) (map[string][]Param, string, bool, error) {
result := map[string][]Param{
"create": {},
"modify": {},
}
createMan := ""
hasSuspend := false
for _, op := range ops {
name := strings.ToLower(strings.TrimSpace(op.Operation))
if name == "suspend" {
hasSuspend = true
}
if name != "create" && name != "modify" {
continue
}
opInfo, err := c.getServiceOperation(op.SvcOperationId)
if err != nil {
return nil, "", false, err
}
if name == "create" && createMan == "" {
createMan = strings.TrimSpace(opInfo.Man)
}
params := make([]Param, 0, len(opInfo.CfsParams))
for _, p := range opInfo.CfsParams {
// Name -> UUID mapping is driven by refSvcId metadata from API.
// We persist it in YAML so the Go generator can mark name-based inputs.
params = append(params, Param{
ID: p.ID,
Code: p.Code,
Type: mapType(p.DataType),
Required: p.IsRequired,
Default: formatDefault(p.DefaultValue),
RefSvcId: refSvcIdValue(p.RefSvcId),
})
}
sort.Slice(params, func(i, j int) bool { return params[i].ID < params[j].ID })
result[name] = params
}
return result, createMan, hasSuspend, nil
}
func mapType(dt string) string {
d := strings.ToLower(strings.TrimSpace(dt))
if strings.Contains(d, "bool") {
return "bool"
}
if strings.Contains(d, "int") {
return "int64"
}
return "string"
}
func formatDefault(v interface{}) string {
if v == nil {
return ""
}
switch t := v.(type) {
case string:
return t
case bool:
if t {
return "true"
}
return "false"
case float64:
if t == float64(int64(t)) {
return fmt.Sprintf("%d", int64(t))
}
return fmt.Sprintf("%v", t)
default:
b, _ := json.Marshal(t)
return string(b)
}
}
// ===== spec =====
type ResourceSpec struct {
Name string `yaml:"name"`
ServiceID int `yaml:"service_id"`
Create Operation `yaml:"create"`
Modify Operation `yaml:"modify"`
Outputs OutputSection `yaml:"outputs"`
Lifecycle Lifecycle `yaml:"lifecycle"`
}
type Operation struct {
Params []Param `yaml:"params"`
}
type OutputSection struct {
Params []OutputParam `yaml:"params"`
}
type Lifecycle struct {
SuspendOnDestroyDefault bool `yaml:"suspend_on_destroy_default"`
AdoptExistingOnCreateDefault bool `yaml:"adopt_existing_on_create_default"`
}
type Param struct {
ID int `yaml:"id"`
Code string `yaml:"code"`
Type string `yaml:"type"`
Required bool `yaml:"required"`
Default string `yaml:"default,omitempty"`
RefSvcId int `yaml:"ref_svc_id,omitempty"`
}
type OutputParam struct {
Code string `yaml:"code"`
Type string `yaml:"type"`
Sensitive bool `yaml:"sensitive,omitempty"`
}
func refSvcIdValue(value *int) int {
if value == nil {
return 0
}
return *value
}
func defaultOutputParams() []OutputParam {
return []OutputParam{
{Code: "state_params", Type: "map"},
{Code: "state_out", Type: "map"},
{Code: "state_params_flat", Type: "map"},
{Code: "state_out_flat", Type: "map"},
{Code: "vault_secrets", Type: "map", Sensitive: true},
{Code: "vault_url", Type: "string"},
{Code: "vault_user_path", Type: "string"},
{Code: "vault_fields", Type: "list"},
}
}
// ===== extra params validation =====
type ExtraParamSpec struct {
Type string `yaml:"type"`
Required bool `yaml:"required"`
}
func findRepoRoot() (string, error) {
wd, err := os.Getwd()
if err != nil {
return "", err
}
current := wd
for i := 0; i < 8; i++ {
candidate := filepath.Join(current, "resources_yaml")
info, err := os.Stat(candidate)
if err == nil && info.IsDir() {
return current, nil
}
parent := filepath.Dir(current)
if parent == current {
break
}
current = parent
}
return "", errors.New("failed to locate universal_rebuild repo root")
}
func validateExtraParams(extraDir string) error {
entries, err := os.ReadDir(extraDir)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".yaml") {
continue
}
if _, err := parseServiceID(entry.Name()); err != nil {
return err
}
path := filepath.Join(extraDir, entry.Name())
if err := validateExtraParamsFile(path); err != nil {
return err
}
}
return nil
}
func validateExtraParamsFile(path string) error {
b, err := os.ReadFile(path)
if err != nil {
return err
}
var raw []map[string]ExtraParamSpec
if err := yaml.Unmarshal(b, &raw); err != nil {
return fmt.Errorf("invalid extra params yaml %s: %w", path, err)
}
for _, item := range raw {
if len(item) != 1 {
return fmt.Errorf("invalid extra params entry in %s: expected single key", path)
}
for code, spec := range item {
if strings.TrimSpace(code) == "" {
return fmt.Errorf("invalid extra params entry in %s: empty code", path)
}
_ = normalizeExtraType(spec.Type)
}
}
return nil
}
func normalizeExtraType(value string) string {
value = strings.ToLower(strings.TrimSpace(value))
if value == "" {
return "string"
}
switch value {
case "bool", "boolean":
return "bool"
case "int", "int64", "number":
return "int64"
default:
return "string"
}
}
func parseServiceID(name string) (int, error) {
trimmed := strings.TrimSpace(name)
var digits []rune
for _, r := range trimmed {
if r < '0' || r > '9' {
break
}
digits = append(digits, r)
}
if len(digits) == 0 {
return 0, fmt.Errorf("extra params file %q must start with service id", name)
}
value, err := strconv.Atoi(string(digits))
if err != nil {
return 0, fmt.Errorf("invalid service id in extra params file %q", name)
}
return value, nil
}