add: generator tools (sources)
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Generator (placeholder)
|
||||
|
||||
Этот каталог будет содержать генератор:
|
||||
- читает resources_yaml/*
|
||||
- генерирует thin ресурсы в internal/resources_gen
|
||||
- обновляет registry.go
|
||||
@@ -0,0 +1,996 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user