diff --git a/universal_rebuild/tools/gen/README.md b/universal_rebuild/tools/gen/README.md deleted file mode 100644 index 224af6f..0000000 --- a/universal_rebuild/tools/gen/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Generator (placeholder) - -Этот каталог будет содержать генератор: -- читает resources_yaml/* -- генерирует thin ресурсы в internal/resources_gen -- обновляет registry.go diff --git a/universal_rebuild/tools/gen/generate_resources.go b/universal_rebuild/tools/gen/generate_resources.go deleted file mode 100644 index 43dfeab..0000000 --- a/universal_rebuild/tools/gen/generate_resources.go +++ /dev/null @@ -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 -} diff --git a/universal_rebuild/tools/gen_v2/generate_resources_v2.go b/universal_rebuild/tools/gen_v2/generate_resources_v2.go index e1e220d..ff3710f 100644 --- a/universal_rebuild/tools/gen_v2/generate_resources_v2.go +++ b/universal_rebuild/tools/gen_v2/generate_resources_v2.go @@ -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) diff --git a/universal_rebuild/tools/service_ops_gen/generate_service_ops.go b/universal_rebuild/tools/service_ops_gen/generate_service_ops.go deleted file mode 100644 index a21c23a..0000000 --- a/universal_rebuild/tools/service_ops_gen/generate_service_ops.go +++ /dev/null @@ -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: /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"` -} diff --git a/universal_rebuild/tools/service_params_gen/generate_service_params.go b/universal_rebuild/tools/service_params_gen/generate_service_params.go deleted file mode 100644 index ea1227f..0000000 --- a/universal_rebuild/tools/service_params_gen/generate_service_params.go +++ /dev/null @@ -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 -}