chore: P2 — remove dead tools (service_ops_gen, service_params_gen, gen/v1)

This commit is contained in:
“Naeel”
2026-06-30 18:36:13 +04:00
parent 3cda6ec479
commit 781f538f7b
5 changed files with 0 additions and 2020 deletions
-6
View File
@@ -1,6 +0,0 @@
# Generator (placeholder)
Этот каталог будет содержать генератор:
- читает resources_yaml/*
- генерирует thin ресурсы в internal/resources_gen
- обновляет registry.go
@@ -1,996 +0,0 @@
package main
import (
"bytes"
"fmt"
"go/format"
"io/fs"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"text/template"
"unicode"
"gopkg.in/yaml.v3"
)
type Param struct {
ID int `yaml:"id"`
Code string `yaml:"code"`
Type string `yaml:"type"`
Required bool `yaml:"required"`
Default string `yaml:"default"`
// NOTE(name-to-uuid): ref_svc_id marks params that accept UI names, resolved in core.
RefSvcId int `yaml:"ref_svc_id,omitempty"`
}
type OutputParam struct {
Code string `yaml:"code"`
Type string `yaml:"type"`
Sensitive bool `yaml:"sensitive,omitempty"`
}
type ServiceYAML struct {
Name string `yaml:"name"`
ServiceID int `yaml:"service_id"`
Create struct {
Params []Param `yaml:"params"`
} `yaml:"create"`
Modify struct {
Params []Param `yaml:"params"`
} `yaml:"modify"`
Outputs struct {
Params []OutputParam `yaml:"params"`
} `yaml:"outputs"`
Lifecycle struct {
DeleteModeDefault string `yaml:"delete_mode_default"`
ResumeIfExistsDefault bool `yaml:"resume_if_exists_default"`
} `yaml:"lifecycle"`
}
type GenResource struct {
Name string
ServiceID int
CreateParams []Param
CreateFixedParams []FixedParam
ModifyParams []Param
SchemaParams []Param
OutputParams []OutputParam
DeleteMode string
ResumeIfExists bool
UsesBool bool
UsesInt64 bool
UsesString bool
HasDefaults bool
NeedsBoolDefault bool
NeedsInt64Default bool
NeedsStringDefault bool
}
func main() {
root, err := os.Getwd()
if err != nil {
panic(err)
}
resourcesDir := filepath.Join(root, "resources_yaml")
extraParamsDir := filepath.Join(root, "extra-params")
outDir := filepath.Join(root, "internal", "resources_gen")
services, err := loadServices(resourcesDir, extraParamsDir)
if err != nil {
panic(err)
}
for _, svc := range services {
if err := writeResource(outDir, svc); err != nil {
panic(err)
}
}
if err := writeRegistry(outDir, services); err != nil {
panic(err)
}
}
func loadServices(dir string, extraDir string) ([]GenResource, error) {
var services []GenResource
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 svc ServiceYAML
if err := yaml.Unmarshal(b, &svc); err != nil {
return err
}
createParams, createFixed := splitCreateParams(svc.Name, svc.Create.Params)
modifyParams := svc.Modify.Params
modifyParams = normalizeModifyParams(createParams, modifyParams)
schemaParams, err := buildSchemaParams(extraDir, svc.ServiceID, createParams, modifyParams)
if err != nil {
return err
}
outputParams := mergeOutputParams(commonOutputParams(), normalizeOutputParams(svc.Outputs.Params))
gr := GenResource{
Name: svc.Name,
ServiceID: svc.ServiceID,
CreateParams: createParams,
CreateFixedParams: createFixed,
ModifyParams: modifyParams,
SchemaParams: schemaParams,
OutputParams: outputParams,
DeleteMode: svc.Lifecycle.DeleteModeDefault,
ResumeIfExists: svc.Lifecycle.ResumeIfExistsDefault,
}
analyzeParams := func(p Param) {
switch strings.ToLower(p.Type) {
case "bool":
gr.UsesBool = true
if p.Default != "" && !p.Required {
gr.NeedsBoolDefault = true
gr.HasDefaults = true
}
case "int", "int64", "number":
gr.UsesInt64 = true
if p.Default != "" && !p.Required {
gr.NeedsInt64Default = true
gr.HasDefaults = true
}
default:
gr.UsesString = true
if p.Default != "" && !p.Required {
gr.NeedsStringDefault = true
gr.HasDefaults = true
}
}
}
for _, p := range gr.SchemaParams {
analyzeParams(p)
}
// Schema defaults apply only to create params
gr.NeedsBoolDefault = false
gr.NeedsInt64Default = false
gr.NeedsStringDefault = false
for _, p := range gr.SchemaParams {
if p.Default == "" || p.Required {
continue
}
switch strings.ToLower(p.Type) {
case "bool":
gr.NeedsBoolDefault = true
case "int", "int64", "number":
gr.NeedsInt64Default = true
default:
gr.NeedsStringDefault = true
}
}
services = append(services, gr)
return nil
})
if walkErr != nil {
return nil, walkErr
}
sort.Slice(services, func(i, j int) bool { return services[i].Name < services[j].Name })
return services, nil
}
func writeResource(outDir string, svc GenResource) error {
fileName := fmt.Sprintf("%d_%s_resource.go", svc.ServiceID, svc.Name)
filePath := filepath.Join(outDir, fileName)
tpl, err := template.New("resource").Funcs(template.FuncMap{
"ToCamel": toCamel,
"ToSnake": toSnake,
"ParamType": paramType,
"ParamDefault": paramDefault,
"ParamDefaultExpr": paramDefaultExpr,
"ParamFormat": paramFormat,
"ParamParse": paramParse,
"ParamDescription": paramDescription,
"OutputType": outputParamType,
"OutputIsMap": outputParamIsMap,
"OutputIsList": outputParamIsList,
"OutputSensitive": outputParamSensitive,
"FixedParamExpr": fixedParamExpr,
"bt": func() string { return "`" },
}).Parse(resourceTemplate)
if err != nil {
return err
}
var buf bytes.Buffer
if err := tpl.Execute(&buf, svc); err != nil {
return err
}
formatted, err := format.Source(buf.Bytes())
if err != nil {
formatted = buf.Bytes()
}
return os.WriteFile(filePath, formatted, 0644)
}
func writeRegistry(outDir string, services []GenResource) error {
var buf bytes.Buffer
buf.WriteString("package resources_gen\n\n")
buf.WriteString("import \"github.com/hashicorp/terraform-plugin-framework/resource\"\n\n")
buf.WriteString("// Code generated by tools/gen. DO NOT EDIT.\n")
buf.WriteString("func AllResources() []func() resource.Resource {\n")
buf.WriteString("\treturn []func() resource.Resource{\n")
for _, svc := range services {
buf.WriteString(fmt.Sprintf("\t\tNew%[1]sResource,\n", toCamel(svc.Name)))
}
buf.WriteString("\t}\n")
buf.WriteString("}\n")
formatted, err := format.Source(buf.Bytes())
if err != nil {
formatted = buf.Bytes()
}
return os.WriteFile(filepath.Join(outDir, "registry.go"), formatted, 0644)
}
func toCamel(s string) string {
parts := strings.FieldsFunc(s, func(r rune) bool { return r == '_' || r == '-' })
for i, p := range parts {
if len(p) == 0 {
continue
}
parts[i] = strings.ToUpper(p[:1]) + p[1:]
}
out := strings.Join(parts, "")
return ensureGoIdent(out)
}
func toSnake(s string) string {
var out []rune
for i, r := range s {
if i > 0 && r >= 'A' && r <= 'Z' {
out = append(out, '_')
}
out = append(out, rune(strings.ToLower(string(r))[0]))
}
return string(out)
}
func paramType(p Param) string {
switch strings.ToLower(p.Type) {
case "bool":
return "types.Bool"
case "int", "int64", "number":
return "types.Int64"
default:
return "types.String"
}
}
func paramDefault(p Param) string {
return p.Default
}
func ensureGoIdent(s string) string {
if s == "" {
return "R"
}
first := rune(s[0])
if unicode.IsLetter(first) || first == '_' {
return s
}
return "R" + s
}
func paramDefaultExpr(p Param) string {
if p.Default == "" {
return ""
}
switch strings.ToLower(p.Type) {
case "bool":
val := strings.ToLower(p.Default)
return fmt.Sprintf("booldefault.StaticBool(%s)", val)
case "int", "int64", "number":
return fmt.Sprintf("int64default.StaticInt64(%s)", p.Default)
default:
return fmt.Sprintf("stringdefault.StaticString(%q)", p.Default)
}
}
func outputParamType(p OutputParam) string {
switch strings.ToLower(p.Type) {
case "map":
return "types.Map"
case "list":
return "types.List"
default:
return "types.String"
}
}
func outputParamIsMap(p OutputParam) bool {
return strings.EqualFold(strings.TrimSpace(p.Type), "map")
}
func outputParamIsList(p OutputParam) bool {
return strings.EqualFold(strings.TrimSpace(p.Type), "list")
}
func outputParamSensitive(p OutputParam) bool {
return p.Sensitive
}
func paramFormat(p Param, varName string) string {
switch strings.ToLower(p.Type) {
case "bool":
return fmt.Sprintf("resources_core.FormatBool(%s)", varName)
case "int", "int64", "number":
return fmt.Sprintf("resources_core.FormatInt64(%s)", varName)
default:
return fmt.Sprintf("resources_core.FormatString(%s)", varName)
}
}
func paramDescription(p Param) string {
if p.RefSvcId <= 0 {
return ""
}
// refSvcId marks name-based references that must be resolved to UUID in core.
return fmt.Sprintf("\"UI display_name for service_id=%d (mapped to UUID in core)\"", p.RefSvcId)
}
const resourceTemplate = `package resources_gen
import (
"context"
"terraform-provider-nubes/internal/core"
"terraform-provider-nubes/internal/resources_core"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
{{- if .NeedsInt64Default }}
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
{{- end }}
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
"github.com/hashicorp/terraform-plugin-framework/types"
)
// Code generated by tools/gen. DO NOT EDIT.
// Service: {{.Name}}
// Service ID: {{.ServiceID}}
var _ resource.Resource = &{{ToCamel .Name}}Resource{}
var _ resource.ResourceWithModifyPlan = &{{ToCamel .Name}}Resource{}
type {{ToCamel .Name}}Resource struct {
client *core.UniversalClient
}
type {{ToCamel .Name}}Model struct {
ID types.String {{bt}}tfsdk:"id"{{bt}}
ResourceName types.String {{bt}}tfsdk:"resource_name"{{bt}}
{{- range .SchemaParams }}
{{ToCamel .Code}} {{ParamType .}} {{bt}}tfsdk:"{{ToSnake .Code}}"{{bt}}
{{- end }}
DeleteMode types.String {{bt}}tfsdk:"delete_mode"{{bt}}
ResumeIfExists types.Bool {{bt}}tfsdk:"resume_if_exists"{{bt}}
{{- range .OutputParams }}
{{ToCamel .Code}} {{OutputType .}} {{bt}}tfsdk:"{{ToSnake .Code}}"{{bt}}
{{- end }}
}
func New{{ToCamel .Name}}Resource() resource.Resource {
return &{{ToCamel .Name}}Resource{}
}
func (r *{{ToCamel .Name}}Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_{{.Name}}"
}
func (r *{{ToCamel .Name}}Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
attrs := map[string]schema.Attribute{
"id": schema.StringAttribute{Computed: true},
"resource_name": schema.StringAttribute{Required: true},
{{- range .SchemaParams }}
"{{ToSnake .Code}}": schema.{{if eq (ParamType .) "types.Bool"}}Bool{{else if eq (ParamType .) "types.Int64"}}Int64{{else}}String{{end}}Attribute{
{{if .Required}}Required: true,{{else}}Optional: true,{{end}}
{{if and (ParamDefault .) (not .Required)}}Computed: true,{{end}}
{{if and (ParamDefault .) (not .Required)}}Default: {{ParamDefaultExpr .}},{{end}}
{{- if ParamDescription . }}MarkdownDescription: {{ParamDescription .}},{{end}}
},
{{- end }}
"delete_mode": schema.StringAttribute{
Optional: true,
Computed: true,
Default: stringdefault.StaticString("{{if .DeleteMode}}{{.DeleteMode}}{{else}}state_only{{end}}"),
},
"resume_if_exists": schema.BoolAttribute{
Optional: true,
Computed: true,
Default: booldefault.StaticBool({{if .ResumeIfExists}}true{{else}}false{{end}}),
},
{{- range .OutputParams }}
"{{ToSnake .Code}}": schema.{{if OutputIsMap .}}Map{{else if OutputIsList .}}List{{else}}String{{end}}Attribute{
Computed: true,
{{- if or (OutputIsMap .) (OutputIsList .) }}ElementType: types.StringType,{{- end }}
{{- if OutputSensitive . }}Sensitive: true,{{- end }}
},
{{- end }}
}
resp.Schema = schema.Schema{Attributes: attrs}
}
func (r *{{ToCamel .Name}}Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
if r.client == nil {
return
}
var config *{{ToCamel .Name}}Model
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}
if config == nil {
return
}
var state *{{ToCamel .Name}}Model
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
return
}
if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
return
}
resumeIfExists := true
if !config.ResumeIfExists.IsNull() && !config.ResumeIfExists.IsUnknown() {
resumeIfExists = config.ResumeIfExists.ValueBool()
}
resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnostics(ctx, r.client, {{.ServiceID}}, config.ResourceName.ValueString(), resumeIfExists)...)
}
func (r *{{ToCamel .Name}}Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data {{ToCamel .Name}}Model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
resourceName := data.ResourceName.ValueString()
resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, {{.ServiceID}}, resourceName, data.ResumeIfExists.ValueBool())...)
params := map[int]string{
{{- range .CreateFixedParams }}
{{.ID}}: {{FixedParamExpr .}},
{{- end }}
{{- range .CreateParams }}
{{.ID}}: {{ParamFormat . (printf "data.%s" (ToCamel .Code))}},
{{- end }}
}
id, err := resources_core.CreateResource(ctx, r.client, {{.ServiceID}}, resourceName, data.ResumeIfExists.ValueBool(), params)
if err != nil {
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
return
}
data.ID = types.StringValue(id)
if r.client != nil {
outputs, outDiags := resources_core.FetchInstanceOutputs(ctx, r.client, data.ID.ValueString())
resp.Diagnostics.Append(outDiags...)
if resp.Diagnostics.HasError() {
return
}
{{- range .OutputParams }}
data.{{ToCamel .Code}} = outputs.{{ToCamel .Code}}
{{- end }}
{{- if .SchemaParams }}
// Sync input params from state_params to avoid drift (ex: git_path).
paramsMap, paramsDiags := resources_core.StringMapFromTypesMap(ctx, outputs.StateParams)
resp.Diagnostics.Append(paramsDiags...)
if resp.Diagnostics.HasError() {
return
}
// Map refSvcId UUIDs back to display_name for stable state.
paramsMap, paramsDiags = resources_core.ResolveRefSvcParamDisplayNames(ctx, r.client, {{.ServiceID}}, paramsMap)
resp.Diagnostics.Append(paramsDiags...)
if resp.Diagnostics.HasError() {
return
}
{{- range .SchemaParams }}
if val, ok := paramsMap["{{.Code}}"] ; ok {
data.{{ToCamel .Code}} = {{ParamParse .}}(val)
}
{{- end }}
{{- end }}
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *{{ToCamel .Name}}Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data *{{ToCamel .Name}}Model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
if data == nil {
return
}
if r.client != nil && !data.ID.IsNull() && !data.ID.IsUnknown() {
remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, data.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
return
}
if remove {
resp.State.RemoveResource(ctx)
return
}
outputs, outDiags := resources_core.FetchInstanceOutputs(ctx, r.client, data.ID.ValueString())
resp.Diagnostics.Append(outDiags...)
if resp.Diagnostics.HasError() {
return
}
{{- range .OutputParams }}
data.{{ToCamel .Code}} = outputs.{{ToCamel .Code}}
{{- end }}
{{- if .SchemaParams }}
// Sync input params from state_params to avoid drift (ex: git_path).
paramsMap, paramsDiags := resources_core.StringMapFromTypesMap(ctx, outputs.StateParams)
resp.Diagnostics.Append(paramsDiags...)
if resp.Diagnostics.HasError() {
return
}
// Map refSvcId UUIDs back to display_name for stable state.
paramsMap, paramsDiags = resources_core.ResolveRefSvcParamDisplayNames(ctx, r.client, {{.ServiceID}}, paramsMap)
resp.Diagnostics.Append(paramsDiags...)
if resp.Diagnostics.HasError() {
return
}
{{- range .SchemaParams }}
if val, ok := paramsMap["{{.Code}}"] ; ok {
data.{{ToCamel .Code}} = {{ParamParse .}}(val)
}
{{- end }}
{{- end }}
}
resp.Diagnostics.Append(resp.State.Set(ctx, data)...)
}
func (r *{{ToCamel .Name}}Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan {{ToCamel .Name}}Model
var state {{ToCamel .Name}}Model
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
instanceID := state.ID
if instanceID.IsNull() || instanceID.IsUnknown() {
instanceID = plan.ID
}
if instanceID.IsNull() || instanceID.IsUnknown() {
resp.Diagnostics.AddError("Ошибка клиента", "отсутствует id инстанса для изменения")
return
}
params := map[int]string{
{{- range .ModifyParams }}
{{.ID}}: {{ParamFormat . (printf "plan.%s" (ToCamel .Code))}},
{{- end }}
}
if err := resources_core.UpdateResource(ctx, r.client, instanceID.ValueString(), params); err != nil {
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
return
}
plan.ID = instanceID
if r.client != nil && !instanceID.IsNull() && !instanceID.IsUnknown() {
outputs, outDiags := resources_core.FetchInstanceOutputs(ctx, r.client, instanceID.ValueString())
resp.Diagnostics.Append(outDiags...)
if resp.Diagnostics.HasError() {
return
}
{{- range .OutputParams }}
plan.{{ToCamel .Code}} = outputs.{{ToCamel .Code}}
{{- end }}
{{- if .SchemaParams }}
// Sync input params from state_params to avoid drift (ex: git_path).
paramsMap, paramsDiags := resources_core.StringMapFromTypesMap(ctx, outputs.StateParams)
resp.Diagnostics.Append(paramsDiags...)
if resp.Diagnostics.HasError() {
return
}
// Map refSvcId UUIDs back to display_name for stable state.
paramsMap, paramsDiags = resources_core.ResolveRefSvcParamDisplayNames(ctx, r.client, {{.ServiceID}}, paramsMap)
resp.Diagnostics.Append(paramsDiags...)
if resp.Diagnostics.HasError() {
return
}
{{- range .SchemaParams }}
if val, ok := paramsMap["{{.Code}}"] ; ok {
plan.{{ToCamel .Code}} = {{ParamParse .}}(val)
}
{{- end }}
{{- end }}
}
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}
func (r *{{ToCamel .Name}}Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state *{{ToCamel .Name}}Model
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
if state == nil || state.ID.IsNull() || state.ID.IsUnknown() {
return
}
if err := resources_core.DeleteResource(ctx, r.client, state.ID.ValueString(), state.DeleteMode.ValueString()); err != nil {
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
return
}
}
func (r *{{ToCamel .Name}}Resource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*core.UniversalClient)
if !ok {
resp.Diagnostics.AddError("Ошибка", "Неверный тип клиента, ожидается *core.UniversalClient")
return
}
r.client = client
}
// format helpers live in resources_core/helpers.go
`
type FixedParam struct {
ID int
Value string
}
func splitCreateParams(serviceName string, params []Param) ([]Param, []FixedParam) {
if len(params) == 0 {
return nil, nil
}
return params, nil
}
func isResourceRealmParam(p Param) bool {
return strings.EqualFold(strings.TrimSpace(p.Code), "resourceRealm")
}
func fixedParamExpr(p FixedParam) string {
return fmt.Sprintf("resources_core.FormatString(types.StringValue(%q))", p.Value)
}
func mergeParams(createParams []Param, modifyParams []Param) []Param {
if len(createParams) == 0 && len(modifyParams) == 0 {
return nil
}
byCode := make(map[string]Param)
order := make([]string, 0, len(createParams)+len(modifyParams))
for _, p := range createParams {
key := strings.ToLower(strings.TrimSpace(p.Code))
if key == "" {
continue
}
byCode[key] = p
order = append(order, key)
}
for _, p := range modifyParams {
key := strings.ToLower(strings.TrimSpace(p.Code))
if key == "" {
continue
}
if _, exists := byCode[key]; !exists {
p.Required = false
byCode[key] = p
order = append(order, key)
}
}
merged := make([]Param, 0, len(order))
seen := make(map[string]struct{}, len(order))
for _, key := range order {
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
merged = append(merged, byCode[key])
}
return merged
}
func normalizeModifyParams(createParams []Param, modifyParams []Param) []Param {
if len(modifyParams) == 0 {
return nil
}
createByCode := make(map[string]Param)
for _, p := range createParams {
key := strings.ToLower(strings.TrimSpace(p.Code))
if key == "" {
continue
}
createByCode[key] = p
}
normalized := make([]Param, 0, len(modifyParams))
for _, p := range modifyParams {
key := strings.ToLower(strings.TrimSpace(p.Code))
if key == "" {
continue
}
if base, ok := createByCode[key]; ok {
p.Type = base.Type
}
normalized = append(normalized, p)
}
return normalized
}
// ===== extra params support =====
type ExtraParamSpec struct {
Type string `yaml:"type"`
Required bool `yaml:"required"`
}
func buildSchemaParams(extraDir string, serviceID int, createParams []Param, modifyParams []Param) ([]Param, error) {
base := mergeParams(createParams, modifyParams)
extra, err := loadExtraParams(extraDir, serviceID)
if err != nil {
return nil, err
}
if len(extra) == 0 {
return base, nil
}
return mergeExtraParams(base, extra)
}
func loadExtraParams(extraDir string, serviceID int) ([]Param, error) {
if extraDir == "" {
return nil, nil
}
entries, err := os.ReadDir(extraDir)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
var params []Param
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".yaml") {
continue
}
fileSvcID, err := parseServiceID(entry.Name())
if err != nil {
return nil, err
}
if fileSvcID != serviceID {
continue
}
path := filepath.Join(extraDir, entry.Name())
fileParams, err := parseExtraParamsFile(path)
if err != nil {
return nil, err
}
params = append(params, fileParams...)
}
return params, nil
}
func parseExtraParamsFile(path string) ([]Param, error) {
b, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var raw []map[string]ExtraParamSpec
if err := yaml.Unmarshal(b, &raw); err != nil {
return nil, fmt.Errorf("invalid extra params yaml %s: %w", path, err)
}
if len(raw) == 0 {
return nil, nil
}
params := make([]Param, 0, len(raw))
for _, item := range raw {
if len(item) != 1 {
return nil, fmt.Errorf("invalid extra params entry in %s: expected single key", path)
}
for code, spec := range item {
code = strings.TrimSpace(code)
if code == "" {
return nil, fmt.Errorf("invalid extra params entry in %s: empty code", path)
}
paramType := normalizeExtraType(spec.Type)
params = append(params, Param{
Code: code,
Type: paramType,
Required: spec.Required,
})
}
}
return params, 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 mergeExtraParams(base []Param, extra []Param) ([]Param, error) {
if len(extra) == 0 {
return base, nil
}
byCode := make(map[string]struct{}, len(base))
for _, p := range base {
key := strings.ToLower(strings.TrimSpace(p.Code))
if key == "" {
continue
}
byCode[key] = struct{}{}
}
for _, p := range extra {
key := strings.ToLower(strings.TrimSpace(p.Code))
if key == "" {
return nil, fmt.Errorf("invalid extra param: empty code")
}
if _, exists := byCode[key]; exists {
return nil, fmt.Errorf("extra param %q conflicts with existing API param", p.Code)
}
byCode[key] = struct{}{}
base = append(base, p)
}
return base, nil
}
func normalizeOutputParams(params []OutputParam) []OutputParam {
normalized := make([]OutputParam, 0, len(params))
for _, p := range params {
code := strings.TrimSpace(p.Code)
if code == "" {
continue
}
p.Code = code
p.Type = normalizeOutputType(p.Type)
normalized = append(normalized, p)
}
return normalized
}
func normalizeOutputType(value string) string {
value = strings.ToLower(strings.TrimSpace(value))
if value == "" {
return "string"
}
switch value {
case "map", "list", "string":
return value
default:
return "string"
}
}
func commonOutputParams() []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"},
}
}
func paramParse(p Param) string {
switch strings.ToLower(p.Type) {
case "bool":
return "resources_core.ParseBool"
case "int", "int64", "number":
return "resources_core.ParseInt64"
default:
return "resources_core.ParseString"
}
}
func mergeOutputParams(base []OutputParam, extra []OutputParam) []OutputParam {
if len(extra) == 0 {
return base
}
byCode := make(map[string]struct{}, len(base))
for _, p := range base {
key := strings.ToLower(strings.TrimSpace(p.Code))
if key == "" {
continue
}
byCode[key] = struct{}{}
}
for _, p := range extra {
key := strings.ToLower(strings.TrimSpace(p.Code))
if key == "" {
continue
}
if _, exists := byCode[key]; exists {
continue
}
byCode[key] = struct{}{}
base = append(base, p)
}
return base
}
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
}
@@ -2066,7 +2066,6 @@ func validateSpec(path string, spec *ServiceSpec) error {
return nil
}
// formatSourceOrWarn форматирует Go-код. При ошибке пишет warning в stderr и возвращает неформатированный код.
func formatSourceOrWarn(filePath string, src []byte) []byte {
formatted, err := format.Source(src)
@@ -1,493 +0,0 @@
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)
}
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; Terraform-Provider-Nubes/Generator)")
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"`
}
@@ -1,524 +0,0 @@
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)
}
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; Terraform-Provider-Nubes/Generator)")
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
}