add: generator tools (sources)
This commit is contained in:
@@ -0,0 +1,492 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// service_ops_gen: builds per-service ops YAML from /index.cfm?endpoint=/services/{svcId}
|
||||
// and /index.cfm?endpoint=/serviceOperation/{svcOperationId}.
|
||||
//
|
||||
// Env:
|
||||
// - NUBES_API_TOKEN (preferred) or TOKEN_FILE
|
||||
// - NUBES_API_ENDPOINT (default: https://deck-api.ngcloud.ru/api/v1/index.cfm)
|
||||
// - NUBES_SERVICE_ID (optional for single service)
|
||||
// - NUBES_SERVICE_NAME (optional override for single service)
|
||||
// - NUBES_SERVICES_FILE (default: devops/config/services_list.txt)
|
||||
// - NUBES_OUTPUT_DIR (default: <repo>/resources_ops_yaml)
|
||||
|
||||
func main() {
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
repoRoot, err := findRepoRoot()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
client := &apiClient{endpoint: cfg.ApiEndpoint, token: cfg.ApiToken}
|
||||
|
||||
services := cfg.Services
|
||||
if cfg.SingleServiceID > 0 {
|
||||
services = []serviceRef{{ID: cfg.SingleServiceID, Name: cfg.SingleServiceName}}
|
||||
}
|
||||
|
||||
outputDir := cfg.OutputDir
|
||||
if outputDir == "" {
|
||||
outputDir = filepath.Join(repoRoot, "resources_ops_yaml")
|
||||
}
|
||||
if err := os.MkdirAll(outputDir, 0o755); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, svc := range services {
|
||||
if svc.ID <= 0 {
|
||||
continue
|
||||
}
|
||||
info, err := client.getService(svc.ID)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
name := strings.TrimSpace(svc.Name)
|
||||
if name == "" {
|
||||
name = strings.TrimSpace(info.ShortName)
|
||||
}
|
||||
if name == "" {
|
||||
name = strings.TrimSpace(info.DisplayName)
|
||||
}
|
||||
if name == "" {
|
||||
name = fmt.Sprintf("service_%d", svc.ID)
|
||||
}
|
||||
|
||||
opSpecs, err := client.collectOperations(info.Operations)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
spec := ServiceOpsSpec{
|
||||
Name: name,
|
||||
ServiceID: svc.ID,
|
||||
ServiceDisplayName: info.DisplayName,
|
||||
ServiceShortName: info.ShortName,
|
||||
ServiceMan: strings.TrimSpace(info.Man),
|
||||
Operations: opSpecs,
|
||||
}
|
||||
|
||||
outPath := filepath.Join(outputDir, fmt.Sprintf("%d_%s.yaml", svc.ID, name))
|
||||
buf, err := yaml.Marshal(spec)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := os.WriteFile(outPath, buf, 0o644); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("written %s\n", outPath)
|
||||
}
|
||||
}
|
||||
|
||||
// ===== config =====
|
||||
|
||||
type config struct {
|
||||
ApiEndpoint string
|
||||
ApiToken string
|
||||
Services []serviceRef
|
||||
SingleServiceID int
|
||||
SingleServiceName string
|
||||
OutputDir string
|
||||
}
|
||||
|
||||
type serviceRef struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
|
||||
func loadConfig() (config, error) {
|
||||
apiEndpoint := getenvDefault("NUBES_API_ENDPOINT", "https://deck-api.ngcloud.ru/api/v1/index.cfm")
|
||||
apiToken, err := loadToken()
|
||||
if err != nil {
|
||||
return config{}, err
|
||||
}
|
||||
|
||||
singleID := 0
|
||||
if raw := strings.TrimSpace(os.Getenv("NUBES_SERVICE_ID")); raw != "" {
|
||||
val, err := strconv.Atoi(raw)
|
||||
if err != nil {
|
||||
return config{}, fmt.Errorf("invalid NUBES_SERVICE_ID: %s", raw)
|
||||
}
|
||||
singleID = val
|
||||
}
|
||||
|
||||
singleName := strings.TrimSpace(os.Getenv("NUBES_SERVICE_NAME"))
|
||||
outputDir := strings.TrimSpace(os.Getenv("NUBES_OUTPUT_DIR"))
|
||||
|
||||
services := []serviceRef{}
|
||||
if singleID == 0 {
|
||||
listPath := strings.TrimSpace(os.Getenv("NUBES_SERVICES_FILE"))
|
||||
if listPath == "" {
|
||||
repoRoot, err := findRepoRoot()
|
||||
if err != nil {
|
||||
return config{}, err
|
||||
}
|
||||
listPath = filepath.Join(repoRoot, "devops", "config", "services_list.txt")
|
||||
}
|
||||
list, err := readServicesList(listPath)
|
||||
if err != nil {
|
||||
return config{}, err
|
||||
}
|
||||
services = list
|
||||
}
|
||||
|
||||
return config{
|
||||
ApiEndpoint: apiEndpoint,
|
||||
ApiToken: apiToken,
|
||||
Services: services,
|
||||
SingleServiceID: singleID,
|
||||
SingleServiceName: singleName,
|
||||
OutputDir: outputDir,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getenvDefault(key string, def string) string {
|
||||
val := strings.TrimSpace(os.Getenv(key))
|
||||
if val == "" {
|
||||
return def
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func loadToken() (string, error) {
|
||||
if tok := strings.TrimSpace(os.Getenv("NUBES_API_TOKEN")); tok != "" {
|
||||
return tok, nil
|
||||
}
|
||||
if tf := strings.TrimSpace(os.Getenv("TOKEN_FILE")); tf != "" {
|
||||
b, err := os.ReadFile(tf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(string(b)), nil
|
||||
}
|
||||
repoRoot, err := findRepoRoot()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
latest, err := findLatestToken(repoRoot)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if latest == "" {
|
||||
return "", errors.New("NUBES_API_TOKEN or TOKEN_FILE is required")
|
||||
}
|
||||
b, err := os.ReadFile(latest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(string(b)), nil
|
||||
}
|
||||
|
||||
func findLatestToken(dir string) (string, error) {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var latest string
|
||||
var latestTime int64
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || !strings.HasSuffix(e.Name(), ".token") {
|
||||
continue
|
||||
}
|
||||
info, err := e.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
mt := info.ModTime().Unix()
|
||||
if mt > latestTime {
|
||||
latestTime = mt
|
||||
latest = filepath.Join(dir, e.Name())
|
||||
}
|
||||
}
|
||||
return latest, nil
|
||||
}
|
||||
|
||||
func readServicesList(path string) ([]serviceRef, error) {
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lines := strings.Split(string(b), "\n")
|
||||
services := []serviceRef{}
|
||||
for _, raw := range lines {
|
||||
line := strings.TrimSpace(strings.ReplaceAll(raw, "\r", ""))
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(line, "#") {
|
||||
line = strings.TrimSpace(strings.SplitN(line, "#", 2)[0])
|
||||
}
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) == 0 {
|
||||
continue
|
||||
}
|
||||
id, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid service id in %s: %s", path, parts[0])
|
||||
}
|
||||
name := ""
|
||||
if len(parts) > 1 {
|
||||
name = parts[1]
|
||||
}
|
||||
services = append(services, serviceRef{ID: id, Name: name})
|
||||
}
|
||||
return services, nil
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
// ===== api client =====
|
||||
|
||||
type apiClient struct {
|
||||
endpoint string
|
||||
token string
|
||||
}
|
||||
|
||||
type serviceResponse struct {
|
||||
Service serviceInfo `json:"svc"`
|
||||
}
|
||||
|
||||
type serviceInfo struct {
|
||||
ID int `json:"svcId"`
|
||||
DisplayName string `json:"svc"`
|
||||
ShortName 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"`
|
||||
ValueList []interface{} `json:"valueList"`
|
||||
RefSvcId *int `json:"refSvcId"`
|
||||
IsRequired bool `json:"isRequired"`
|
||||
DefaultValue interface{} `json:"defaultValue"`
|
||||
Func string `json:"func"`
|
||||
Regex string `json:"regex"`
|
||||
UniqueScope string `json:"uniqueScope"`
|
||||
MaxLength *int `json:"maxlength"`
|
||||
MinLength *int `json:"minlength"`
|
||||
MaxValue interface{} `json:"maxvalue"`
|
||||
MinValue interface{} `json:"minvalue"`
|
||||
Descr string `json:"descr"`
|
||||
Man string `json:"man"`
|
||||
Sort *int `json:"sort"`
|
||||
DependsOnCfsParams interface{} `json:"dependsOnCfsParams"`
|
||||
IsModifiable *bool `json:"isModifiable"`
|
||||
IsSensitive bool `json:"isSensitive"`
|
||||
}
|
||||
|
||||
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) ([]OperationSpec, error) {
|
||||
result := make([]OperationSpec, 0, len(ops))
|
||||
for _, op := range ops {
|
||||
opName := strings.ToLower(strings.TrimSpace(op.Operation))
|
||||
if opName == "" {
|
||||
continue
|
||||
}
|
||||
opInfo, err := c.getServiceOperation(op.SvcOperationId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
params := make([]ParamSpec, 0, len(opInfo.CfsParams))
|
||||
for _, p := range opInfo.CfsParams {
|
||||
params = append(params, ParamSpec{
|
||||
ID: p.ID,
|
||||
Code: p.Code,
|
||||
DataType: strings.TrimSpace(p.DataType),
|
||||
Required: p.IsRequired,
|
||||
Default: normalizeDefault(p.DefaultValue),
|
||||
ValueList: normalizeValueList(p.ValueList),
|
||||
RefSvcId: p.RefSvcId,
|
||||
Func: strings.TrimSpace(p.Func),
|
||||
Regex: strings.TrimSpace(p.Regex),
|
||||
UniqueScope: strings.TrimSpace(p.UniqueScope),
|
||||
MaxLength: p.MaxLength,
|
||||
MinLength: p.MinLength,
|
||||
MaxValue: normalizeDefault(p.MaxValue),
|
||||
MinValue: normalizeDefault(p.MinValue),
|
||||
Descr: strings.TrimSpace(p.Descr),
|
||||
Man: strings.TrimSpace(p.Man),
|
||||
Sort: p.Sort,
|
||||
DependsOn: p.DependsOnCfsParams,
|
||||
IsModifiable: p.IsModifiable,
|
||||
IsSensitive: p.IsSensitive,
|
||||
})
|
||||
}
|
||||
sort.Slice(params, func(i, j int) bool { return params[i].ID < params[j].ID })
|
||||
result = append(result, OperationSpec{
|
||||
Name: opName,
|
||||
ID: opInfo.SvcOperationId,
|
||||
Man: strings.TrimSpace(opInfo.Man),
|
||||
Params: params,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
if result[i].Name == result[j].Name {
|
||||
return result[i].ID < result[j].ID
|
||||
}
|
||||
return result[i].Name < result[j].Name
|
||||
})
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func normalizeValueList(values []interface{}) []string {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, 0, len(values))
|
||||
for _, v := range values {
|
||||
out = append(out, fmt.Sprintf("%v", v))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizeDefault(value interface{}) interface{} {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
switch t := value.(type) {
|
||||
case string:
|
||||
return strings.TrimSpace(t)
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
// ===== spec =====
|
||||
|
||||
type ServiceOpsSpec struct {
|
||||
Name string `yaml:"name"`
|
||||
ServiceID int `yaml:"service_id"`
|
||||
ServiceDisplayName string `yaml:"service_display_name,omitempty"`
|
||||
ServiceShortName string `yaml:"service_short_name,omitempty"`
|
||||
ServiceMan string `yaml:"service_man,omitempty"`
|
||||
Operations []OperationSpec `yaml:"operations"`
|
||||
}
|
||||
|
||||
type OperationSpec struct {
|
||||
Name string `yaml:"name"`
|
||||
ID int `yaml:"id"`
|
||||
Man string `yaml:"man,omitempty"`
|
||||
Params []ParamSpec `yaml:"params"`
|
||||
}
|
||||
|
||||
type ParamSpec struct {
|
||||
ID int `yaml:"id"`
|
||||
Code string `yaml:"code"`
|
||||
DataType string `yaml:"data_type,omitempty"`
|
||||
Required bool `yaml:"required"`
|
||||
Default interface{} `yaml:"default,omitempty"`
|
||||
ValueList []string `yaml:"value_list,omitempty"`
|
||||
RefSvcId *int `yaml:"ref_svc_id,omitempty"`
|
||||
Func string `yaml:"func,omitempty"`
|
||||
Regex string `yaml:"regex,omitempty"`
|
||||
UniqueScope string `yaml:"unique_scope,omitempty"`
|
||||
MaxLength *int `yaml:"maxlength,omitempty"`
|
||||
MinLength *int `yaml:"minlength,omitempty"`
|
||||
MaxValue interface{} `yaml:"maxvalue,omitempty"`
|
||||
MinValue interface{} `yaml:"minvalue,omitempty"`
|
||||
Descr string `yaml:"descr,omitempty"`
|
||||
Man string `yaml:"man,omitempty"`
|
||||
Sort *int `yaml:"sort,omitempty"`
|
||||
DependsOn interface{} `yaml:"depends_on,omitempty"`
|
||||
IsModifiable *bool `yaml:"is_modifiable,omitempty"`
|
||||
IsSensitive bool `yaml:"is_sensitive,omitempty"`
|
||||
}
|
||||
Reference in New Issue
Block a user