Files
tf_provider/TOOLS/resource-generator/internal/templates/subresource.go
T

453 lines
19 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package templates
const Subresource = `package resources_gen
import (
"context"
"strings"
"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/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
{{- if .NeedsInt64Default }}
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
{{- end }}
{{- if .NeedsStringDefault }}
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
{{- end }}
{{- if .NeedsBoolPlanMod }}
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
{{- end }}
{{- if .NeedsInt64PlanMod }}
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
{{- end }}
"github.com/hashicorp/terraform-plugin-framework/types"
)
// Code generated by TOOLS/resource-generator. DO NOT EDIT.
// Service: {{.ServiceName}}
// Subresource: {{.SubName}}
var _ resource.Resource = &{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource{}
// {{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource manages a subresource via operations.
type {{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource struct {
client *core.UniversalClient
}
type {{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Model struct {
ID types.String ` + "`" + `tfsdk:"id"` + "`" + `
{{ToCamel .ServiceName}}ID types.String ` + "`" + `tfsdk:"{{ToSnake .ServiceName}}_id"` + "`" + `
AdoptExistingOnCreate types.Bool ` + "`" + `tfsdk:"adopt_existing_on_create"` + "`" + `
SkipMissingOnDelete types.Bool ` + "`" + `tfsdk:"skip_missing_on_delete"` + "`" + `
OperationTimeout types.String ` + "`" + `tfsdk:"operation_timeout"` + "`" + `
LogLevel types.String ` + "`" + `tfsdk:"log_level"` + "`" + `
{{- range .SchemaParams }}
{{ToCamel .Code}} {{ParamType .}} ` + "`" + `tfsdk:"{{ToSnake .Code}}"` + "`" + `
{{- end }}
}
func New{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource() resource.Resource {
return &{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource{}
}
func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_{{.ServiceName}}_{{.SubName}}"
}
func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
attrs := map[string]schema.Attribute{
"id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}},
"{{ToSnake .ServiceName}}_id": schema.StringAttribute{
Required: true,
PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
},
"adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
"skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
"operation_timeout": schema.StringAttribute{Optional: true},
"log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."},
{{- range .SchemaParams }}
"{{ToSnake .Code}}": schema.{{if eq (ParamType .) "types.Bool"}}Bool{{else if eq (ParamType .) "types.Int64"}}Int64{{else}}String{{end}}Attribute{
{{- if and .Required (eq (ParamDefaultExpr .) "") (eq .RefSvcId 0) }}Required: true,{{else}}Optional: true,{{end}}
{{- if ne (ParamDefaultExpr .) "" }}Computed: true, Default: {{ParamDefaultExpr .}},{{- else if .IsJson }}Computed: true,{{- end }}
{{- if ne (ParamDescription .) "" }}MarkdownDescription: {{ParamDescription .}},{{end}}
{{- if .Sensitive }}Sensitive: true,{{end}}
{{- if .ForceNew }}PlanModifiers: []planmodifier.{{PlanModifierType .}}{ {{PlanModifierExpr .}} },{{else if .IsJson}}PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()},{{end}}
},
{{- end }}
}
resp.Schema = schema.Schema{Attributes: attrs}
}
func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan {{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Model
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
{{- range .SchemaParams }}
{{- if and .Required (eq (ParamDefaultExpr .) "") }}
if plan.{{ToCamel .Code}}.IsNull() || plan.{{ToCamel .Code}}.IsUnknown() {
resp.Diagnostics.AddError("Missing required attribute", "{{ToSnake .Code}} is required.")
return
}
{{- end }}
{{- end }}
{{- if .HasRefSvcParams }}
{{- range .CreateParams }}
{{- if and (gt .RefSvcId 0) (eq (ParamType .) "types.String") }}
// Подресурсный refSvc-параметр резолвим только в локальную переменную.
// Сам plan не трогаем: UUID нужен лишь для backend-вызова, а в state
// Terraform должен видеть исходное пользовательское значение.
resolved{{ToCamel .Code}} := plan.{{ToCamel .Code}}.ValueString()
if !plan.{{ToCamel .Code}}.IsNull() && !plan.{{ToCamel .Code}}.IsUnknown() {
var err error
resolved{{ToCamel .Code}}, err = r.client.ResolveRefSvcParamValue(ctx, {{.RefSvcId}}, plan.{{ToCamel .Code}}.ValueString())
if err != nil {
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
return
}
}
{{- end }}
{{- end }}
{{- end }}
instanceUID := strings.TrimSpace(plan.{{ToCamel .ServiceName}}ID.ValueString())
if instanceUID == "" {
resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
return
}
adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
idParams := map[string]string{
{{- range .IdentityParams }}
"{{.Code}}": {{ParamFormat . (printf "plan.%s" (ToCamel .Code))}},
{{- end }}
}
identityCodes := []string{
{{- range .IdentityParams }}
"{{.Code}}",
{{- end }}
}
listKey, idKey := resources_core.ResolveSubresourceStateKeys("{{.SubName}}", identityCodes)
targetValue := strings.TrimSpace(idParams[idKey])
if targetValue == "" {
for _, code := range identityCodes {
candidate := strings.TrimSpace(idParams[code])
if candidate == "" {
continue
}
targetValue = candidate
idKey = code
break
}
}
if targetValue != "" {
found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue)
if err != nil {
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
return
}
if known && found {
if adoptExistingOnCreate {
plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "{{.SubName}}", idParams))
resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue)
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
return
}
resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue)
return
}
}
params := resources_core.CompactParams(map[string]string{
{{- range .CreateParams }}
{{- if and (gt .RefSvcId 0) (eq (ParamType .) "types.String") }}
"{{.Code}}": resolved{{ToCamel .Code}},
{{- else }}
"{{.Code}}": {{ParamFormat . (printf "plan.%s" (ToCamel .Code))}},
{{- end }}
{{- end }}
})
operationTimeout := ""
if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() {
operationTimeout = plan.OperationTimeout.ValueString()
}
if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() {
ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString())
}
createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "{{.CreateOpName}}", params, operationTimeout)
if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "{{.CreateOpName}}", params, operationTimeout)
}
if createErr != nil {
if targetValue != "" {
found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue)
if checkErr != nil {
resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error())
return
}
if known && found {
plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "{{.SubName}}", idParams))
resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue)
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
return
}
}
if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) {
if targetValue != "" {
found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue)
if checkErr != nil {
resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error())
return
}
if found || !known {
plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "{{.SubName}}", idParams))
resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state")
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
return
}
}
resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out")
return
}
resp.Diagnostics.AddError("Ошибка клиента", createErr.Error())
return
}
if targetValue != "" {
found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue)
if err != nil {
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
return
}
if known && !found {
resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue)
return
}
}
plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "{{.SubName}}", idParams))
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}
func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state {{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Model
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}
func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan {{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Model
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
{{- if .HasRefSvcParams }}
{{- range .ModifyParams }}
{{- if and (gt .RefSvcId 0) (eq (ParamType .) "types.String") }}
// Update для подресурса следует тому же правилу, что и Create:
// резолвим значение только в локальную переменную и не мутируем plan.
resolved{{ToCamel .Code}} := plan.{{ToCamel .Code}}.ValueString()
if !plan.{{ToCamel .Code}}.IsNull() && !plan.{{ToCamel .Code}}.IsUnknown() {
var err error
resolved{{ToCamel .Code}}, err = r.client.ResolveRefSvcParamValue(ctx, {{.RefSvcId}}, plan.{{ToCamel .Code}}.ValueString())
if err != nil {
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
return
}
}
{{- end }}
{{- end }}
{{- end }}
if "{{.ModifyOpName}}" == "" {
var state {{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Model
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
if plan.ID.IsNull() || plan.ID.IsUnknown() {
plan.ID = state.ID
}
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
return
}
instanceUID := strings.TrimSpace(plan.{{ToCamel .ServiceName}}ID.ValueString())
if instanceUID == "" {
resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
return
}
params := resources_core.CompactParams(map[string]string{
{{- range .ModifyParams }}
{{- if and (gt .RefSvcId 0) (eq (ParamType .) "types.String") }}
"{{.Code}}": resolved{{ToCamel .Code}},
{{- else }}
"{{.Code}}": {{ParamFormat . (printf "plan.%s" (ToCamel .Code))}},
{{- end }}
{{- end }}
})
idParams := map[string]string{
{{- range .IdentityParams }}
"{{.Code}}": {{ParamFormat . (printf "plan.%s" (ToCamel .Code))}},
{{- end }}
}
operationTimeout := ""
if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() {
operationTimeout = plan.OperationTimeout.ValueString()
}
if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() {
ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString())
}
if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "{{.ModifyOpName}}", params, operationTimeout); err != nil {
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
return
}
plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "{{.SubName}}", idParams))
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}
func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state {{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Model
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
{{- if .HasRefSvcParams }}
{{- range .SchemaParams }}
{{- if and (gt .RefSvcId 0) (eq (ParamType .) "types.String") }}
if !state.{{ToCamel .Code}}.IsNull() && !state.{{ToCamel .Code}}.IsUnknown() {
resolved{{ToCamel .Code}}, err := r.client.ResolveRefSvcParamValue(ctx, {{.RefSvcId}}, state.{{ToCamel .Code}}.ValueString())
if err != nil {
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
return
}
if resolved{{ToCamel .Code}} != "" && resolved{{ToCamel .Code}} != state.{{ToCamel .Code}}.ValueString() {
state.{{ToCamel .Code}} = types.StringValue(resolved{{ToCamel .Code}})
}
}
{{- end }}
{{- end }}
{{- end }}
if "{{.DeleteOpName}}" == "" {
return
}
instanceUID := strings.TrimSpace(state.{{ToCamel .ServiceName}}ID.ValueString())
if instanceUID == "" {
return
}
skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
idParams := map[string]string{
{{- range .IdentityParams }}
"{{.Code}}": {{ParamFormat . (printf "state.%s" (ToCamel .Code))}},
{{- end }}
}
identityCodes := []string{
{{- range .IdentityParams }}
"{{.Code}}",
{{- end }}
}
listKey, idKey := resources_core.ResolveSubresourceStateKeys("{{.SubName}}", identityCodes)
targetValue := strings.TrimSpace(idParams[idKey])
if targetValue == "" {
for _, code := range identityCodes {
candidate := strings.TrimSpace(idParams[code])
if candidate == "" {
continue
}
targetValue = candidate
idKey = code
break
}
}
if targetValue != "" {
found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue)
if err != nil {
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
return
}
if known && !found {
resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue)
return
}
}
params := resources_core.CompactParams(map[string]string{
{{- range .DeleteParams }}
"{{.Code}}": {{ParamFormat . (printf "state.%s" (ToCamel .Code))}},
{{- end }}
})
operationTimeout := ""
if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() {
operationTimeout = state.OperationTimeout.ValueString()
}
if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() {
ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString())
}
if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "{{.DeleteOpName}}", params, operationTimeout); err != nil {
if resources_core.IsSubresourceMissingError(err) {
if targetValue != "" {
found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue)
if checkErr != nil {
resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error())
return
}
if known && found {
resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue)
return
}
}
if skipMissingOnDelete {
resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено")
return
}
}
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
return
}
if targetValue != "" {
found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue)
if err != nil {
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
return
}
if known && found {
resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue)
return
}
}
}
func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}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("Error", "Invalid client type")
return
}
r.client = client
}
`