diff --git a/TOOLS/resource-generator/internal/helpers/helpers.go b/TOOLS/resource-generator/internal/helpers/helpers.go
index 4d131dc..e3abf12 100644
--- a/TOOLS/resource-generator/internal/helpers/helpers.go
+++ b/TOOLS/resource-generator/internal/helpers/helpers.go
@@ -193,14 +193,15 @@ func PlanModifierExpr(p types.Param) string {
// ─── Nested (map-fixed) helpers ──────────────────────────────────────────────
-// IsNested возвращает true для параметров с SubParams (map-fixed/array-map-fixed).
+// IsNested возвращает true для параметров с SubParams (map-fixed).
+// array-map-fixed исключается — требует отдельной обработки.
func IsNested(p types.Param) bool {
- return p.IsNested
+ return p.HasSubParams && !IsNestedList(p)
}
// IsNestedList возвращает true для array-map-fixed (список объектов).
func IsNestedList(p types.Param) bool {
- return p.IsNested && strings.Contains(strings.ToLower(p.Type), "array")
+ return p.HasSubParams && strings.Contains(strings.ToLower(p.Type), "array")
}
// NestedModelName возвращает имя вложенного Go-struct для ресурса и параметра.
@@ -245,12 +246,13 @@ func NestedSchemaBlock(p types.Param) string {
}
// NestedSchemaEnd возвращает закрывающую строку для nested-атрибута.
+// Закрывает Attributes map и сам атрибут: `}},` для SingleNested, `}}},` для ListNested.
func NestedSchemaEnd(p types.Param) string {
t := NestedSchemaType(p)
if t == "ListNested" {
- return "}},"
+ return "}}},"
}
- return "},"
+ return "}},"
}
// SubSchemaType возвращает тип схемы для подпараметра.
@@ -295,23 +297,20 @@ func SubParamFormat(p types.Param, varPrefix string) string {
}
// NestedJSONExpr возвращает выражение для сборки JSON из nested struct через resources_core.BuildJSON.
-// BuildJSON принимает map[string]string где ключи — имена полей,
-// значения — уже отформатированные строки (числа без кавычек, строки с кавычками).
-// Пример: resources_core.BuildJSON(map[string]string{"cpu": "500", "name": "\"hello\""}) → {"cpu":500,"name":"hello"}
-func NestedJSONExpr(p types.Param, varName string) string {
+// varPrefix — префикс переменной (например "data" или "plan").
+// Генерирует доступ: data.ClusterConfiguration.Cpu, data.ClusterConfiguration.Memory, ...
+func NestedJSONExpr(p types.Param, varPrefix string) string {
+ varName := varPrefix + "." + ToCamel(p.Code)
pairs := make([]string, 0, len(p.SubParams))
for _, sp := range p.SubParams {
field := varName + "." + ToCamel(sp.Code)
var valExpr string
switch strings.ToLower(sp.Type) {
case "bool":
- // BuildJSON ожидает "true"/"false" (без кавычек)
valExpr = fmt.Sprintf(`fmt.Sprintf("%%v", %s.ValueBool())`, field)
case "int64", "int", "number":
- // BuildJSON ожидает "500" (без кавычек)
valExpr = fmt.Sprintf(`fmt.Sprintf("%%d", %s.ValueInt64())`, field)
default:
- // BuildJSON ожидает "\"hello\"" (с кавычками)
valExpr = fmt.Sprintf(`fmt.Sprintf("\"%%s\"", %s.ValueString())`, field)
}
pairs = append(pairs, fmt.Sprintf(`"%s": %s`, sp.Code, valExpr))
diff --git a/TOOLS/resource-generator/internal/loader/loader.go b/TOOLS/resource-generator/internal/loader/loader.go
index 5414f8b..ed2e4cb 100644
--- a/TOOLS/resource-generator/internal/loader/loader.go
+++ b/TOOLS/resource-generator/internal/loader/loader.go
@@ -104,8 +104,13 @@ func LoadSpecs(dir string) ([]types.GenResource, []types.GenSubresource, []types
HasDomainParam: hasDomainParam,
}
params.Analyze(&gr.UsesBool, &gr.UsesInt64, &gr.UsesString, &gr.HasDefaults, &gr.NeedsBoolDefault, &gr.NeedsInt64Default, &gr.NeedsStringDefault, gr.SchemaParams)
+ // Анализируем nested sub-params для default-импортов
+ if params.AnalyzeNestedDefaults(&gr.NeedsBoolDefault, &gr.NeedsInt64Default, &gr.NeedsStringDefault, gr.SchemaParams) {
+ gr.NeedsFmtImport = true
+ }
gr.NeedsJsonPlanMod = params.AnalyzeJsonPlanMod(gr.SchemaParams)
gr.NeedsStringsImport = params.HasRestoreCasingParams(gr.SchemaParams) || params.AnalyzeNeedsStrings(gr.ModifyParams) || params.AnalyzeNeedsStrings(gr.CreateOnlyRequiredParams) || params.AnalyzeNeedsStrings(gr.CreateOnlyParams)
+ gr.NeedsFmtImport = params.HasNestedParams(gr.SchemaParams)
// --- Action processing (ДО append, чтобы HasRedeploy попал в слайс) ---
for _, op := range spec.Operations {
@@ -239,18 +244,18 @@ func ConvertParams(params []types.ParamSpec) []types.Param {
}
}
out = append(out, types.Param{
- ID: p.ID,
- Code: p.Code,
- Type: typeName,
- Required: p.Required,
- Default: defVal,
- RefSvcId: ref,
- Descr: strings.TrimSpace(p.Descr),
- Man: strings.TrimSpace(p.Man),
- Sensitive: p.IsSensitive,
- IsJson: strings.EqualFold(strings.TrimSpace(p.DataType), "json"),
- IsNested: isNested,
- SubParams: subParams,
+ ID: p.ID,
+ Code: p.Code,
+ Type: typeName,
+ Required: p.Required,
+ Default: defVal,
+ RefSvcId: ref,
+ Descr: strings.TrimSpace(p.Descr),
+ Man: strings.TrimSpace(p.Man),
+ Sensitive: p.IsSensitive,
+ IsJson: strings.EqualFold(strings.TrimSpace(p.DataType), "json"),
+ HasSubParams: isNested,
+ SubParams: subParams,
})
}
return out
diff --git a/TOOLS/resource-generator/internal/params/params.go b/TOOLS/resource-generator/internal/params/params.go
index 58b88d4..5461d49 100644
--- a/TOOLS/resource-generator/internal/params/params.go
+++ b/TOOLS/resource-generator/internal/params/params.go
@@ -66,6 +66,21 @@ func AlignParamTypes(params []types.Param, schema []types.Param) []types.Param {
}
}
+ // Рекурсивно выравниваем SubParams (например cpu может быть string в modify, но int64 в create).
+ schemaByCode := make(map[string]types.Param, len(schema))
+ for _, p := range schema {
+ schemaByCode[strings.ToLower(strings.TrimSpace(p.Code))] = p
+ }
+ for i, p := range params {
+ if !p.HasSubParams {
+ continue
+ }
+ key := strings.ToLower(strings.TrimSpace(p.Code))
+ if sp, ok := schemaByCode[key]; ok && len(sp.SubParams) > 0 {
+ params[i].SubParams = AlignParamTypes(p.SubParams, sp.SubParams)
+ }
+ }
+
return params
}
@@ -231,6 +246,42 @@ func AnalyzeJsonPlanMod(params []types.Param) bool {
return false
}
+// HasNestedParams возвращает true, если есть nested (только map-fixed, не array-map-fixed) параметры.
+func HasNestedParams(params []types.Param) bool {
+ for _, p := range params {
+ if p.HasSubParams && !strings.Contains(strings.ToLower(p.Type), "array") {
+ return true
+ }
+ }
+ return false
+}
+
+// AnalyzeNestedDefaults проверяет sub-params nested-параметров на наличие default'ов.
+// Возвращает true если найдены nested параметры.
+func AnalyzeNestedDefaults(needsBoolDefault *bool, needsInt64Default *bool, needsStringDefault *bool, params []types.Param) bool {
+ found := false
+ for _, p := range params {
+ if len(p.SubParams) == 0 {
+ continue
+ }
+ found = true
+ for _, sp := range p.SubParams {
+ if sp.Default == "" {
+ continue
+ }
+ switch strings.ToLower(sp.Type) {
+ case "bool":
+ *needsBoolDefault = true
+ case "int64", "int", "number":
+ *needsInt64Default = true
+ default:
+ *needsStringDefault = true
+ }
+ }
+ }
+ return found
+}
+
// HasRestoreCasingParams возвращает true, если есть ref_svc-параметры (кроме S3).
func HasRestoreCasingParams(params []types.Param) bool {
for _, p := range params {
@@ -244,6 +295,10 @@ func HasRestoreCasingParams(params []types.Param) bool {
// AnalyzeNeedsStrings возвращает true, если есть строковые параметры.
func AnalyzeNeedsStrings(params []types.Param) bool {
for _, p := range params {
+ // Пропускаем IsNested (map-fixed без array): они не участвуют в strings.EqualFold
+ if p.HasSubParams && !strings.Contains(strings.ToLower(p.Type), "array") {
+ continue
+ }
t := strings.ToLower(strings.TrimSpace(p.Type))
if t != "bool" && t != "int" && t != "int64" && t != "number" {
return true
diff --git a/TOOLS/resource-generator/internal/templates/instance.go b/TOOLS/resource-generator/internal/templates/instance.go
index 2b6d2f3..d66d30a 100644
--- a/TOOLS/resource-generator/internal/templates/instance.go
+++ b/TOOLS/resource-generator/internal/templates/instance.go
@@ -7,6 +7,9 @@ import (
{{- if .NeedsStringsImport }}
"strings"
{{- end }}
+ {{- if .NeedsFmtImport }}
+ "fmt"
+ {{- end }}
"terraform-provider-nubes/internal/core"
"terraform-provider-nubes/internal/resources_core"
@@ -39,7 +42,7 @@ type {{ToCamel .Name}}Resource struct {
}
{{- range .SchemaParams }}
-{{- if .IsNested }}
+{{- if (IsNested .) }}
// {{NestedModelName $.Name .Code}} — вложенная модель для map-fixed параметра {{.Code}}.
type {{NestedModelName $.Name .Code}} struct {
{{- range .SubParams }}
@@ -61,7 +64,7 @@ type {{ToCamel .Name}}Model struct {
GitRevision types.String ` + "`" + `tfsdk:"git_revision"` + "`" + `
{{- end }}
{{- range .SchemaParams }}
- {{- if .IsNested }}
+ {{- if (IsNested .) }}
{{ToCamel .Code}} {{NestedTfType . $.Name}} ` + "`" + `tfsdk:"{{ToSnake .Code}}"` + "`" + `
{{- else }}
{{ToCamel .Code}} {{ParamType .}} ` + "`" + `tfsdk:"{{ToSnake .Code}}"` + "`" + `
@@ -89,7 +92,7 @@ func (r *{{ToCamel .Name}}Resource) Schema(ctx context.Context, req resource.Sch
"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 }}
- {{- if .IsNested }}
+ {{- if (IsNested .) }}
"{{ToSnake .Code}}": {{NestedSchemaBlock .}}
{{- range .SubParams }}
"{{ToSnake .Code}}": schema.{{SubSchemaType .}}Attribute{
@@ -171,6 +174,7 @@ func (r *{{ToCamel .Name}}Resource) ModifyPlan(ctx context.Context, req resource
}
}
{{- range .CreateOnlyParams }}
+ {{- if not (IsNested .) }}
if !plan.{{ToCamel .Code}}.IsNull() && !plan.{{ToCamel .Code}}.IsUnknown() && !state.{{ToCamel .Code}}.IsNull() && !state.{{ToCamel .Code}}.IsUnknown() {
{{- if eq (ParamType .) "types.Bool" }}
if plan.{{ToCamel .Code}}.ValueBool() != state.{{ToCamel .Code}}.ValueBool() {
@@ -193,10 +197,11 @@ func (r *{{ToCamel .Name}}Resource) ModifyPlan(ctx context.Context, req resource
{{- end }}
}
{{- end }}
+ {{- end }}
return
}
{{- range .SchemaParams }}
- {{- if and .Required (eq (ParamDefaultExpr .) "") }}
+ {{- if and .Required (eq (ParamDefaultExpr .) "") (not (IsNested .)) }}
if config.{{ToCamel .Code}}.IsNull() {
resp.Diagnostics.AddError("Missing required attribute", "{{ToSnake .Code}} is required.")
return
@@ -230,7 +235,7 @@ func (r *{{ToCamel .Name}}Resource) ModifyPlan(ctx context.Context, req resource
{{- end }}
params := map[int]string{
{{- range .CreateParams }}
- {{- if .IsNested }}
+ {{- if (IsNested .) }}
{{.ID}}: {{NestedJSONExpr . "config"}},
{{- else }}
{{.ID}}: {{ParamFormat . (printf "config.%s" (ToCamel .Code))}},
@@ -255,7 +260,7 @@ func (r *{{ToCamel .Name}}Resource) Create(ctx context.Context, req resource.Cre
return
}
{{- range .SchemaParams }}
- {{- if and .Required (eq (ParamDefaultExpr .) "") }}
+ {{- if and .Required (eq (ParamDefaultExpr .) "") (not (IsNested .)) }}
if data.{{ToCamel .Code}}.IsNull() || data.{{ToCamel .Code}}.IsUnknown() {
resp.Diagnostics.AddError("Missing required attribute", "{{ToSnake .Code}} is required.")
return
@@ -320,7 +325,7 @@ func (r *{{ToCamel .Name}}Resource) Create(ctx context.Context, req resource.Cre
params := map[int]string{
{{- range .CreateParams }}
- {{- if .IsNested }}
+ {{- if (IsNested .) }}
{{.ID}}: {{NestedJSONExpr . "data"}},
{{- else }}
{{.ID}}: {{ParamFormat . (printf "data.%s" (ToCamel .Code))}},
@@ -477,6 +482,7 @@ func (r *{{ToCamel .Name}}Resource) Update(ctx context.Context, req resource.Upd
hasServiceParamChanges := false
{{- range .ModifyParams }}
+ {{- if not (IsNested .) }}
if !hasServiceParamChanges {
if plan.{{ToCamel .Code}}.IsNull() != state.{{ToCamel .Code}}.IsNull() || plan.{{ToCamel .Code}}.IsUnknown() != state.{{ToCamel .Code}}.IsUnknown() {
hasServiceParamChanges = true
@@ -497,6 +503,7 @@ func (r *{{ToCamel .Name}}Resource) Update(ctx context.Context, req resource.Upd
}
}
{{- end }}
+ {{- end }}
{{- if .HasRedeploy }}
// --- Redeploy support (ARCHITECTURE.md) ---
@@ -540,7 +547,7 @@ func (r *{{ToCamel .Name}}Resource) Update(ctx context.Context, req resource.Upd
{{- end }}
params := map[int]string{
{{- range .ModifyParams }}
- {{- if .IsNested }}
+ {{- if (IsNested .) }}
{{.ID}}: {{NestedJSONExpr . "plan"}},
{{- else }}
{{.ID}}: {{ParamFormat . (printf "plan.%s" (ToCamel .Code))}},
diff --git a/TOOLS/resource-generator/internal/types/types.go b/TOOLS/resource-generator/internal/types/types.go
index a85fbe6..4942c32 100644
--- a/TOOLS/resource-generator/internal/types/types.go
+++ b/TOOLS/resource-generator/internal/types/types.go
@@ -53,8 +53,8 @@ type Param struct {
// При IsJson=true в схему добавляется JsonNormalize план-модификатор,
// чтобы план и API-ответ (компактный JSON) всегда совпадали.
IsJson bool
- // IsNested — параметр типа map-fixed/array-map-fixed с подполями.
- IsNested bool
+ // HasSubParams — параметр имеет подполя (map-fixed или array-map-fixed).
+ HasSubParams bool
// SubParams — подполя для map-fixed/array-map-fixed параметров.
SubParams []Param
}
@@ -93,6 +93,8 @@ type GenResource struct {
NeedsJsonPlanMod bool
// NeedsStringsImport: есть строковые ModifyParams (нужен EqualFold в Update).
NeedsStringsImport bool
+ // NeedsFmtImport: есть nested (map-fixed) параметры, нужен fmt.Sprintf.
+ NeedsFmtImport bool
}
// GenSubresource — подресурс (nubes_{service}_{sub}).
diff --git a/generated/dev/go/100_openwhisk_resource.go b/generated/dev/go/100_openwhisk_resource.go
new file mode 100644
index 0000000..c4cbf39
--- /dev/null
+++ b/generated/dev/go/100_openwhisk_resource.go
@@ -0,0 +1,382 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: openwhisk
+// Service ID: 100
+
+var _ resource.Resource = &OpenwhiskResource{}
+var _ resource.ResourceWithModifyPlan = &OpenwhiskResource{}
+var _ resource.ResourceWithImportState = &OpenwhiskResource{}
+
+type OpenwhiskResource struct {
+ client *core.UniversalClient
+}
+
+type OpenwhiskModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ Username types.String `tfsdk:"username"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewOpenwhiskResource() resource.Resource {
+ return &OpenwhiskResource{}
+}
+
+func (r *OpenwhiskResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_openwhisk"
+}
+
+func (r *OpenwhiskResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ "username": schema.StringAttribute{Required: true, MarkdownDescription: "Имя пользователя Openwhisk Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *OpenwhiskResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *OpenwhiskModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *OpenwhiskModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan OpenwhiskModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.Username.IsNull() && !plan.Username.IsUnknown() && !state.Username.IsNull() && !state.Username.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Username.ValueString(), state.Username.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить username", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.ResourceRealm.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+ if config.ResourceRealm.IsUnknown() {
+ return
+ }
+ if config.Username.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "username is required.")
+ return
+ }
+ if config.Username.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 64: resources_core.FormatString(config.Username),
+ 281: resources_core.FormatString(config.ResourceRealm),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 100, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *OpenwhiskResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data OpenwhiskModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+ if data.Username.IsNull() || data.Username.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "username is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 100, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 64: resources_core.FormatString(data.Username),
+ 281: resources_core.FormatString(data.ResourceRealm),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 100, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 100, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "username", Field: "Username", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *OpenwhiskResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state OpenwhiskModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 100, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "username", Field: "Username", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *OpenwhiskResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan OpenwhiskModel
+ var state OpenwhiskModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 100, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "username", Field: "Username", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *OpenwhiskResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state OpenwhiskModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *OpenwhiskResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *OpenwhiskResource) 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
+}
diff --git a/generated/dev/go/109_zones_v2_record_resource.go b/generated/dev/go/109_zones_v2_record_resource.go
new file mode 100644
index 0000000..9cc2073
--- /dev/null
+++ b/generated/dev/go/109_zones_v2_record_resource.go
@@ -0,0 +1,362 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: zones_v2
+// Subresource: record
+
+var _ resource.Resource = &ZonesV2RecordResource{}
+
+// ZonesV2RecordResource manages a subresource via operations.
+type ZonesV2RecordResource struct {
+ client *core.UniversalClient
+}
+
+type ZonesV2RecordModel struct {
+ ID types.String `tfsdk:"id"`
+ ZonesV2ID types.String `tfsdk:"zones_v2_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"`
+ RecordInput types.String `tfsdk:"record_input"`
+ RecordName types.String `tfsdk:"record_name"`
+ RecordTTL types.Int64 `tfsdk:"record_t_t_l"`
+ RecordType types.String `tfsdk:"record_type"`
+}
+
+func NewZonesV2RecordResource() resource.Resource {
+ return &ZonesV2RecordResource{}
+}
+
+func (r *ZonesV2RecordResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_zones_v2_record"
+}
+
+func (r *ZonesV2RecordResource) 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()}},
+ "zones_v2_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."},
+ "record_input": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Валидация производится только после запуска операции. Если при указанном типе будет указано некорректно значение, это будет описано в логе ошибки операции"},
+ "record_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя зоны в конце проставляется автоматически Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "record_t_t_l": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(3600), MarkdownDescription: "Time-to-Leave записи. Раз в какое время запись будет обновлена"},
+ "record_type": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("A")},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *ZonesV2RecordResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan ZonesV2RecordModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if plan.RecordName.IsNull() || plan.RecordName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "record_name is required.")
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.ZonesV2ID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "recordName": resources_core.FormatString(plan.RecordName),
+ }
+ identityCodes := []string{
+ "recordName",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("record", 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, "record", 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{
+ "recordName": resources_core.FormatString(plan.RecordName),
+ "recordTTL": resources_core.FormatInt64(plan.RecordTTL),
+ "recordType": resources_core.FormatString(plan.RecordType),
+ "recordInput": resources_core.FormatString(plan.RecordInput),
+ })
+
+ 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, "create_record", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_record", 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, "record", 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, "record", 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, "record", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *ZonesV2RecordResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state ZonesV2RecordModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *ZonesV2RecordResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan ZonesV2RecordModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "modify_record" == "" {
+ var state ZonesV2RecordModel
+ 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.ZonesV2ID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{
+ "recordName": resources_core.FormatString(plan.RecordName),
+ "recordTTL": resources_core.FormatInt64(plan.RecordTTL),
+ "recordType": resources_core.FormatString(plan.RecordType),
+ "recordInput": resources_core.FormatString(plan.RecordInput),
+ })
+
+ idParams := map[string]string{
+ "recordName": resources_core.FormatString(plan.RecordName),
+ }
+
+ 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, "modify_record", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "record", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *ZonesV2RecordResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state ZonesV2RecordModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_record" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.ZonesV2ID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "recordName": resources_core.FormatString(state.RecordName),
+ }
+ identityCodes := []string{
+ "recordName",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("record", 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{
+ "recordName": resources_core.FormatString(state.RecordName),
+ })
+
+ 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, "delete_record", 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 *ZonesV2RecordResource) 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
+}
diff --git a/generated/dev/go/109_zones_v2_resource.go b/generated/dev/go/109_zones_v2_resource.go
new file mode 100644
index 0000000..f07aab8
--- /dev/null
+++ b/generated/dev/go/109_zones_v2_resource.go
@@ -0,0 +1,484 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: zones_v2
+// Service ID: 109
+
+var _ resource.Resource = &ZonesV2Resource{}
+var _ resource.ResourceWithModifyPlan = &ZonesV2Resource{}
+var _ resource.ResourceWithImportState = &ZonesV2Resource{}
+
+type ZonesV2Resource struct {
+ client *core.UniversalClient
+}
+
+type ZonesV2Model struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ Records types.String `tfsdk:"records"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ ZoneExpire types.Int64 `tfsdk:"zone_expire"`
+ ZoneKind types.String `tfsdk:"zone_kind"`
+ ZoneMastersForSlave types.String `tfsdk:"zone_masters_for_slave"`
+ ZoneName types.String `tfsdk:"zone_name"`
+ ZoneRefresh types.Int64 `tfsdk:"zone_refresh"`
+ ZoneRetry types.Int64 `tfsdk:"zone_retry"`
+ ZoneTTL types.Int64 `tfsdk:"zone_t_t_l"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewZonesV2Resource() resource.Resource {
+ return &ZonesV2Resource{}
+}
+
+func (r *ZonesV2Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_zones_v2"
+}
+
+func (r *ZonesV2Resource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "records": schema.StringAttribute{Required: true},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("powerdns"), MarkdownDescription: "Не изменяется после создания."},
+ "zone_expire": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(3600000), MarkdownDescription: "Время в течение которого вторичный DNS будет пытаться завершить синхронизацию зоны с первичным Не изменяется после создания."},
+ "zone_kind": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("NATIVE"), MarkdownDescription: "Тип зоны Не изменяется после создания."},
+ "zone_masters_for_slave": schema.StringAttribute{Optional: true, MarkdownDescription: "Использовать если выбран тип SLAVE. IP адреса в формате через пробел для настройки Master-серверов Не изменяется после создания."},
+ "zone_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя зоны Не изменяется после создания."},
+ "zone_refresh": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(86400), MarkdownDescription: "Время ожидания ответа вторичного DNS перед запросом SOA-записи с первичных серверов Не изменяется после создания."},
+ "zone_retry": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(7200), MarkdownDescription: "Время в течение которого сервер повторно попробует синхронизировать информацию с первичных DNS-серверов Не изменяется после создания."},
+ "zone_t_t_l": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(86400), MarkdownDescription: "Время, в течение которого информация будет кешироваться другими DNS-серверами Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *ZonesV2Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *ZonesV2Model
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *ZonesV2Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan ZonesV2Model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneExpire.IsNull() && !plan.ZoneExpire.IsUnknown() && !state.ZoneExpire.IsNull() && !state.ZoneExpire.IsUnknown() {
+ if plan.ZoneExpire.ValueInt64() != state.ZoneExpire.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить zone_expire", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneKind.IsNull() && !plan.ZoneKind.IsUnknown() && !state.ZoneKind.IsNull() && !state.ZoneKind.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneKind.ValueString(), state.ZoneKind.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_kind", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneMastersForSlave.IsNull() && !plan.ZoneMastersForSlave.IsUnknown() && !state.ZoneMastersForSlave.IsNull() && !state.ZoneMastersForSlave.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneMastersForSlave.ValueString(), state.ZoneMastersForSlave.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_masters_for_slave", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneName.IsNull() && !plan.ZoneName.IsUnknown() && !state.ZoneName.IsNull() && !state.ZoneName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneName.ValueString(), state.ZoneName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneRefresh.IsNull() && !plan.ZoneRefresh.IsUnknown() && !state.ZoneRefresh.IsNull() && !state.ZoneRefresh.IsUnknown() {
+ if plan.ZoneRefresh.ValueInt64() != state.ZoneRefresh.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить zone_refresh", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneRetry.IsNull() && !plan.ZoneRetry.IsUnknown() && !state.ZoneRetry.IsNull() && !state.ZoneRetry.IsUnknown() {
+ if plan.ZoneRetry.ValueInt64() != state.ZoneRetry.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить zone_retry", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneTTL.IsNull() && !plan.ZoneTTL.IsUnknown() && !state.ZoneTTL.IsNull() && !state.ZoneTTL.IsUnknown() {
+ if plan.ZoneTTL.ValueInt64() != state.ZoneTTL.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить zone_t_t_l", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.Records.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "records is required.")
+ return
+ }
+ if config.Records.IsUnknown() {
+ return
+ }
+ if config.ZoneName.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "zone_name is required.")
+ return
+ }
+ if config.ZoneName.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 761: resources_core.FormatString(config.ZoneName),
+ 762: resources_core.FormatString(config.ZoneKind),
+ 763: resources_core.FormatString(config.ZoneMastersForSlave),
+ 764: resources_core.FormatInt64(config.ZoneRefresh),
+ 765: resources_core.FormatInt64(config.ZoneRetry),
+ 766: resources_core.FormatInt64(config.ZoneExpire),
+ 767: resources_core.FormatInt64(config.ZoneTTL),
+ 768: resources_core.FormatString(config.ResourceRealm),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 109, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *ZonesV2Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data ZonesV2Model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.Records.IsNull() || data.Records.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "records is required.")
+ return
+ }
+ if data.ZoneName.IsNull() || data.ZoneName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "zone_name is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 109, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 761: resources_core.FormatString(data.ZoneName),
+ 762: resources_core.FormatString(data.ZoneKind),
+ 763: resources_core.FormatString(data.ZoneMastersForSlave),
+ 764: resources_core.FormatInt64(data.ZoneRefresh),
+ 765: resources_core.FormatInt64(data.ZoneRetry),
+ 766: resources_core.FormatInt64(data.ZoneExpire),
+ 767: resources_core.FormatInt64(data.ZoneTTL),
+ 768: resources_core.FormatString(data.ResourceRealm),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 109, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 109, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "records", Field: "Records", Type: "array-map-fixed"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "zoneExpire", Field: "ZoneExpire", Type: "int64"},
+ {Code: "zoneKind", Field: "ZoneKind", Type: "string"},
+ {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"},
+ {Code: "zoneName", Field: "ZoneName", Type: "string"},
+ {Code: "zoneRefresh", Field: "ZoneRefresh", Type: "int64"},
+ {Code: "zoneRetry", Field: "ZoneRetry", Type: "int64"},
+ {Code: "zoneTTL", Field: "ZoneTTL", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *ZonesV2Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state ZonesV2Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 109, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "records", Field: "Records", Type: "array-map-fixed"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "zoneExpire", Field: "ZoneExpire", Type: "int64"},
+ {Code: "zoneKind", Field: "ZoneKind", Type: "string"},
+ {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"},
+ {Code: "zoneName", Field: "ZoneName", Type: "string"},
+ {Code: "zoneRefresh", Field: "ZoneRefresh", Type: "int64"},
+ {Code: "zoneRetry", Field: "ZoneRetry", Type: "int64"},
+ {Code: "zoneTTL", Field: "ZoneTTL", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *ZonesV2Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan ZonesV2Model
+ var state ZonesV2Model
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.Records.IsNull() != state.Records.IsNull() || plan.Records.IsUnknown() != state.Records.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.Records.IsNull() && !plan.Records.IsUnknown() && !state.Records.IsNull() && !state.Records.IsUnknown() {
+ if !strings.EqualFold(plan.Records.ValueString(), state.Records.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 769: resources_core.FormatString(plan.Records),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 109, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "records", Field: "Records", Type: "array-map-fixed"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "zoneExpire", Field: "ZoneExpire", Type: "int64"},
+ {Code: "zoneKind", Field: "ZoneKind", Type: "string"},
+ {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"},
+ {Code: "zoneName", Field: "ZoneName", Type: "string"},
+ {Code: "zoneRefresh", Field: "ZoneRefresh", Type: "int64"},
+ {Code: "zoneRetry", Field: "ZoneRetry", Type: "int64"},
+ {Code: "zoneTTL", Field: "ZoneTTL", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *ZonesV2Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state ZonesV2Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *ZonesV2Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *ZonesV2Resource) 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
+}
diff --git a/generated/dev/go/110_dnszone_resource.go b/generated/dev/go/110_dnszone_resource.go
new file mode 100644
index 0000000..fab197b
--- /dev/null
+++ b/generated/dev/go/110_dnszone_resource.go
@@ -0,0 +1,495 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: dnszone
+// Service ID: 110
+
+var _ resource.Resource = &DnszoneResource{}
+var _ resource.ResourceWithModifyPlan = &DnszoneResource{}
+var _ resource.ResourceWithImportState = &DnszoneResource{}
+
+type DnszoneResource struct {
+ client *core.UniversalClient
+}
+
+type DnszoneModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ ZoneEmail types.String `tfsdk:"zone_email"`
+ ZoneKind types.String `tfsdk:"zone_kind"`
+ ZoneMastersForSlave types.String `tfsdk:"zone_masters_for_slave"`
+ ZoneName types.String `tfsdk:"zone_name"`
+ ZoneTimeToExpire types.String `tfsdk:"zone_time_to_expire"`
+ ZoneTimeToLive types.String `tfsdk:"zone_time_to_live"`
+ ZoneTimeToRefresh types.String `tfsdk:"zone_time_to_refresh"`
+ ZoneTimeToRetry types.String `tfsdk:"zone_time_to_retry"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewDnszoneResource() resource.Resource {
+ return &DnszoneResource{}
+}
+
+func (r *DnszoneResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_dnszone"
+}
+
+func (r *DnszoneResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("powerdns"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ "zone_email": schema.StringAttribute{Required: true, MarkdownDescription: "Контактный e-mail Не изменяется после создания."},
+ "zone_kind": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("NATIVE"), MarkdownDescription: "Тип зоны Не изменяется после создания."},
+ "zone_masters_for_slave": schema.StringAttribute{Optional: true, MarkdownDescription: "Использовать если выбран тип SLAVE. IP адреса в формате через пробел для настройки Master-серверов Не изменяется после создания."},
+ "zone_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя зоны Не изменяется после создания."},
+ "zone_time_to_expire": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("3600000"), MarkdownDescription: "Время в течение которого вторичный DNS будет пытаться завершить синхронизацию зоны с первичным Не изменяется после создания."},
+ "zone_time_to_live": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("86400"), MarkdownDescription: "Время, в течение которого информация будет кешироваться другими DNS-серверами Не изменяется после создания."},
+ "zone_time_to_refresh": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("86400"), MarkdownDescription: "Время ожидания ответа вторичного DNS перед запросом SOA-записи с первичных серверов Не изменяется после создания."},
+ "zone_time_to_retry": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("7200"), MarkdownDescription: "Время в течение которого сервер повторно попробует синхронизировать информацию с первичных DNS-серверов Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *DnszoneResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *DnszoneModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *DnszoneModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan DnszoneModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneEmail.IsNull() && !plan.ZoneEmail.IsUnknown() && !state.ZoneEmail.IsNull() && !state.ZoneEmail.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneEmail.ValueString(), state.ZoneEmail.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_email", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneKind.IsNull() && !plan.ZoneKind.IsUnknown() && !state.ZoneKind.IsNull() && !state.ZoneKind.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneKind.ValueString(), state.ZoneKind.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_kind", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneMastersForSlave.IsNull() && !plan.ZoneMastersForSlave.IsUnknown() && !state.ZoneMastersForSlave.IsNull() && !state.ZoneMastersForSlave.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneMastersForSlave.ValueString(), state.ZoneMastersForSlave.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_masters_for_slave", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneName.IsNull() && !plan.ZoneName.IsUnknown() && !state.ZoneName.IsNull() && !state.ZoneName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneName.ValueString(), state.ZoneName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneTimeToExpire.IsNull() && !plan.ZoneTimeToExpire.IsUnknown() && !state.ZoneTimeToExpire.IsNull() && !state.ZoneTimeToExpire.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneTimeToExpire.ValueString(), state.ZoneTimeToExpire.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_time_to_expire", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneTimeToLive.IsNull() && !plan.ZoneTimeToLive.IsUnknown() && !state.ZoneTimeToLive.IsNull() && !state.ZoneTimeToLive.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneTimeToLive.ValueString(), state.ZoneTimeToLive.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_time_to_live", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneTimeToRefresh.IsNull() && !plan.ZoneTimeToRefresh.IsUnknown() && !state.ZoneTimeToRefresh.IsNull() && !state.ZoneTimeToRefresh.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneTimeToRefresh.ValueString(), state.ZoneTimeToRefresh.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_time_to_refresh", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneTimeToRetry.IsNull() && !plan.ZoneTimeToRetry.IsUnknown() && !state.ZoneTimeToRetry.IsNull() && !state.ZoneTimeToRetry.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneTimeToRetry.ValueString(), state.ZoneTimeToRetry.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_time_to_retry", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.ZoneEmail.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "zone_email is required.")
+ return
+ }
+ if config.ZoneEmail.IsUnknown() {
+ return
+ }
+ if config.ZoneName.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "zone_name is required.")
+ return
+ }
+ if config.ZoneName.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 66: resources_core.FormatString(config.ZoneEmail),
+ 67: resources_core.FormatString(config.ZoneName),
+ 68: resources_core.FormatString(config.ZoneKind),
+ 69: resources_core.FormatString(config.ZoneMastersForSlave),
+ 70: resources_core.FormatString(config.ZoneTimeToRefresh),
+ 71: resources_core.FormatString(config.ZoneTimeToRetry),
+ 72: resources_core.FormatString(config.ZoneTimeToExpire),
+ 73: resources_core.FormatString(config.ZoneTimeToLive),
+ 284: resources_core.FormatString(config.ResourceRealm),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 110, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *DnszoneResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data DnszoneModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.ZoneEmail.IsNull() || data.ZoneEmail.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "zone_email is required.")
+ return
+ }
+ if data.ZoneName.IsNull() || data.ZoneName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "zone_name is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 110, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 66: resources_core.FormatString(data.ZoneEmail),
+ 67: resources_core.FormatString(data.ZoneName),
+ 68: resources_core.FormatString(data.ZoneKind),
+ 69: resources_core.FormatString(data.ZoneMastersForSlave),
+ 70: resources_core.FormatString(data.ZoneTimeToRefresh),
+ 71: resources_core.FormatString(data.ZoneTimeToRetry),
+ 72: resources_core.FormatString(data.ZoneTimeToExpire),
+ 73: resources_core.FormatString(data.ZoneTimeToLive),
+ 284: resources_core.FormatString(data.ResourceRealm),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 110, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 110, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "zoneEmail", Field: "ZoneEmail", Type: "string"},
+ {Code: "zoneKind", Field: "ZoneKind", Type: "string"},
+ {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"},
+ {Code: "zoneName", Field: "ZoneName", Type: "string"},
+ {Code: "zoneTimeToExpire", Field: "ZoneTimeToExpire", Type: "string"},
+ {Code: "zoneTimeToLive", Field: "ZoneTimeToLive", Type: "string"},
+ {Code: "zoneTimeToRefresh", Field: "ZoneTimeToRefresh", Type: "string"},
+ {Code: "zoneTimeToRetry", Field: "ZoneTimeToRetry", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *DnszoneResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state DnszoneModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 110, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "zoneEmail", Field: "ZoneEmail", Type: "string"},
+ {Code: "zoneKind", Field: "ZoneKind", Type: "string"},
+ {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"},
+ {Code: "zoneName", Field: "ZoneName", Type: "string"},
+ {Code: "zoneTimeToExpire", Field: "ZoneTimeToExpire", Type: "string"},
+ {Code: "zoneTimeToLive", Field: "ZoneTimeToLive", Type: "string"},
+ {Code: "zoneTimeToRefresh", Field: "ZoneTimeToRefresh", Type: "string"},
+ {Code: "zoneTimeToRetry", Field: "ZoneTimeToRetry", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *DnszoneResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan DnszoneModel
+ var state DnszoneModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 110, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "zoneEmail", Field: "ZoneEmail", Type: "string"},
+ {Code: "zoneKind", Field: "ZoneKind", Type: "string"},
+ {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"},
+ {Code: "zoneName", Field: "ZoneName", Type: "string"},
+ {Code: "zoneTimeToExpire", Field: "ZoneTimeToExpire", Type: "string"},
+ {Code: "zoneTimeToLive", Field: "ZoneTimeToLive", Type: "string"},
+ {Code: "zoneTimeToRefresh", Field: "ZoneTimeToRefresh", Type: "string"},
+ {Code: "zoneTimeToRetry", Field: "ZoneTimeToRetry", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *DnszoneResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state DnszoneModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *DnszoneResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *DnszoneResource) 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
+}
diff --git a/generated/dev/go/111_dnsrecord_resource.go b/generated/dev/go/111_dnsrecord_resource.go
new file mode 100644
index 0000000..b28d2d8
--- /dev/null
+++ b/generated/dev/go/111_dnsrecord_resource.go
@@ -0,0 +1,546 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: dnsrecord
+// Service ID: 111
+
+var _ resource.Resource = &DnsrecordResource{}
+var _ resource.ResourceWithModifyPlan = &DnsrecordResource{}
+var _ resource.ResourceWithImportState = &DnsrecordResource{}
+
+type DnsrecordResource struct {
+ client *core.UniversalClient
+}
+
+type DnsrecordModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ RecordInput types.String `tfsdk:"record_input"`
+ RecordName types.String `tfsdk:"record_name"`
+ RecordTTL types.Int64 `tfsdk:"record_t_t_l"`
+ RecordType types.String `tfsdk:"record_type"`
+ ZoneUid types.String `tfsdk:"zone_uid"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewDnsrecordResource() resource.Resource {
+ return &DnsrecordResource{}
+}
+
+func (r *DnsrecordResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_dnsrecord"
+}
+
+func (r *DnsrecordResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "record_input": schema.StringAttribute{Required: true, MarkdownDescription: "Значение. Необходима валидация, в зависимости от типа записи. Должно быть заменено на UUID-внешнийIP"},
+ "record_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя записи. Имя зоны в конце проставляется автоматически Не изменяется после создания."},
+ "record_t_t_l": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(3600), MarkdownDescription: "Time-to-Leave записи. Раз в какое время запись будет обновлена Не изменяется после создания."},
+ "record_type": schema.StringAttribute{Required: true, MarkdownDescription: "Тип DNS-записи Не изменяется после создания."},
+ "zone_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID экземпляра услуги DNS-зоны Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *DnsrecordResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *DnsrecordModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *DnsrecordModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan DnsrecordModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.RecordName.IsNull() && !plan.RecordName.IsUnknown() && !state.RecordName.IsNull() && !state.RecordName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.RecordName.ValueString(), state.RecordName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить record_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.RecordTTL.IsNull() && !plan.RecordTTL.IsUnknown() && !state.RecordTTL.IsNull() && !state.RecordTTL.IsUnknown() {
+ if plan.RecordTTL.ValueInt64() != state.RecordTTL.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить record_t_t_l", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.RecordType.IsNull() && !plan.RecordType.IsUnknown() && !state.RecordType.IsNull() && !state.RecordType.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.RecordType.ValueString(), state.RecordType.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить record_type", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneUid.IsNull() && !plan.ZoneUid.IsUnknown() && !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneUid.ValueString(), state.ZoneUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.RecordInput.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "record_input is required.")
+ return
+ }
+ if config.RecordInput.IsUnknown() {
+ return
+ }
+ if config.RecordName.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "record_name is required.")
+ return
+ }
+ if config.RecordName.IsUnknown() {
+ return
+ }
+ if config.RecordType.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "record_type is required.")
+ return
+ }
+ if config.RecordType.IsUnknown() {
+ return
+ }
+ if config.ZoneUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "zone_uid is required.")
+ return
+ }
+ if config.ZoneUid.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.ZoneUid.IsNull() && !config.ZoneUid.IsUnknown() {
+ resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 109, config.ZoneUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve zone_uid", err.Error())
+ } else if resolvedZoneUid != "" && resolvedZoneUid != config.ZoneUid.ValueString() {
+ config.ZoneUid = types.StringValue(resolvedZoneUid)
+ }
+ }
+ params := map[int]string{
+ 75: resources_core.FormatString(config.ZoneUid),
+ 76: resources_core.FormatString(config.RecordType),
+ 77: resources_core.FormatString(config.RecordName),
+ 78: resources_core.FormatString(config.RecordInput),
+ 450: resources_core.FormatInt64(config.RecordTTL),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 111, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *DnsrecordResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data DnsrecordModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.RecordInput.IsNull() || data.RecordInput.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "record_input is required.")
+ return
+ }
+ if data.RecordName.IsNull() || data.RecordName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "record_name is required.")
+ return
+ }
+ if data.RecordType.IsNull() || data.RecordType.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "record_type is required.")
+ return
+ }
+ if data.ZoneUid.IsNull() || data.ZoneUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "zone_uid is required.")
+ return
+ }
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalZoneUid := data.ZoneUid
+ if !data.ZoneUid.IsNull() && !data.ZoneUid.IsUnknown() {
+ resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 109, data.ZoneUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedZoneUid != "" && resolvedZoneUid != data.ZoneUid.ValueString() {
+ data.ZoneUid = types.StringValue(resolvedZoneUid)
+ }
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 111, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 75: resources_core.FormatString(data.ZoneUid),
+ 76: resources_core.FormatString(data.RecordType),
+ 77: resources_core.FormatString(data.RecordName),
+ 78: resources_core.FormatString(data.RecordInput),
+ 450: resources_core.FormatInt64(data.RecordTTL),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 111, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 111, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "recordInput", Field: "RecordInput", Type: "string"},
+ {Code: "recordName", Field: "RecordName", Type: "string"},
+ {Code: "recordTTL", Field: "RecordTTL", Type: "int64"},
+ {Code: "recordType", Field: "RecordType", Type: "string"},
+ {Code: "zoneUid", Field: "ZoneUid", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() {
+ resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 109, state.ZoneUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve zone_uid", err.Error())
+ } else if resolvedZoneUid != "" && resolvedZoneUid != state.ZoneUid.ValueString() {
+ state.ZoneUid = types.StringValue(resolvedZoneUid)
+ }
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+ if !originalZoneUid.IsNull() && !originalZoneUid.IsUnknown() && !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() {
+ if strings.EqualFold(state.ZoneUid.ValueString(), originalZoneUid.ValueString()) {
+ state.ZoneUid = originalZoneUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *DnsrecordResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state DnsrecordModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 111, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "recordInput", Field: "RecordInput", Type: "string"},
+ {Code: "recordName", Field: "RecordName", Type: "string"},
+ {Code: "recordTTL", Field: "RecordTTL", Type: "int64"},
+ {Code: "recordType", Field: "RecordType", Type: "string"},
+ {Code: "zoneUid", Field: "ZoneUid", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !newState.ZoneUid.IsNull() && !newState.ZoneUid.IsUnknown() {
+ resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 109, newState.ZoneUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve zone_uid", err.Error())
+ } else if resolvedZoneUid != "" && resolvedZoneUid != newState.ZoneUid.ValueString() {
+ newState.ZoneUid = types.StringValue(resolvedZoneUid)
+ }
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+ if !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() && !newState.ZoneUid.IsNull() && !newState.ZoneUid.IsUnknown() {
+ if strings.EqualFold(newState.ZoneUid.ValueString(), state.ZoneUid.ValueString()) {
+ newState.ZoneUid = state.ZoneUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *DnsrecordResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan DnsrecordModel
+ var state DnsrecordModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.RecordInput.IsNull() != state.RecordInput.IsNull() || plan.RecordInput.IsUnknown() != state.RecordInput.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.RecordInput.IsNull() && !plan.RecordInput.IsUnknown() && !state.RecordInput.IsNull() && !state.RecordInput.IsUnknown() {
+ if !strings.EqualFold(plan.RecordInput.ValueString(), state.RecordInput.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 256: resources_core.FormatString(plan.RecordInput),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 111, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "recordInput", Field: "RecordInput", Type: "string"},
+ {Code: "recordName", Field: "RecordName", Type: "string"},
+ {Code: "recordTTL", Field: "RecordTTL", Type: "int64"},
+ {Code: "recordType", Field: "RecordType", Type: "string"},
+ {Code: "zoneUid", Field: "ZoneUid", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() {
+ resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 109, state.ZoneUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve zone_uid", err.Error())
+ } else if resolvedZoneUid != "" && resolvedZoneUid != state.ZoneUid.ValueString() {
+ state.ZoneUid = types.StringValue(resolvedZoneUid)
+ }
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+ if !plan.ZoneUid.IsNull() && !plan.ZoneUid.IsUnknown() && !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() {
+ if strings.EqualFold(state.ZoneUid.ValueString(), plan.ZoneUid.ValueString()) {
+ state.ZoneUid = plan.ZoneUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *DnsrecordResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state DnsrecordModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *DnsrecordResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *DnsrecordResource) 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
+}
diff --git a/generated/dev/go/112_tenant_resource.go b/generated/dev/go/112_tenant_resource.go
new file mode 100644
index 0000000..57ca92c
--- /dev/null
+++ b/generated/dev/go/112_tenant_resource.go
@@ -0,0 +1,372 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: tenant
+// Service ID: 112
+
+var _ resource.Resource = &TenantResource{}
+var _ resource.ResourceWithModifyPlan = &TenantResource{}
+var _ resource.ResourceWithImportState = &TenantResource{}
+
+type TenantResource struct {
+ client *core.UniversalClient
+}
+
+type TenantModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ GrafanaOrgName types.String `tfsdk:"grafana_org_name"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewTenantResource() resource.Resource {
+ return &TenantResource{}
+}
+
+func (r *TenantResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_tenant"
+}
+
+func (r *TenantResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "grafana_org_name": schema.StringAttribute{Required: true, MarkdownDescription: "WZ-Код клиента Не изменяется после создания."},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("grafana.ngcloud.ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *TenantResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *TenantModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *TenantModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan TenantModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.GrafanaOrgName.IsNull() && !plan.GrafanaOrgName.IsUnknown() && !state.GrafanaOrgName.IsNull() && !state.GrafanaOrgName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.GrafanaOrgName.ValueString(), state.GrafanaOrgName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить grafana_org_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.GrafanaOrgName.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "grafana_org_name is required.")
+ return
+ }
+ if config.GrafanaOrgName.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 239: resources_core.FormatString(config.GrafanaOrgName),
+ 240: resources_core.FormatString(config.ResourceRealm),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 112, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *TenantResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data TenantModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.GrafanaOrgName.IsNull() || data.GrafanaOrgName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "grafana_org_name is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 112, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 239: resources_core.FormatString(data.GrafanaOrgName),
+ 240: resources_core.FormatString(data.ResourceRealm),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 112, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 112, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "grafanaOrgName", Field: "GrafanaOrgName", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *TenantResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state TenantModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 112, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "grafanaOrgName", Field: "GrafanaOrgName", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *TenantResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan TenantModel
+ var state TenantModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 112, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "grafanaOrgName", Field: "GrafanaOrgName", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *TenantResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state TenantModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *TenantResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *TenantResource) 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
+}
diff --git a/generated/dev/go/113_vc_complex_resource.go b/generated/dev/go/113_vc_complex_resource.go
new file mode 100644
index 0000000..f8eace6
--- /dev/null
+++ b/generated/dev/go/113_vc_complex_resource.go
@@ -0,0 +1,563 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_complex
+// Service ID: 113
+
+var _ resource.Resource = &VcComplexResource{}
+var _ resource.ResourceWithModifyPlan = &VcComplexResource{}
+var _ resource.ResourceWithImportState = &VcComplexResource{}
+
+type VcComplexResource struct {
+ client *core.UniversalClient
+}
+
+type VcComplexModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ CpuAllocated types.String `tfsdk:"cpu_allocated"`
+ CpuGuaranteed types.Int64 `tfsdk:"cpu_guaranteed"`
+ IpSpaceName types.String `tfsdk:"ip_space_name"`
+ MemAllocated types.Int64 `tfsdk:"mem_allocated"`
+ MemGuaranteed types.Int64 `tfsdk:"mem_guaranteed"`
+ NeedEnableAVI types.Bool `tfsdk:"need_enable_a_v_i"`
+ NeedExternalAddressSNAT types.Bool `tfsdk:"need_external_address_s_n_a_t"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SegroupName types.String `tfsdk:"segroup_name"`
+ StorageConfig types.String `tfsdk:"storage_config"`
+ VIPCount types.Int64 `tfsdk:"v_i_p_count"`
+ VappName types.String `tfsdk:"vapp_name"`
+ VdcNetworkPool types.String `tfsdk:"vdc_network_pool"`
+ VdcProviderGateway types.String `tfsdk:"vdc_provider_gateway"`
+ VirtualServicesCount types.String `tfsdk:"virtual_services_count"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVcComplexResource() resource.Resource {
+ return &VcComplexResource{}
+}
+
+func (r *VcComplexResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_complex"
+}
+
+func (r *VcComplexResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "cpu_allocated": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("1"), MarkdownDescription: "шт Не изменяется после создания."},
+ "cpu_guaranteed": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(20), MarkdownDescription: "Не изменяется после создания."},
+ "ip_space_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("internet-ipv4-v1"), MarkdownDescription: "Имя ip Space для внешнего IP. Целесообразнее брать самый дешёвый, так как это vip для исходящего трафика. Необходимо задавать если включен needExternalAddressSNAT Не изменяется после создания."},
+ "mem_allocated": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "gb Не изменяется после создания."},
+ "mem_guaranteed": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(20), MarkdownDescription: "Не изменяется после создания."},
+ "need_enable_a_v_i": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает AVI в Cloud Director Не изменяется после создания."},
+ "need_external_address_s_n_a_t": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Выделяет внешний IP на Edge. Используется для SNAT всего трафика Не изменяется после создания."},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("sandbox.nubes.ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ "segroup_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("SEGROUP-SANDBOX-CL1-SHARED-01"), MarkdownDescription: "Наименование segroup, в котором будут выделяться пулы на AVI Не изменяется после создания."},
+ "storage_config": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{\"name\": \"\", \"size\": 1024}]"), MarkdownDescription: "Описывается как массив объектов, где описывается имя политики и её размер Не изменяется после создания.", PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()}},
+ "v_i_p_count": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "шт Не изменяется после создания."},
+ "vapp_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя vApp Не изменяется после создания."},
+ "vdc_network_pool": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("nsxt-sandbox-geneve-np"), MarkdownDescription: "Не изменяется после создания."},
+ "vdc_provider_gateway": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("sandbox-v1cl1-pvdc"), MarkdownDescription: "Взять из свойств ресурсной платформы.
Путь .vcd.providerVdc - взять KEY
providerGateway Cloud Director. Влияет ресурсы будут предоставлены. Является полем (List<String>) в свойствах рес. платформы ЛК, но сам параметр передаётся только как один String Не изменяется после создания."},
+ "virtual_services_count": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("1"), MarkdownDescription: "Кол-во виртуальных сервисов, которые выделяются на AVI. Во избежании коллапса пока выделяется до 4 Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcComplexResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VcComplexModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VcComplexModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VcComplexModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.CpuAllocated.IsNull() && !plan.CpuAllocated.IsUnknown() && !state.CpuAllocated.IsNull() && !state.CpuAllocated.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.CpuAllocated.ValueString(), state.CpuAllocated.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить cpu_allocated", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.CpuGuaranteed.IsNull() && !plan.CpuGuaranteed.IsUnknown() && !state.CpuGuaranteed.IsNull() && !state.CpuGuaranteed.IsUnknown() {
+ if plan.CpuGuaranteed.ValueInt64() != state.CpuGuaranteed.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить cpu_guaranteed", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.IpSpaceName.IsNull() && !plan.IpSpaceName.IsUnknown() && !state.IpSpaceName.IsNull() && !state.IpSpaceName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.IpSpaceName.ValueString(), state.IpSpaceName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить ip_space_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.MemAllocated.IsNull() && !plan.MemAllocated.IsUnknown() && !state.MemAllocated.IsNull() && !state.MemAllocated.IsUnknown() {
+ if plan.MemAllocated.ValueInt64() != state.MemAllocated.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить mem_allocated", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.MemGuaranteed.IsNull() && !plan.MemGuaranteed.IsUnknown() && !state.MemGuaranteed.IsNull() && !state.MemGuaranteed.IsUnknown() {
+ if plan.MemGuaranteed.ValueInt64() != state.MemGuaranteed.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить mem_guaranteed", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.NeedEnableAVI.IsNull() && !plan.NeedEnableAVI.IsUnknown() && !state.NeedEnableAVI.IsNull() && !state.NeedEnableAVI.IsUnknown() {
+ if plan.NeedEnableAVI.ValueBool() != state.NeedEnableAVI.ValueBool() {
+ resp.Diagnostics.AddError("Нельзя изменить need_enable_a_v_i", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.NeedExternalAddressSNAT.IsNull() && !plan.NeedExternalAddressSNAT.IsUnknown() && !state.NeedExternalAddressSNAT.IsNull() && !state.NeedExternalAddressSNAT.IsUnknown() {
+ if plan.NeedExternalAddressSNAT.ValueBool() != state.NeedExternalAddressSNAT.ValueBool() {
+ resp.Diagnostics.AddError("Нельзя изменить need_external_address_s_n_a_t", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.SegroupName.IsNull() && !plan.SegroupName.IsUnknown() && !state.SegroupName.IsNull() && !state.SegroupName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.SegroupName.ValueString(), state.SegroupName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить segroup_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.StorageConfig.IsNull() && !plan.StorageConfig.IsUnknown() && !state.StorageConfig.IsNull() && !state.StorageConfig.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.StorageConfig.ValueString(), state.StorageConfig.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить storage_config", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VIPCount.IsNull() && !plan.VIPCount.IsUnknown() && !state.VIPCount.IsNull() && !state.VIPCount.IsUnknown() {
+ if plan.VIPCount.ValueInt64() != state.VIPCount.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить v_i_p_count", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VappName.IsNull() && !plan.VappName.IsUnknown() && !state.VappName.IsNull() && !state.VappName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VappName.ValueString(), state.VappName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vapp_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VdcNetworkPool.IsNull() && !plan.VdcNetworkPool.IsUnknown() && !state.VdcNetworkPool.IsNull() && !state.VdcNetworkPool.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VdcNetworkPool.ValueString(), state.VdcNetworkPool.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vdc_network_pool", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VdcProviderGateway.IsNull() && !plan.VdcProviderGateway.IsUnknown() && !state.VdcProviderGateway.IsNull() && !state.VdcProviderGateway.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VdcProviderGateway.ValueString(), state.VdcProviderGateway.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vdc_provider_gateway", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VirtualServicesCount.IsNull() && !plan.VirtualServicesCount.IsUnknown() && !state.VirtualServicesCount.IsNull() && !state.VirtualServicesCount.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VirtualServicesCount.ValueString(), state.VirtualServicesCount.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить virtual_services_count", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.VappName.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "vapp_name is required.")
+ return
+ }
+ if config.VappName.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 332: resources_core.FormatString(config.ResourceRealm),
+ 334: resources_core.FormatString(config.VappName),
+ 358: resources_core.FormatString(config.VdcProviderGateway),
+ 359: resources_core.FormatString(config.VirtualServicesCount),
+ 360: resources_core.FormatString(config.IpSpaceName),
+ 399: resources_core.FormatString(config.StorageConfig),
+ 400: resources_core.FormatString(config.VdcNetworkPool),
+ 401: resources_core.FormatInt64(config.CpuGuaranteed),
+ 402: resources_core.FormatInt64(config.MemGuaranteed),
+ 403: resources_core.FormatString(config.SegroupName),
+ 404: resources_core.FormatBool(config.NeedExternalAddressSNAT),
+ 405: resources_core.FormatBool(config.NeedEnableAVI),
+ 566: resources_core.FormatString(config.CpuAllocated),
+ 567: resources_core.FormatInt64(config.MemAllocated),
+ 568: resources_core.FormatInt64(config.VIPCount),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 113, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VcComplexResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VcComplexModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.VappName.IsNull() || data.VappName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "vapp_name is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 113, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 332: resources_core.FormatString(data.ResourceRealm),
+ 334: resources_core.FormatString(data.VappName),
+ 358: resources_core.FormatString(data.VdcProviderGateway),
+ 359: resources_core.FormatString(data.VirtualServicesCount),
+ 360: resources_core.FormatString(data.IpSpaceName),
+ 399: resources_core.FormatString(data.StorageConfig),
+ 400: resources_core.FormatString(data.VdcNetworkPool),
+ 401: resources_core.FormatInt64(data.CpuGuaranteed),
+ 402: resources_core.FormatInt64(data.MemGuaranteed),
+ 403: resources_core.FormatString(data.SegroupName),
+ 404: resources_core.FormatBool(data.NeedExternalAddressSNAT),
+ 405: resources_core.FormatBool(data.NeedEnableAVI),
+ 566: resources_core.FormatString(data.CpuAllocated),
+ 567: resources_core.FormatInt64(data.MemAllocated),
+ 568: resources_core.FormatInt64(data.VIPCount),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 113, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 113, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "cpuAllocated", Field: "CpuAllocated", Type: "string"},
+ {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "memAllocated", Field: "MemAllocated", Type: "int64"},
+ {Code: "memGuaranteed", Field: "MemGuaranteed", Type: "int64"},
+ {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"},
+ {Code: "needExternalAddressSNAT", Field: "NeedExternalAddressSNAT", Type: "bool"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "segroupName", Field: "SegroupName", Type: "string"},
+ {Code: "storageConfig", Field: "StorageConfig", Type: "string"},
+ {Code: "vIPCount", Field: "VIPCount", Type: "int64"},
+ {Code: "vappName", Field: "VappName", Type: "string"},
+ {Code: "vdcNetworkPool", Field: "VdcNetworkPool", Type: "string"},
+ {Code: "vdcProviderGateway", Field: "VdcProviderGateway", Type: "string"},
+ {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcComplexResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcComplexModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 113, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "cpuAllocated", Field: "CpuAllocated", Type: "string"},
+ {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "memAllocated", Field: "MemAllocated", Type: "int64"},
+ {Code: "memGuaranteed", Field: "MemGuaranteed", Type: "int64"},
+ {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"},
+ {Code: "needExternalAddressSNAT", Field: "NeedExternalAddressSNAT", Type: "bool"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "segroupName", Field: "SegroupName", Type: "string"},
+ {Code: "storageConfig", Field: "StorageConfig", Type: "string"},
+ {Code: "vIPCount", Field: "VIPCount", Type: "int64"},
+ {Code: "vappName", Field: "VappName", Type: "string"},
+ {Code: "vdcNetworkPool", Field: "VdcNetworkPool", Type: "string"},
+ {Code: "vdcProviderGateway", Field: "VdcProviderGateway", Type: "string"},
+ {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VcComplexResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcComplexModel
+ var state VcComplexModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 113, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "cpuAllocated", Field: "CpuAllocated", Type: "string"},
+ {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "memAllocated", Field: "MemAllocated", Type: "int64"},
+ {Code: "memGuaranteed", Field: "MemGuaranteed", Type: "int64"},
+ {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"},
+ {Code: "needExternalAddressSNAT", Field: "NeedExternalAddressSNAT", Type: "bool"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "segroupName", Field: "SegroupName", Type: "string"},
+ {Code: "storageConfig", Field: "StorageConfig", Type: "string"},
+ {Code: "vIPCount", Field: "VIPCount", Type: "int64"},
+ {Code: "vappName", Field: "VappName", Type: "string"},
+ {Code: "vdcNetworkPool", Field: "VdcNetworkPool", Type: "string"},
+ {Code: "vdcProviderGateway", Field: "VdcProviderGateway", Type: "string"},
+ {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcComplexResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcComplexModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VcComplexResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VcComplexResource) 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
+}
diff --git a/generated/dev/go/115_mariadb_database_resource.go b/generated/dev/go/115_mariadb_database_resource.go
new file mode 100644
index 0000000..cd6dd52
--- /dev/null
+++ b/generated/dev/go/115_mariadb_database_resource.go
@@ -0,0 +1,343 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: mariadb
+// Subresource: database
+
+var _ resource.Resource = &MariadbDatabaseResource{}
+
+// MariadbDatabaseResource manages a subresource via operations.
+type MariadbDatabaseResource struct {
+ client *core.UniversalClient
+}
+
+type MariadbDatabaseModel struct {
+ ID types.String `tfsdk:"id"`
+ MariadbID types.String `tfsdk:"mariadb_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"`
+ DbName types.String `tfsdk:"db_name"`
+}
+
+func NewMariadbDatabaseResource() resource.Resource {
+ return &MariadbDatabaseResource{}
+}
+
+func (r *MariadbDatabaseResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_mariadb_database"
+}
+
+func (r *MariadbDatabaseResource) 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()}},
+ "mariadb_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."},
+ "db_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("db-dxinlvxx"), MarkdownDescription: "Пример: `db01` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *MariadbDatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan MariadbDatabaseModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.MariadbID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "dbName": resources_core.FormatString(plan.DbName),
+ }
+ identityCodes := []string{
+ "dbName",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", 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, "database", 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{
+ "dbName": resources_core.FormatString(plan.DbName),
+ })
+
+ 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, "create_database", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_database", 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, "database", 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, "database", 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, "database", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *MariadbDatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state MariadbDatabaseModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *MariadbDatabaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan MariadbDatabaseModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state MariadbDatabaseModel
+ 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.MariadbID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "dbName": resources_core.FormatString(plan.DbName),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *MariadbDatabaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state MariadbDatabaseModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_database" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.MariadbID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "dbName": resources_core.FormatString(state.DbName),
+ }
+ identityCodes := []string{
+ "dbName",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", 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{
+ "dbName": resources_core.FormatString(state.DbName),
+ })
+
+ 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, "delete_database", 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 *MariadbDatabaseResource) 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
+}
diff --git a/generated/dev/go/115_mariadb_resource.go b/generated/dev/go/115_mariadb_resource.go
new file mode 100644
index 0000000..8ed7b1d
--- /dev/null
+++ b/generated/dev/go/115_mariadb_resource.go
@@ -0,0 +1,447 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: mariadb
+// Service ID: 115
+
+var _ resource.Resource = &MariadbResource{}
+var _ resource.ResourceWithModifyPlan = &MariadbResource{}
+var _ resource.ResourceWithImportState = &MariadbResource{}
+
+type MariadbResource struct {
+ client *core.UniversalClient
+}
+
+// MariadbAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type MariadbAccessConfigurationModel struct {
+ MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"`
+ MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"`
+}
+
+// MariadbAutoscaleConfigurationModel — вложенная модель для map-fixed параметра autoscaleConfiguration.
+type MariadbAutoscaleConfigurationModel struct {
+ Enabled types.Bool `tfsdk:"enabled" json:"enabled"`
+ Schedule types.Int64 `tfsdk:"schedule" json:"schedule"`
+ Quota types.Int64 `tfsdk:"quota" json:"quota"`
+ Percent types.Int64 `tfsdk:"percent" json:"percent"`
+}
+
+// MariadbBackupConfigurationModel — вложенная модель для map-fixed параметра backupConfiguration.
+type MariadbBackupConfigurationModel struct {
+ S3Uid types.String `tfsdk:"s3_uid" json:"s3Uid"`
+ Retain types.Int64 `tfsdk:"retain" json:"retain"`
+ Schedule types.String `tfsdk:"schedule" json:"schedule"`
+}
+
+// MariadbClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type MariadbClusterConfigurationModel struct {
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+ Disk types.Int64 `tfsdk:"disk" json:"disk"`
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+}
+
+// MariadbMariadbConfigurationModel — вложенная модель для map-fixed параметра mariadbConfiguration.
+type MariadbMariadbConfigurationModel struct {
+ Version types.String `tfsdk:"version" json:"version"`
+}
+
+// MariadbStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type MariadbStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+}
+
+type MariadbModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AccessConfiguration *MariadbAccessConfigurationModel `tfsdk:"access_configuration"`
+ AutoscaleConfiguration *MariadbAutoscaleConfigurationModel `tfsdk:"autoscale_configuration"`
+ BackupConfiguration *MariadbBackupConfigurationModel `tfsdk:"backup_configuration"`
+ ClusterConfiguration *MariadbClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ MariadbConfiguration *MariadbMariadbConfigurationModel `tfsdk:"mariadb_configuration"`
+ StartupConfiguration *MariadbStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewMariadbResource() resource.Resource {
+ return &MariadbResource{}
+}
+
+func (r *MariadbResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_mariadb"
+}
+
+func (r *MariadbResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "master_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"},
+ "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"},
+ }},
+ "autoscale_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "enabled": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает автоскейлинг PV. При включении параметра необходимо настроить autoScalePercentage и autoScaleTechWindow. Алерт при срабатывании вызывает modify у текущей услуги"},
+ "schedule": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "Требует включения autoScale. Окно обновления (пока не реализовано). Указывается час в виде Integer (0 / 5 / 12 / 23)"},
+ "quota": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100), MarkdownDescription: "До какого размера возможно увеличивать диск, когда включен Autoscale"},
+ "percent": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Требует включения autoScale. Позволяет указать в процентах расширение от текущего макс объема
Минимальный размер на который расширяется - **1Gb**"},
+ }},
+ "backup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "s3_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Экземпляр S3 обязателен"},
+ "retain": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(720)},
+ "schedule": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("0 0 * * *")},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Указывается в Megabytes"},
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Пока `Galera` выключена
Как будет запрос на включение и тестирование, реплики можно будет увеличивать"},
+ "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Указывается в GIGAbytes"},
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ }},
+ "mariadb_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("9.4.0")},
+ }},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"},
+ }},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *MariadbResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *MariadbModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *MariadbModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan MariadbModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 877: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}),
+ 878: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64())}),
+ 879: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString())}),
+ 880: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.MariadbConfiguration.Version.ValueString())}),
+ 881: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", config.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", config.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", config.BackupConfiguration.Schedule.ValueString())}),
+ 882: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", config.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", config.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", config.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", config.AutoscaleConfiguration.Percent.ValueInt64())}),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 115, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *MariadbResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data MariadbModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 115, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 877: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}),
+ 878: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64())}),
+ 879: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString())}),
+ 880: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.MariadbConfiguration.Version.ValueString())}),
+ 881: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", data.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", data.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", data.BackupConfiguration.Schedule.ValueString())}),
+ 882: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", data.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", data.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", data.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", data.AutoscaleConfiguration.Percent.ValueInt64())}),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 115, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 115, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"},
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "mariadbConfiguration", Field: "MariadbConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *MariadbResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state MariadbModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 115, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"},
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "mariadbConfiguration", Field: "MariadbConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *MariadbResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan MariadbModel
+ var state MariadbModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 883: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64())}),
+ 884: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString())}),
+ 885: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.MariadbConfiguration.Version.ValueString())}),
+ 886: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", plan.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.Schedule.ValueString())}),
+ 887: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", plan.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Percent.ValueInt64())}),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 115, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"},
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "mariadbConfiguration", Field: "MariadbConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *MariadbResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state MariadbModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *MariadbResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *MariadbResource) 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
+}
diff --git a/generated/dev/go/115_mariadb_user_resource.go b/generated/dev/go/115_mariadb_user_resource.go
new file mode 100644
index 0000000..5a2f790
--- /dev/null
+++ b/generated/dev/go/115_mariadb_user_resource.go
@@ -0,0 +1,367 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: mariadb
+// Subresource: user
+
+var _ resource.Resource = &MariadbUserResource{}
+
+// MariadbUserResource manages a subresource via operations.
+type MariadbUserResource struct {
+ client *core.UniversalClient
+}
+
+type MariadbUserModel struct {
+ ID types.String `tfsdk:"id"`
+ MariadbID types.String `tfsdk:"mariadb_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"`
+ AccessHosts types.String `tfsdk:"access_hosts"`
+ DbName types.String `tfsdk:"db_name"`
+ Password types.String `tfsdk:"password"`
+ Username types.String `tfsdk:"username"`
+}
+
+func NewMariadbUserResource() resource.Resource {
+ return &MariadbUserResource{}
+}
+
+func (r *MariadbUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_mariadb_user"
+}
+
+func (r *MariadbUserResource) 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()}},
+ "mariadb_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."},
+ "access_hosts": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("%"), MarkdownDescription: "Пример: `%` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "db_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Необходимо указать массив существующих баз Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "password": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("mhs3keq0"), MarkdownDescription: "Секрет будет записан в хранилище секретов и не будет отображаться публично Не изменяется после создания. Изменение требует пересоздания.", Sensitive: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "username": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("userz2hsgoyo"), MarkdownDescription: "Пример: `myusername` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *MariadbUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan MariadbUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.MariadbID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "accessHosts": resources_core.FormatString(plan.AccessHosts),
+ "dbName": resources_core.FormatString(plan.DbName),
+ "password": resources_core.FormatString(plan.Password),
+ "username": resources_core.FormatString(plan.Username),
+ }
+ identityCodes := []string{
+ "accessHosts",
+ "dbName",
+ "password",
+ "username",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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, "user", 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{
+ "username": resources_core.FormatString(plan.Username),
+ "password": resources_core.FormatString(plan.Password),
+ "dbName": resources_core.FormatString(plan.DbName),
+ "accessHosts": resources_core.FormatString(plan.AccessHosts),
+ })
+
+ 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, "create_user", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", 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, "user", 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, "user", 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, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *MariadbUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state MariadbUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *MariadbUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan MariadbUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state MariadbUserModel
+ 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.MariadbID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "accessHosts": resources_core.FormatString(plan.AccessHosts),
+ "dbName": resources_core.FormatString(plan.DbName),
+ "password": resources_core.FormatString(plan.Password),
+ "username": resources_core.FormatString(plan.Username),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *MariadbUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state MariadbUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_user" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.MariadbID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "accessHosts": resources_core.FormatString(state.AccessHosts),
+ "dbName": resources_core.FormatString(state.DbName),
+ "password": resources_core.FormatString(state.Password),
+ "username": resources_core.FormatString(state.Username),
+ }
+ identityCodes := []string{
+ "accessHosts",
+ "dbName",
+ "password",
+ "username",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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{
+ "username": resources_core.FormatString(state.Username),
+ })
+
+ 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, "delete_user", 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 *MariadbUserResource) 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
+}
diff --git a/generated/dev/go/116_kafka_resource.go b/generated/dev/go/116_kafka_resource.go
new file mode 100644
index 0000000..402172e
--- /dev/null
+++ b/generated/dev/go/116_kafka_resource.go
@@ -0,0 +1,389 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: kafka
+// Service ID: 116
+
+var _ resource.Resource = &KafkaResource{}
+var _ resource.ResourceWithModifyPlan = &KafkaResource{}
+var _ resource.ResourceWithImportState = &KafkaResource{}
+
+type KafkaResource struct {
+ client *core.UniversalClient
+}
+
+// KafkaAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type KafkaAccessConfigurationModel struct {
+ AllocationExternal types.Bool `tfsdk:"allocation_external" json:"allocationExternal"`
+ MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"`
+}
+
+// KafkaClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type KafkaClusterConfigurationModel struct {
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+ Disk types.Int64 `tfsdk:"disk" json:"disk"`
+}
+
+// KafkaStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type KafkaStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+}
+
+type KafkaModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AccessConfiguration *KafkaAccessConfigurationModel `tfsdk:"access_configuration"`
+ ClusterConfiguration *KafkaClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ StartupConfiguration *KafkaStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewKafkaResource() resource.Resource {
+ return &KafkaResource{}
+}
+
+func (r *KafkaResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_kafka"
+}
+
+func (r *KafkaResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "allocation_external": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2000), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2048)},
+ "replicas": schema.Int64Attribute{Required: true},
+ "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ }},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"},
+ }},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *KafkaResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *KafkaModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *KafkaModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan KafkaModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 1084: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1085: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64())}),
+ 1086: resources_core.BuildJSON(map[string]string{"allocationExternal": fmt.Sprintf("%v", config.AccessConfiguration.AllocationExternal.ValueBool()), "masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString())}),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 116, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *KafkaResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data KafkaModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 116, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 1084: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1085: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64())}),
+ 1086: resources_core.BuildJSON(map[string]string{"allocationExternal": fmt.Sprintf("%v", data.AccessConfiguration.AllocationExternal.ValueBool()), "masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString())}),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 116, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 116, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *KafkaResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state KafkaModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 116, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *KafkaResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan KafkaModel
+ var state KafkaModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 1087: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64())}),
+ 1088: resources_core.BuildJSON(map[string]string{"allocationExternal": fmt.Sprintf("%v", plan.AccessConfiguration.AllocationExternal.ValueBool()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString())}),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 116, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *KafkaResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state KafkaModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *KafkaResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *KafkaResource) 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
+}
diff --git a/generated/dev/go/116_kafka_topic_resource.go b/generated/dev/go/116_kafka_topic_resource.go
new file mode 100644
index 0000000..3f289b3
--- /dev/null
+++ b/generated/dev/go/116_kafka_topic_resource.go
@@ -0,0 +1,364 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: kafka
+// Subresource: topic
+
+var _ resource.Resource = &KafkaTopicResource{}
+
+// KafkaTopicResource manages a subresource via operations.
+type KafkaTopicResource struct {
+ client *core.UniversalClient
+}
+
+type KafkaTopicModel struct {
+ ID types.String `tfsdk:"id"`
+ KafkaID types.String `tfsdk:"kafka_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"`
+ NameTopic types.String `tfsdk:"name_topic"`
+ Partitions types.Int64 `tfsdk:"partitions"`
+ Replicas types.Int64 `tfsdk:"replicas"`
+}
+
+func NewKafkaTopicResource() resource.Resource {
+ return &KafkaTopicResource{}
+}
+
+func (r *KafkaTopicResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_kafka_topic"
+}
+
+func (r *KafkaTopicResource) 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()}},
+ "kafka_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."},
+ "name_topic": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `orders-events` Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "partitions": schema.Int64Attribute{Required: true, MarkdownDescription: "Число партиций можно увеличить, но уменьшить их нельзя."},
+ "replicas": schema.Int64Attribute{Required: true, MarkdownDescription: "Количество реплик не может привышать количество реплик брокера"},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *KafkaTopicResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan KafkaTopicModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if plan.NameTopic.IsNull() || plan.NameTopic.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "name_topic is required.")
+ return
+ }
+ if plan.Partitions.IsNull() || plan.Partitions.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "partitions is required.")
+ return
+ }
+ if plan.Replicas.IsNull() || plan.Replicas.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "replicas is required.")
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.KafkaID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "nameTopic": resources_core.FormatString(plan.NameTopic),
+ }
+ identityCodes := []string{
+ "nameTopic",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("topic", 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, "topic", 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{
+ "nameTopic": resources_core.FormatString(plan.NameTopic),
+ "partitions": resources_core.FormatInt64(plan.Partitions),
+ "replicas": resources_core.FormatInt64(plan.Replicas),
+ })
+
+ 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, "create_topic", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_topic", 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, "topic", 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, "topic", 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, "topic", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *KafkaTopicResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state KafkaTopicModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *KafkaTopicResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan KafkaTopicModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "modify_topic" == "" {
+ var state KafkaTopicModel
+ 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.KafkaID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{
+ "nameTopic": resources_core.FormatString(plan.NameTopic),
+ "partitions": resources_core.FormatInt64(plan.Partitions),
+ "replicas": resources_core.FormatInt64(plan.Replicas),
+ })
+
+ idParams := map[string]string{
+ "nameTopic": resources_core.FormatString(plan.NameTopic),
+ }
+
+ 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, "modify_topic", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "topic", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *KafkaTopicResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state KafkaTopicModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_topic" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.KafkaID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "nameTopic": resources_core.FormatString(state.NameTopic),
+ }
+ identityCodes := []string{
+ "nameTopic",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("topic", 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{
+ "nameTopic": resources_core.FormatString(state.NameTopic),
+ })
+
+ 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, "delete_topic", 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 *KafkaTopicResource) 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
+}
diff --git a/generated/dev/go/116_kafka_user_resource.go b/generated/dev/go/116_kafka_user_resource.go
new file mode 100644
index 0000000..52ea73c
--- /dev/null
+++ b/generated/dev/go/116_kafka_user_resource.go
@@ -0,0 +1,373 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: kafka
+// Subresource: user
+
+var _ resource.Resource = &KafkaUserResource{}
+
+// KafkaUserResource manages a subresource via operations.
+type KafkaUserResource struct {
+ client *core.UniversalClient
+}
+
+type KafkaUserModel struct {
+ ID types.String `tfsdk:"id"`
+ KafkaID types.String `tfsdk:"kafka_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"`
+ AccessHosts types.String `tfsdk:"access_hosts"`
+ Group types.String `tfsdk:"group"`
+ NameTopic types.String `tfsdk:"name_topic"`
+ Operations types.String `tfsdk:"operations"`
+ Username types.String `tfsdk:"username"`
+}
+
+func NewKafkaUserResource() resource.Resource {
+ return &KafkaUserResource{}
+}
+
+func (r *KafkaUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_kafka_user"
+}
+
+func (r *KafkaUserResource) 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()}},
+ "kafka_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."},
+ "access_hosts": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("*"), MarkdownDescription: "Пример: `192.168.1.0/24` или `*`"},
+ "group": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("*"), MarkdownDescription: "Пример: `order-consumers` или `*`"},
+ "name_topic": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `orders-events`. Использовать маску нельзя(`orders*`), только полное название или `*`"},
+ "operations": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `Create, Describe, Read, Write`"},
+ "username": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `username-admin` Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *KafkaUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan KafkaUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if plan.NameTopic.IsNull() || plan.NameTopic.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "name_topic is required.")
+ return
+ }
+ if plan.Operations.IsNull() || plan.Operations.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "operations is required.")
+ return
+ }
+ if plan.Username.IsNull() || plan.Username.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "username is required.")
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.KafkaID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "username": resources_core.FormatString(plan.Username),
+ }
+ identityCodes := []string{
+ "username",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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, "user", 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{
+ "username": resources_core.FormatString(plan.Username),
+ "nameTopic": resources_core.FormatString(plan.NameTopic),
+ "operations": resources_core.FormatString(plan.Operations),
+ "group": resources_core.FormatString(plan.Group),
+ "accessHosts": resources_core.FormatString(plan.AccessHosts),
+ })
+
+ 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, "create_user", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", 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, "user", 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, "user", 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, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *KafkaUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state KafkaUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *KafkaUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan KafkaUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "modify_user" == "" {
+ var state KafkaUserModel
+ 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.KafkaID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{
+ "nameTopic": resources_core.FormatString(plan.NameTopic),
+ "operations": resources_core.FormatString(plan.Operations),
+ "group": resources_core.FormatString(plan.Group),
+ "accessHosts": resources_core.FormatString(plan.AccessHosts),
+ "username": resources_core.FormatString(plan.Username),
+ })
+
+ idParams := map[string]string{
+ "username": resources_core.FormatString(plan.Username),
+ }
+
+ 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, "modify_user", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *KafkaUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state KafkaUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_user" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.KafkaID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "username": resources_core.FormatString(state.Username),
+ }
+ identityCodes := []string{
+ "username",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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{
+ "username": resources_core.FormatString(state.Username),
+ })
+
+ 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, "delete_user", 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 *KafkaUserResource) 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
+}
diff --git a/generated/dev/go/119_akhq_resource.go b/generated/dev/go/119_akhq_resource.go
new file mode 100644
index 0000000..101c30a
--- /dev/null
+++ b/generated/dev/go/119_akhq_resource.go
@@ -0,0 +1,554 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: akhq
+// Service ID: 119
+
+var _ resource.Resource = &AkhqResource{}
+var _ resource.ResourceWithModifyPlan = &AkhqResource{}
+var _ resource.ResourceWithImportState = &AkhqResource{}
+
+type AkhqResource struct {
+ client *core.UniversalClient
+}
+
+type AkhqModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ Domain types.String `tfsdk:"domain"`
+ KafkaUid types.String `tfsdk:"kafka_uid"`
+ ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"`
+ ResourceInstances types.Int64 `tfsdk:"resource_instances"`
+ ResourceMemory types.Int64 `tfsdk:"resource_memory"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewAkhqResource() resource.Resource {
+ return &AkhqResource{}
+}
+
+func (r *AkhqResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_akhq"
+}
+
+func (r *AkhqResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "domain": schema.StringAttribute{Required: true, MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."},
+ "kafka_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Выбрать кластер из списка"},
+ "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Пример: `1000` Не изменяется после создания."},
+ "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Не изменяется после создания."},
+ "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Пример: `1024` Не изменяется после создания."},
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Выбрать платформу из списка Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *AkhqResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *AkhqModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *AkhqModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan AkhqModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() {
+ if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_c_p_u", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() {
+ if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_instances", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() {
+ if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_memory", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.Domain.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "domain is required.")
+ return
+ }
+ if config.Domain.IsUnknown() {
+ return
+ }
+ if config.KafkaUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "kafka_uid is required.")
+ return
+ }
+ if config.KafkaUid.IsUnknown() {
+ return
+ }
+ if config.ResourceRealm.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+ if config.ResourceRealm.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.KafkaUid.IsNull() && !config.KafkaUid.IsUnknown() {
+ resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, config.KafkaUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error())
+ } else if resolvedKafkaUid != "" && resolvedKafkaUid != config.KafkaUid.ValueString() {
+ config.KafkaUid = types.StringValue(resolvedKafkaUid)
+ }
+ }
+ params := map[int]string{
+ 550: resources_core.FormatString(config.ResourceRealm),
+ 551: resources_core.FormatInt64(config.ResourceMemory),
+ 552: resources_core.FormatInt64(config.ResourceCPU),
+ 553: resources_core.FormatString(config.Domain),
+ 554: resources_core.FormatInt64(config.ResourceInstances),
+ 555: resources_core.FormatString(config.KafkaUid),
+ }
+ desiredDomain := ""
+ if !config.Domain.IsNull() && !config.Domain.IsUnknown() {
+ desiredDomain = config.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 119, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *AkhqResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data AkhqModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.Domain.IsNull() || data.Domain.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "domain is required.")
+ return
+ }
+ if data.KafkaUid.IsNull() || data.KafkaUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "kafka_uid is required.")
+ return
+ }
+ if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalKafkaUid := data.KafkaUid
+ if !data.KafkaUid.IsNull() && !data.KafkaUid.IsUnknown() {
+ resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, data.KafkaUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedKafkaUid != "" && resolvedKafkaUid != data.KafkaUid.ValueString() {
+ data.KafkaUid = types.StringValue(resolvedKafkaUid)
+ }
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ if !data.Domain.IsNull() && !data.Domain.IsUnknown() {
+ desiredDomain = data.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 119, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 550: resources_core.FormatString(data.ResourceRealm),
+ 551: resources_core.FormatInt64(data.ResourceMemory),
+ 552: resources_core.FormatInt64(data.ResourceCPU),
+ 553: resources_core.FormatString(data.Domain),
+ 554: resources_core.FormatInt64(data.ResourceInstances),
+ 555: resources_core.FormatString(data.KafkaUid),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 119, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 119, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "kafkaUid", Field: "KafkaUid", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() {
+ resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, state.KafkaUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error())
+ } else if resolvedKafkaUid != "" && resolvedKafkaUid != state.KafkaUid.ValueString() {
+ state.KafkaUid = types.StringValue(resolvedKafkaUid)
+ }
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+ if !originalKafkaUid.IsNull() && !originalKafkaUid.IsUnknown() && !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() {
+ if strings.EqualFold(state.KafkaUid.ValueString(), originalKafkaUid.ValueString()) {
+ state.KafkaUid = originalKafkaUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *AkhqResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state AkhqModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 119, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "kafkaUid", Field: "KafkaUid", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !newState.KafkaUid.IsNull() && !newState.KafkaUid.IsUnknown() {
+ resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, newState.KafkaUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error())
+ } else if resolvedKafkaUid != "" && resolvedKafkaUid != newState.KafkaUid.ValueString() {
+ newState.KafkaUid = types.StringValue(resolvedKafkaUid)
+ }
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+ if !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() && !newState.KafkaUid.IsNull() && !newState.KafkaUid.IsUnknown() {
+ if strings.EqualFold(newState.KafkaUid.ValueString(), state.KafkaUid.ValueString()) {
+ newState.KafkaUid = state.KafkaUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *AkhqResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan AkhqModel
+ var state AkhqModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.KafkaUid.IsNull() != state.KafkaUid.IsNull() || plan.KafkaUid.IsUnknown() != state.KafkaUid.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.KafkaUid.IsNull() && !plan.KafkaUid.IsUnknown() && !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() {
+ if !strings.EqualFold(plan.KafkaUid.ValueString(), state.KafkaUid.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 565: resources_core.FormatString(plan.KafkaUid),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 119, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "kafkaUid", Field: "KafkaUid", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() {
+ resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, state.KafkaUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error())
+ } else if resolvedKafkaUid != "" && resolvedKafkaUid != state.KafkaUid.ValueString() {
+ state.KafkaUid = types.StringValue(resolvedKafkaUid)
+ }
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+ if !plan.KafkaUid.IsNull() && !plan.KafkaUid.IsUnknown() && !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() {
+ if strings.EqualFold(state.KafkaUid.ValueString(), plan.KafkaUid.ValueString()) {
+ state.KafkaUid = plan.KafkaUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *AkhqResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state AkhqModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *AkhqResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *AkhqResource) 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
+}
diff --git a/generated/dev/go/120_clickhouse_database_resource.go b/generated/dev/go/120_clickhouse_database_resource.go
new file mode 100644
index 0000000..cc5c42e
--- /dev/null
+++ b/generated/dev/go/120_clickhouse_database_resource.go
@@ -0,0 +1,343 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: clickhouse
+// Subresource: database
+
+var _ resource.Resource = &ClickhouseDatabaseResource{}
+
+// ClickhouseDatabaseResource manages a subresource via operations.
+type ClickhouseDatabaseResource struct {
+ client *core.UniversalClient
+}
+
+type ClickhouseDatabaseModel struct {
+ ID types.String `tfsdk:"id"`
+ ClickhouseID types.String `tfsdk:"clickhouse_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"`
+ DbName types.String `tfsdk:"db_name"`
+}
+
+func NewClickhouseDatabaseResource() resource.Resource {
+ return &ClickhouseDatabaseResource{}
+}
+
+func (r *ClickhouseDatabaseResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_clickhouse_database"
+}
+
+func (r *ClickhouseDatabaseResource) 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()}},
+ "clickhouse_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."},
+ "db_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("db01"), MarkdownDescription: "Пример: `db01` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *ClickhouseDatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan ClickhouseDatabaseModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.ClickhouseID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "dbName": resources_core.FormatString(plan.DbName),
+ }
+ identityCodes := []string{
+ "dbName",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", 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, "database", 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{
+ "dbName": resources_core.FormatString(plan.DbName),
+ })
+
+ 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, "create_database", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_database", 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, "database", 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, "database", 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, "database", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *ClickhouseDatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state ClickhouseDatabaseModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *ClickhouseDatabaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan ClickhouseDatabaseModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state ClickhouseDatabaseModel
+ 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.ClickhouseID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "dbName": resources_core.FormatString(plan.DbName),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *ClickhouseDatabaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state ClickhouseDatabaseModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_database" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.ClickhouseID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "dbName": resources_core.FormatString(state.DbName),
+ }
+ identityCodes := []string{
+ "dbName",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", 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{
+ "dbName": resources_core.FormatString(state.DbName),
+ })
+
+ 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, "delete_database", 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 *ClickhouseDatabaseResource) 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
+}
diff --git a/generated/dev/go/120_clickhouse_resource.go b/generated/dev/go/120_clickhouse_resource.go
new file mode 100644
index 0000000..12affc2
--- /dev/null
+++ b/generated/dev/go/120_clickhouse_resource.go
@@ -0,0 +1,487 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: clickhouse
+// Service ID: 120
+
+var _ resource.Resource = &ClickhouseResource{}
+var _ resource.ResourceWithModifyPlan = &ClickhouseResource{}
+var _ resource.ResourceWithImportState = &ClickhouseResource{}
+
+type ClickhouseResource struct {
+ client *core.UniversalClient
+}
+
+// ClickhouseAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type ClickhouseAccessConfigurationModel struct {
+ MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"`
+ MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"`
+}
+
+// ClickhouseAutoscaleConfigurationModel — вложенная модель для map-fixed параметра autoscaleConfiguration.
+type ClickhouseAutoscaleConfigurationModel struct {
+ Enabled types.Bool `tfsdk:"enabled" json:"enabled"`
+ Schedule types.Int64 `tfsdk:"schedule" json:"schedule"`
+ Quota types.Int64 `tfsdk:"quota" json:"quota"`
+ Percent types.Int64 `tfsdk:"percent" json:"percent"`
+}
+
+// ClickhouseBackupConfigurationModel — вложенная модель для map-fixed параметра backupConfiguration.
+type ClickhouseBackupConfigurationModel struct {
+ S3Uid types.String `tfsdk:"s3_uid" json:"s3Uid"`
+ Retain types.Int64 `tfsdk:"retain" json:"retain"`
+ Schedule types.String `tfsdk:"schedule" json:"schedule"`
+}
+
+// ClickhouseClickhouseConfigurationModel — вложенная модель для map-fixed параметра clickhouseConfiguration.
+type ClickhouseClickhouseConfigurationModel struct {
+ Version types.String `tfsdk:"version" json:"version"`
+}
+
+// ClickhouseClickhouseKeeperConfigurationModel — вложенная модель для map-fixed параметра clickhouseKeeperConfiguration.
+type ClickhouseClickhouseKeeperConfigurationModel struct {
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+}
+
+// ClickhouseClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type ClickhouseClusterConfigurationModel struct {
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+ Shards types.Int64 `tfsdk:"shards" json:"shards"`
+ Disk types.Int64 `tfsdk:"disk" json:"disk"`
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+}
+
+// ClickhouseStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type ClickhouseStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+}
+
+type ClickhouseModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AccessConfiguration *ClickhouseAccessConfigurationModel `tfsdk:"access_configuration"`
+ AutoscaleConfiguration *ClickhouseAutoscaleConfigurationModel `tfsdk:"autoscale_configuration"`
+ BackupConfiguration *ClickhouseBackupConfigurationModel `tfsdk:"backup_configuration"`
+ ClickhouseConfiguration *ClickhouseClickhouseConfigurationModel `tfsdk:"clickhouse_configuration"`
+ ClickhouseFilesConfiguration types.String `tfsdk:"clickhouse_files_configuration"`
+ ClickhouseKeeperConfiguration *ClickhouseClickhouseKeeperConfigurationModel `tfsdk:"clickhouse_keeper_configuration"`
+ ClusterConfiguration *ClickhouseClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ StartupConfiguration *ClickhouseStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewClickhouseResource() resource.Resource {
+ return &ClickhouseResource{}
+}
+
+func (r *ClickhouseResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_clickhouse"
+}
+
+func (r *ClickhouseResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "master_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"},
+ "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"},
+ }},
+ "autoscale_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "enabled": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает автоскейлинг PV. При включении параметра необходимо настроить autoScalePercentage и autoScaleTechWindow. Алерт при срабатывании вызывает modify у текущей услуги"},
+ "schedule": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "Требует включения autoScale. Окно обновления (пока не реализовано). Указывается час в виде Integer (0 / 5 / 12 / 23)"},
+ "quota": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100), MarkdownDescription: "До какого размера возможно увеличивать диск, когда включен Autoscale"},
+ "percent": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Требует включения autoScale. Позволяет указать в процентах расширение от текущего макс объема
Минимальный размер на который расширяется - **1Gb**"},
+ }},
+ "backup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "s3_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Экземпляр S3 обязателен"},
+ "retain": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(7)},
+ "schedule": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("0 0 * * *")},
+ }},
+ "clickhouse_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("25.10")},
+ }},
+ "clickhouse_files_configuration": schema.StringAttribute{Optional: true, MarkdownDescription: "Формат:
- path: somethings3.xml
xmlConfig: '<clickhouse>......</clickhouse>'"},
+ "clickhouse_keeper_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Указывается в Megabytes"},
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "Если указан 0, то инстансы ClickHouse Keeper развёрнуты не будут"},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2048), MarkdownDescription: "Указывается в Megabytes"},
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ "shards": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "**Шард** - это часть данных, разделенная на разные узлы (серверы) для горизонтального масштабирования и обработки больших объемов информации.
**Реплика** - это точная копия данных внутри одного шарда"},
+ "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Указывается в GIGAbytes"},
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1000), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ }},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"},
+ }},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *ClickhouseResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *ClickhouseModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *ClickhouseModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan ClickhouseModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 837: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}),
+ 838: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "shards": fmt.Sprintf("%d", config.ClusterConfiguration.Shards.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64())}),
+ 839: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString())}),
+ 840: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClickhouseKeeperConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClickhouseKeeperConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClickhouseKeeperConfiguration.Replicas.ValueInt64())}),
+ 841: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.ClickhouseConfiguration.Version.ValueString())}),
+ 842: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", config.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", config.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", config.BackupConfiguration.Schedule.ValueString())}),
+ 843: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", config.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", config.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", config.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", config.AutoscaleConfiguration.Percent.ValueInt64())}),
+ 851: resources_core.FormatString(config.ClickhouseFilesConfiguration),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 120, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *ClickhouseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data ClickhouseModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 120, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 837: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}),
+ 838: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "shards": fmt.Sprintf("%d", data.ClusterConfiguration.Shards.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64())}),
+ 839: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString())}),
+ 840: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClickhouseKeeperConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClickhouseKeeperConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClickhouseKeeperConfiguration.Replicas.ValueInt64())}),
+ 841: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.ClickhouseConfiguration.Version.ValueString())}),
+ 842: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", data.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", data.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", data.BackupConfiguration.Schedule.ValueString())}),
+ 843: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", data.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", data.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", data.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", data.AutoscaleConfiguration.Percent.ValueInt64())}),
+ 851: resources_core.FormatString(data.ClickhouseFilesConfiguration),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 120, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 120, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"},
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clickhouseConfiguration", Field: "ClickhouseConfiguration", Type: "map-fixed"},
+ {Code: "clickhouseFilesConfiguration", Field: "ClickhouseFilesConfiguration", Type: "array-map-fixed"},
+ {Code: "clickhouseKeeperConfiguration", Field: "ClickhouseKeeperConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *ClickhouseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state ClickhouseModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 120, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"},
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clickhouseConfiguration", Field: "ClickhouseConfiguration", Type: "map-fixed"},
+ {Code: "clickhouseFilesConfiguration", Field: "ClickhouseFilesConfiguration", Type: "array-map-fixed"},
+ {Code: "clickhouseKeeperConfiguration", Field: "ClickhouseKeeperConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *ClickhouseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan ClickhouseModel
+ var state ClickhouseModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.ClickhouseFilesConfiguration.IsNull() != state.ClickhouseFilesConfiguration.IsNull() || plan.ClickhouseFilesConfiguration.IsUnknown() != state.ClickhouseFilesConfiguration.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ClickhouseFilesConfiguration.IsNull() && !plan.ClickhouseFilesConfiguration.IsUnknown() && !state.ClickhouseFilesConfiguration.IsNull() && !state.ClickhouseFilesConfiguration.IsUnknown() {
+ if !strings.EqualFold(plan.ClickhouseFilesConfiguration.ValueString(), state.ClickhouseFilesConfiguration.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 844: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "shards": fmt.Sprintf("%d", plan.ClusterConfiguration.Shards.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64())}),
+ 845: resources_core.BuildJSON(map[string]string{"replicas": fmt.Sprintf("%d", plan.ClickhouseKeeperConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("%d", plan.ClickhouseKeeperConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClickhouseKeeperConfiguration.Memory.ValueInt64())}),
+ 846: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString())}),
+ 847: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.ClickhouseConfiguration.Version.ValueString())}),
+ 848: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", plan.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.Schedule.ValueString())}),
+ 849: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", plan.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Percent.ValueInt64())}),
+ 850: resources_core.FormatString(plan.ClickhouseFilesConfiguration),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 120, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"},
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clickhouseConfiguration", Field: "ClickhouseConfiguration", Type: "map-fixed"},
+ {Code: "clickhouseFilesConfiguration", Field: "ClickhouseFilesConfiguration", Type: "array-map-fixed"},
+ {Code: "clickhouseKeeperConfiguration", Field: "ClickhouseKeeperConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *ClickhouseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state ClickhouseModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *ClickhouseResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *ClickhouseResource) 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
+}
diff --git a/generated/dev/go/120_clickhouse_user_resource.go b/generated/dev/go/120_clickhouse_user_resource.go
new file mode 100644
index 0000000..8dc28f7
--- /dev/null
+++ b/generated/dev/go/120_clickhouse_user_resource.go
@@ -0,0 +1,359 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: clickhouse
+// Subresource: user
+
+var _ resource.Resource = &ClickhouseUserResource{}
+
+// ClickhouseUserResource manages a subresource via operations.
+type ClickhouseUserResource struct {
+ client *core.UniversalClient
+}
+
+type ClickhouseUserModel struct {
+ ID types.String `tfsdk:"id"`
+ ClickhouseID types.String `tfsdk:"clickhouse_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"`
+ AccessHosts types.String `tfsdk:"access_hosts"`
+ DbName types.String `tfsdk:"db_name"`
+ Username types.String `tfsdk:"username"`
+}
+
+func NewClickhouseUserResource() resource.Resource {
+ return &ClickhouseUserResource{}
+}
+
+func (r *ClickhouseUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_clickhouse_user"
+}
+
+func (r *ClickhouseUserResource) 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()}},
+ "clickhouse_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."},
+ "access_hosts": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("::/0"), MarkdownDescription: "Пример: `::/0` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "db_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "username": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("myusername"), MarkdownDescription: "Пример: `myusername` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *ClickhouseUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan ClickhouseUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.ClickhouseID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "accessHosts": resources_core.FormatString(plan.AccessHosts),
+ "dbName": resources_core.FormatString(plan.DbName),
+ "username": resources_core.FormatString(plan.Username),
+ }
+ identityCodes := []string{
+ "accessHosts",
+ "dbName",
+ "username",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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, "user", 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{
+ "username": resources_core.FormatString(plan.Username),
+ "dbName": resources_core.FormatString(plan.DbName),
+ "accessHosts": resources_core.FormatString(plan.AccessHosts),
+ })
+
+ 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, "create_user", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", 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, "user", 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, "user", 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, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *ClickhouseUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state ClickhouseUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *ClickhouseUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan ClickhouseUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state ClickhouseUserModel
+ 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.ClickhouseID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "accessHosts": resources_core.FormatString(plan.AccessHosts),
+ "dbName": resources_core.FormatString(plan.DbName),
+ "username": resources_core.FormatString(plan.Username),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *ClickhouseUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state ClickhouseUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_user" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.ClickhouseID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "accessHosts": resources_core.FormatString(state.AccessHosts),
+ "dbName": resources_core.FormatString(state.DbName),
+ "username": resources_core.FormatString(state.Username),
+ }
+ identityCodes := []string{
+ "accessHosts",
+ "dbName",
+ "username",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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{
+ "username": resources_core.FormatString(state.Username),
+ })
+
+ 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, "delete_user", 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 *ClickhouseUserResource) 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
+}
diff --git a/generated/dev/go/12_s3_resource.go b/generated/dev/go/12_s3_resource.go
new file mode 100644
index 0000000..f4db5ce
--- /dev/null
+++ b/generated/dev/go/12_s3_resource.go
@@ -0,0 +1,421 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: s3
+// Service ID: 12
+
+var _ resource.Resource = &S3Resource{}
+var _ resource.ResourceWithModifyPlan = &S3Resource{}
+var _ resource.ResourceWithImportState = &S3Resource{}
+
+type S3Resource struct {
+ client *core.UniversalClient
+}
+
+type S3Model struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ DisplayName types.String `tfsdk:"display_name"`
+ MaxBucketsPerUser types.Int64 `tfsdk:"max_buckets_per_user"`
+ MaxObjectsPerBucket types.Int64 `tfsdk:"max_objects_per_bucket"`
+ MaxSizeGbPerUser types.Int64 `tfsdk:"max_size_gb_per_user"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewS3Resource() resource.Resource {
+ return &S3Resource{}
+}
+
+func (r *S3Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_s3"
+}
+
+func (r *S3Resource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "display_name": schema.StringAttribute{Required: true, MarkdownDescription: "Человеко-читаемое имя (транслит из Elma) - передаётся имя клиента Не изменяется после создания."},
+ "max_buckets_per_user": schema.Int64Attribute{Optional: true, MarkdownDescription: "Квота на пользователя. Количество бакетов пользователя. Передаём руками"},
+ "max_objects_per_bucket": schema.Int64Attribute{Optional: true, MarkdownDescription: "Квота на бакет. Кол-во объектов. Передаём руками"},
+ "max_size_gb_per_user": schema.Int64Attribute{Optional: true, MarkdownDescription: "maxSizeGb. Передаём руками"},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("ceph.tst.nubes.ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *S3Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *S3Model
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *S3Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan S3Model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.DisplayName.IsNull() && !plan.DisplayName.IsUnknown() && !state.DisplayName.IsNull() && !state.DisplayName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.DisplayName.ValueString(), state.DisplayName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить display_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.DisplayName.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "display_name is required.")
+ return
+ }
+ if config.DisplayName.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 163: resources_core.FormatString(config.ResourceRealm),
+ 320: resources_core.FormatString(config.DisplayName),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 12, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *S3Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data S3Model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.DisplayName.IsNull() || data.DisplayName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "display_name is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 12, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 163: resources_core.FormatString(data.ResourceRealm),
+ 320: resources_core.FormatString(data.DisplayName),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 12, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 12, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "displayName", Field: "DisplayName", Type: "string"},
+ {Code: "maxBucketsPerUser", Field: "MaxBucketsPerUser", Type: "int64"},
+ {Code: "maxObjectsPerBucket", Field: "MaxObjectsPerBucket", Type: "int64"},
+ {Code: "maxSizeGbPerUser", Field: "MaxSizeGbPerUser", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *S3Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state S3Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 12, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "displayName", Field: "DisplayName", Type: "string"},
+ {Code: "maxBucketsPerUser", Field: "MaxBucketsPerUser", Type: "int64"},
+ {Code: "maxObjectsPerBucket", Field: "MaxObjectsPerBucket", Type: "int64"},
+ {Code: "maxSizeGbPerUser", Field: "MaxSizeGbPerUser", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *S3Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan S3Model
+ var state S3Model
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.MaxSizeGbPerUser.IsNull() != state.MaxSizeGbPerUser.IsNull() || plan.MaxSizeGbPerUser.IsUnknown() != state.MaxSizeGbPerUser.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.MaxSizeGbPerUser.IsNull() && !plan.MaxSizeGbPerUser.IsUnknown() && !state.MaxSizeGbPerUser.IsNull() && !state.MaxSizeGbPerUser.IsUnknown() {
+ if plan.MaxSizeGbPerUser.ValueInt64() != state.MaxSizeGbPerUser.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.MaxObjectsPerBucket.IsNull() != state.MaxObjectsPerBucket.IsNull() || plan.MaxObjectsPerBucket.IsUnknown() != state.MaxObjectsPerBucket.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.MaxObjectsPerBucket.IsNull() && !plan.MaxObjectsPerBucket.IsUnknown() && !state.MaxObjectsPerBucket.IsNull() && !state.MaxObjectsPerBucket.IsUnknown() {
+ if plan.MaxObjectsPerBucket.ValueInt64() != state.MaxObjectsPerBucket.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.MaxBucketsPerUser.IsNull() != state.MaxBucketsPerUser.IsNull() || plan.MaxBucketsPerUser.IsUnknown() != state.MaxBucketsPerUser.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.MaxBucketsPerUser.IsNull() && !plan.MaxBucketsPerUser.IsUnknown() && !state.MaxBucketsPerUser.IsNull() && !state.MaxBucketsPerUser.IsUnknown() {
+ if plan.MaxBucketsPerUser.ValueInt64() != state.MaxBucketsPerUser.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 50: resources_core.FormatInt64(plan.MaxSizeGbPerUser),
+ 51: resources_core.FormatInt64(plan.MaxObjectsPerBucket),
+ 52: resources_core.FormatInt64(plan.MaxBucketsPerUser),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 12, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "displayName", Field: "DisplayName", Type: "string"},
+ {Code: "maxBucketsPerUser", Field: "MaxBucketsPerUser", Type: "int64"},
+ {Code: "maxObjectsPerBucket", Field: "MaxObjectsPerBucket", Type: "int64"},
+ {Code: "maxSizeGbPerUser", Field: "MaxSizeGbPerUser", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *S3Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state S3Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *S3Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *S3Resource) 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
+}
diff --git a/generated/dev/go/12_s3_sub_user_resource.go b/generated/dev/go/12_s3_sub_user_resource.go
new file mode 100644
index 0000000..b8e0168
--- /dev/null
+++ b/generated/dev/go/12_s3_sub_user_resource.go
@@ -0,0 +1,358 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: s3
+// Subresource: sub_user
+
+var _ resource.Resource = &S3SubUserResource{}
+
+// S3SubUserResource manages a subresource via operations.
+type S3SubUserResource struct {
+ client *core.UniversalClient
+}
+
+type S3SubUserModel struct {
+ ID types.String `tfsdk:"id"`
+ S3ID types.String `tfsdk:"s3_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"`
+ SubUserAccess types.String `tfsdk:"sub_user_access"`
+ SubUserName types.String `tfsdk:"sub_user_name"`
+}
+
+func NewS3SubUserResource() resource.Resource {
+ return &S3SubUserResource{}
+}
+
+func (r *S3SubUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_s3_sub_user"
+}
+
+func (r *S3SubUserResource) 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()}},
+ "s3_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."},
+ "sub_user_access": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "sub_user_name": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *S3SubUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan S3SubUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if plan.SubUserAccess.IsNull() || plan.SubUserAccess.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "sub_user_access is required.")
+ return
+ }
+ if plan.SubUserName.IsNull() || plan.SubUserName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "sub_user_name is required.")
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.S3ID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "subUserAccess": resources_core.FormatString(plan.SubUserAccess),
+ "subUserName": resources_core.FormatString(plan.SubUserName),
+ }
+ identityCodes := []string{
+ "subUserAccess",
+ "subUserName",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("sub_user", 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, "sub_user", 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{
+ "subUserName": resources_core.FormatString(plan.SubUserName),
+ "subUserAccess": resources_core.FormatString(plan.SubUserAccess),
+ })
+
+ 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, "create_sub_user", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_sub_user", 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, "sub_user", 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, "sub_user", 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, "sub_user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *S3SubUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state S3SubUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *S3SubUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan S3SubUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state S3SubUserModel
+ 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.S3ID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "subUserAccess": resources_core.FormatString(plan.SubUserAccess),
+ "subUserName": resources_core.FormatString(plan.SubUserName),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "sub_user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *S3SubUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state S3SubUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_sub_user" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.S3ID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "subUserAccess": resources_core.FormatString(state.SubUserAccess),
+ "subUserName": resources_core.FormatString(state.SubUserName),
+ }
+ identityCodes := []string{
+ "subUserAccess",
+ "subUserName",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("sub_user", 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{
+ "subUserName": resources_core.FormatString(state.SubUserName),
+ })
+
+ 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, "delete_sub_user", 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 *S3SubUserResource) 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
+}
diff --git a/generated/dev/go/13_s3bucket_resource.go b/generated/dev/go/13_s3bucket_resource.go
new file mode 100644
index 0000000..deb2272
--- /dev/null
+++ b/generated/dev/go/13_s3bucket_resource.go
@@ -0,0 +1,478 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: s3bucket
+// Service ID: 13
+
+var _ resource.Resource = &S3bucketResource{}
+var _ resource.ResourceWithModifyPlan = &S3bucketResource{}
+var _ resource.ResourceWithImportState = &S3bucketResource{}
+
+type S3bucketResource struct {
+ client *core.UniversalClient
+}
+
+type S3bucketModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ BucketName types.String `tfsdk:"bucket_name"`
+ CorsAll types.Bool `tfsdk:"cors_all"`
+ ListAll types.Bool `tfsdk:"list_all"`
+ MaxSize types.String `tfsdk:"max_size"`
+ Placement types.String `tfsdk:"placement"`
+ ReadAll types.Bool `tfsdk:"read_all"`
+ S3UserUid types.String `tfsdk:"s3_user_uid"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewS3bucketResource() resource.Resource {
+ return &S3bucketResource{}
+}
+
+func (r *S3bucketResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_s3bucket"
+}
+
+func (r *S3bucketResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "bucket_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя бакета s3 Не изменяется после создания."},
+ "cors_all": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Публичный доступ к чтению настроек позволяет любому пользователю интернета читать конфигурации CORS Не изменяется после создания."},
+ "list_all": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Публичный доступ к листингу позволяет любому пользователю интернета получить список объектов в бакете. Не изменяется после создания."},
+ "max_size": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("-1"), MarkdownDescription: "Максимальный размер бакета s3. Если поставить -1, будет безлимит. Размер указывается в Bytes Не изменяется после создания."},
+ "placement": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("HOT"), MarkdownDescription: "Класс хранилища, который используется по умолчанию при загрузке объектов. Чем холоднее, тем дешевле место, но дороже обращения Не изменяется после создания."},
+ "read_all": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Публичный доступ на чтение позволяет любому пользователю интернета скачивать объекты из бакета. Не изменяется после создания."},
+ "s3_user_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UID Корневой услуги S3. На основе него будет создан бакет. Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *S3bucketResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *S3bucketModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *S3bucketModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan S3bucketModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.BucketName.IsNull() && !plan.BucketName.IsUnknown() && !state.BucketName.IsNull() && !state.BucketName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.BucketName.ValueString(), state.BucketName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить bucket_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.CorsAll.IsNull() && !plan.CorsAll.IsUnknown() && !state.CorsAll.IsNull() && !state.CorsAll.IsUnknown() {
+ if plan.CorsAll.ValueBool() != state.CorsAll.ValueBool() {
+ resp.Diagnostics.AddError("Нельзя изменить cors_all", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ListAll.IsNull() && !plan.ListAll.IsUnknown() && !state.ListAll.IsNull() && !state.ListAll.IsUnknown() {
+ if plan.ListAll.ValueBool() != state.ListAll.ValueBool() {
+ resp.Diagnostics.AddError("Нельзя изменить list_all", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.MaxSize.IsNull() && !plan.MaxSize.IsUnknown() && !state.MaxSize.IsNull() && !state.MaxSize.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.MaxSize.ValueString(), state.MaxSize.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить max_size", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.Placement.IsNull() && !plan.Placement.IsUnknown() && !state.Placement.IsNull() && !state.Placement.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Placement.ValueString(), state.Placement.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить placement", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ReadAll.IsNull() && !plan.ReadAll.IsUnknown() && !state.ReadAll.IsNull() && !state.ReadAll.IsUnknown() {
+ if plan.ReadAll.ValueBool() != state.ReadAll.ValueBool() {
+ resp.Diagnostics.AddError("Нельзя изменить read_all", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.S3UserUid.IsNull() && !plan.S3UserUid.IsUnknown() && !state.S3UserUid.IsNull() && !state.S3UserUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.S3UserUid.ValueString(), state.S3UserUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить s3_user_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.BucketName.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "bucket_name is required.")
+ return
+ }
+ if config.BucketName.IsUnknown() {
+ return
+ }
+ if config.S3UserUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "s3_user_uid is required.")
+ return
+ }
+ if config.S3UserUid.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.S3UserUid.IsNull() && !config.S3UserUid.IsUnknown() {
+ resolvedS3UserUid, err := r.client.ResolveRefSvcParamValue(ctx, 12, config.S3UserUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve s3_user_uid", err.Error())
+ } else if resolvedS3UserUid != "" && resolvedS3UserUid != config.S3UserUid.ValueString() {
+ config.S3UserUid = types.StringValue(resolvedS3UserUid)
+ }
+ }
+ params := map[int]string{
+ 124: resources_core.FormatString(config.S3UserUid),
+ 125: resources_core.FormatString(config.BucketName),
+ 126: resources_core.FormatString(config.MaxSize),
+ 127: resources_core.FormatBool(config.ReadAll),
+ 128: resources_core.FormatBool(config.ListAll),
+ 129: resources_core.FormatBool(config.CorsAll),
+ 130: resources_core.FormatString(config.Placement),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 13, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *S3bucketResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data S3bucketModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.BucketName.IsNull() || data.BucketName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "bucket_name is required.")
+ return
+ }
+ if data.S3UserUid.IsNull() || data.S3UserUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "s3_user_uid is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 13, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 124: resources_core.FormatString(data.S3UserUid),
+ 125: resources_core.FormatString(data.BucketName),
+ 126: resources_core.FormatString(data.MaxSize),
+ 127: resources_core.FormatBool(data.ReadAll),
+ 128: resources_core.FormatBool(data.ListAll),
+ 129: resources_core.FormatBool(data.CorsAll),
+ 130: resources_core.FormatString(data.Placement),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 13, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 13, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "bucketName", Field: "BucketName", Type: "string"},
+ {Code: "corsAll", Field: "CorsAll", Type: "bool"},
+ {Code: "listAll", Field: "ListAll", Type: "bool"},
+ {Code: "maxSize", Field: "MaxSize", Type: "string"},
+ {Code: "placement", Field: "Placement", Type: "string"},
+ {Code: "readAll", Field: "ReadAll", Type: "bool"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *S3bucketResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state S3bucketModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 13, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "bucketName", Field: "BucketName", Type: "string"},
+ {Code: "corsAll", Field: "CorsAll", Type: "bool"},
+ {Code: "listAll", Field: "ListAll", Type: "bool"},
+ {Code: "maxSize", Field: "MaxSize", Type: "string"},
+ {Code: "placement", Field: "Placement", Type: "string"},
+ {Code: "readAll", Field: "ReadAll", Type: "bool"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *S3bucketResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan S3bucketModel
+ var state S3bucketModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 13, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "bucketName", Field: "BucketName", Type: "string"},
+ {Code: "corsAll", Field: "CorsAll", Type: "bool"},
+ {Code: "listAll", Field: "ListAll", Type: "bool"},
+ {Code: "maxSize", Field: "MaxSize", Type: "string"},
+ {Code: "placement", Field: "Placement", Type: "string"},
+ {Code: "readAll", Field: "ReadAll", Type: "bool"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *S3bucketResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state S3bucketModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *S3bucketResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *S3bucketResource) 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
+}
diff --git a/generated/dev/go/148_vc_mgmt_sthutrval_cluster_resource.go b/generated/dev/go/148_vc_mgmt_sthutrval_cluster_resource.go
new file mode 100644
index 0000000..17540bd
--- /dev/null
+++ b/generated/dev/go/148_vc_mgmt_sthutrval_cluster_resource.go
@@ -0,0 +1,419 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_mgmt_sthutrval_cluster
+// Service ID: 148
+
+var _ resource.Resource = &VcMgmtSthutrvalClusterResource{}
+var _ resource.ResourceWithModifyPlan = &VcMgmtSthutrvalClusterResource{}
+var _ resource.ResourceWithImportState = &VcMgmtSthutrvalClusterResource{}
+
+type VcMgmtSthutrvalClusterResource struct {
+ client *core.UniversalClient
+}
+
+// VcMgmtSthutrvalClusterAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type VcMgmtSthutrvalClusterAccessConfigurationModel struct {
+ AccessIpListAPI types.String `tfsdk:"access_ip_list_a_p_i" json:"accessIpListAPI"`
+ NeedExternalAddressIngress types.Bool `tfsdk:"need_external_address_ingress" json:"needExternalAddressIngress"`
+ AccessIpListIngress types.String `tfsdk:"access_ip_list_ingress" json:"accessIpListIngress"`
+ NeedExternalAddressAPI types.Bool `tfsdk:"need_external_address_a_p_i" json:"needExternalAddressAPI"`
+}
+
+// VcMgmtSthutrvalClusterClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type VcMgmtSthutrvalClusterClusterConfigurationModel struct {
+ AppVersion types.String `tfsdk:"app_version" json:"appVersion"`
+}
+
+// VcMgmtSthutrvalClusterControlPlaneConfigurationModel — вложенная модель для map-fixed параметра controlPlaneConfiguration.
+type VcMgmtSthutrvalClusterControlPlaneConfigurationModel struct {
+ SizingDisk types.Int64 `tfsdk:"sizing_disk" json:"sizingDisk"`
+ Count types.Int64 `tfsdk:"count" json:"count"`
+ SizingPolicy types.String `tfsdk:"sizing_policy" json:"sizingPolicy"`
+}
+
+// VcMgmtSthutrvalClusterStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type VcMgmtSthutrvalClusterStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+ ClusterName types.String `tfsdk:"cluster_name" json:"clusterName"`
+}
+
+type VcMgmtSthutrvalClusterModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AccessConfiguration *VcMgmtSthutrvalClusterAccessConfigurationModel `tfsdk:"access_configuration"`
+ ClusterConfiguration *VcMgmtSthutrvalClusterClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ ControlPlaneConfiguration *VcMgmtSthutrvalClusterControlPlaneConfigurationModel `tfsdk:"control_plane_configuration"`
+ StartupConfiguration *VcMgmtSthutrvalClusterStartupConfigurationModel `tfsdk:"startup_configuration"`
+ WorkerConfiguration types.String `tfsdk:"worker_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVcMgmtSthutrvalClusterResource() resource.Resource {
+ return &VcMgmtSthutrvalClusterResource{}
+}
+
+func (r *VcMgmtSthutrvalClusterResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_mgmt_sthutrval_cluster"
+}
+
+func (r *VcMgmtSthutrvalClusterResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "access_ip_list_a_p_i": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Access IP List для подключения к Kubernetes API. Указывается массив. Если передан пустой массив, доступ выделяется всем
Пример: **["0.0.0.0/0"]** или **[]** или **["178.4.13.2", "178.4.14.0/24"]**"},
+ "need_external_address_ingress": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Выделяет внешний IP для доступа к Ingress"},
+ "access_ip_list_ingress": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Access IP List для подключения к Ingress. Указывается массив. Если передан пустой массив, доступ выделяется всем
Пример: **["0.0.0.0/0"]** или **[]** или **["178.4.13.2", "178.4.14.0/24"]**"},
+ "need_external_address_a_p_i": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Выделяет внешний IP для доступа к kubernetes API. Пока нельзя выставить false"},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "app_version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("2.13.1")},
+ }},
+ "control_plane_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "sizing_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(50)},
+ "count": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ "sizing_policy": schema.StringAttribute{Required: true, MarkdownDescription: "Доступные sizing Policy представлены исходя из ресурсной платформы, которая была выбрана"},
+ }},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("sandbox.nubes.ru"), MarkdownDescription: "Облако в котором будет развёрнут тенант"},
+ "cluster_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("example-e5uamzll")},
+ }},
+ "worker_configuration": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcMgmtSthutrvalClusterResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VcMgmtSthutrvalClusterModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VcMgmtSthutrvalClusterModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VcMgmtSthutrvalClusterModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.WorkerConfiguration.IsNull() && !plan.WorkerConfiguration.IsUnknown() && !state.WorkerConfiguration.IsNull() && !state.WorkerConfiguration.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.WorkerConfiguration.ValueString(), state.WorkerConfiguration.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить worker_configuration", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 771: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString()), "clusterName": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ClusterName.ValueString())}),
+ 772: resources_core.BuildJSON(map[string]string{"appVersion": fmt.Sprintf("\"%s\"", config.ClusterConfiguration.AppVersion.ValueString())}),
+ 773: resources_core.BuildJSON(map[string]string{"sizingDisk": fmt.Sprintf("%d", config.ControlPlaneConfiguration.SizingDisk.ValueInt64()), "count": fmt.Sprintf("%d", config.ControlPlaneConfiguration.Count.ValueInt64()), "sizingPolicy": fmt.Sprintf("\"%s\"", config.ControlPlaneConfiguration.SizingPolicy.ValueString())}),
+ 774: resources_core.FormatString(config.WorkerConfiguration),
+ 775: resources_core.BuildJSON(map[string]string{"accessIpListAPI": fmt.Sprintf("\"%s\"", config.AccessConfiguration.AccessIpListAPI.ValueString()), "needExternalAddressIngress": fmt.Sprintf("%v", config.AccessConfiguration.NeedExternalAddressIngress.ValueBool()), "accessIpListIngress": fmt.Sprintf("\"%s\"", config.AccessConfiguration.AccessIpListIngress.ValueString()), "needExternalAddressAPI": fmt.Sprintf("%v", config.AccessConfiguration.NeedExternalAddressAPI.ValueBool())}),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 148, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VcMgmtSthutrvalClusterResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VcMgmtSthutrvalClusterModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 148, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 771: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString()), "clusterName": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ClusterName.ValueString())}),
+ 772: resources_core.BuildJSON(map[string]string{"appVersion": fmt.Sprintf("\"%s\"", data.ClusterConfiguration.AppVersion.ValueString())}),
+ 773: resources_core.BuildJSON(map[string]string{"sizingDisk": fmt.Sprintf("%d", data.ControlPlaneConfiguration.SizingDisk.ValueInt64()), "count": fmt.Sprintf("%d", data.ControlPlaneConfiguration.Count.ValueInt64()), "sizingPolicy": fmt.Sprintf("\"%s\"", data.ControlPlaneConfiguration.SizingPolicy.ValueString())}),
+ 774: resources_core.FormatString(data.WorkerConfiguration),
+ 775: resources_core.BuildJSON(map[string]string{"accessIpListAPI": fmt.Sprintf("\"%s\"", data.AccessConfiguration.AccessIpListAPI.ValueString()), "needExternalAddressIngress": fmt.Sprintf("%v", data.AccessConfiguration.NeedExternalAddressIngress.ValueBool()), "accessIpListIngress": fmt.Sprintf("\"%s\"", data.AccessConfiguration.AccessIpListIngress.ValueString()), "needExternalAddressAPI": fmt.Sprintf("%v", data.AccessConfiguration.NeedExternalAddressAPI.ValueBool())}),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 148, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 148, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcMgmtSthutrvalClusterResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcMgmtSthutrvalClusterModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 148, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VcMgmtSthutrvalClusterResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcMgmtSthutrvalClusterModel
+ var state VcMgmtSthutrvalClusterModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 148, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcMgmtSthutrvalClusterResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcMgmtSthutrvalClusterModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VcMgmtSthutrvalClusterResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VcMgmtSthutrvalClusterResource) 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
+}
diff --git a/generated/dev/go/149_valo_tenant_resource.go b/generated/dev/go/149_valo_tenant_resource.go
new file mode 100644
index 0000000..a3c1a9f
--- /dev/null
+++ b/generated/dev/go/149_valo_tenant_resource.go
@@ -0,0 +1,431 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: valo_tenant
+// Service ID: 149
+
+var _ resource.Resource = &ValoTenantResource{}
+var _ resource.ResourceWithModifyPlan = &ValoTenantResource{}
+var _ resource.ResourceWithImportState = &ValoTenantResource{}
+
+type ValoTenantResource struct {
+ client *core.UniversalClient
+}
+
+// ValoTenantValoOptionsModel — вложенная модель для map-fixed параметра valoOptions.
+type ValoTenantValoOptionsModel struct {
+ UserCount types.Int64 `tfsdk:"user_count" json:"userCount"`
+ RequestApproveMaxPeriod types.Int64 `tfsdk:"request_approve_max_period" json:"requestApproveMaxPeriod"`
+ FfOnlineEditor types.Bool `tfsdk:"ff_online_editor" json:"ffOnlineEditor"`
+ FfWatermark types.Bool `tfsdk:"ff_watermark" json:"ffWatermark"`
+ FfUserImport types.Bool `tfsdk:"ff_user_import" json:"ffUserImport"`
+ FfThemes types.Bool `tfsdk:"ff_themes" json:"ffThemes"`
+ FfPublicLink types.Bool `tfsdk:"ff_public_link" json:"ffPublicLink"`
+ FfLogging types.Bool `tfsdk:"ff_logging" json:"ffLogging"`
+}
+
+type ValoTenantModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ Domain types.String `tfsdk:"domain"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ S3Storage types.Int64 `tfsdk:"s3_storage"`
+ ValoOptions *ValoTenantValoOptionsModel `tfsdk:"valo_options"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewValoTenantResource() resource.Resource {
+ return &ValoTenantResource{}
+}
+
+func (r *ValoTenantResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_valo_tenant"
+}
+
+func (r *ValoTenantResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "domain": schema.StringAttribute{Required: true, MarkdownDescription: "Будет добавлено в зону valo.ngcloud.ru Не изменяется после создания."},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("valo.ngcloud.ru"), MarkdownDescription: "Не изменяется после создания."},
+ "s3_storage": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Параметр указывается в `Gb`"},
+ "valo_options": schema.SingleNestedAttribute{Optional: true, Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "user_count": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10)},
+ "request_approve_max_period": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2), MarkdownDescription: "Максимальное значение периода согласования (в днях)"},
+ "ff_online_editor": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"},
+ "ff_watermark": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"},
+ "ff_user_import": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"},
+ "ff_themes": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"},
+ "ff_public_link": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"},
+ "ff_logging": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"},
+ }},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *ValoTenantResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *ValoTenantModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *ValoTenantModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan ValoTenantModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.Domain.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "domain is required.")
+ return
+ }
+ if config.Domain.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 655: resources_core.FormatString(config.ResourceRealm),
+ 656: resources_core.FormatString(config.Domain),
+ 657: resources_core.BuildJSON(map[string]string{"userCount": fmt.Sprintf("%d", config.ValoOptions.UserCount.ValueInt64()), "requestApproveMaxPeriod": fmt.Sprintf("%d", config.ValoOptions.RequestApproveMaxPeriod.ValueInt64()), "ffOnlineEditor": fmt.Sprintf("%v", config.ValoOptions.FfOnlineEditor.ValueBool()), "ffWatermark": fmt.Sprintf("%v", config.ValoOptions.FfWatermark.ValueBool()), "ffUserImport": fmt.Sprintf("%v", config.ValoOptions.FfUserImport.ValueBool()), "ffThemes": fmt.Sprintf("%v", config.ValoOptions.FfThemes.ValueBool()), "ffPublicLink": fmt.Sprintf("%v", config.ValoOptions.FfPublicLink.ValueBool()), "ffLogging": fmt.Sprintf("%v", config.ValoOptions.FfLogging.ValueBool())}),
+ 658: resources_core.FormatInt64(config.S3Storage),
+ }
+ desiredDomain := ""
+ if !config.Domain.IsNull() && !config.Domain.IsUnknown() {
+ desiredDomain = config.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 149, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *ValoTenantResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data ValoTenantModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.Domain.IsNull() || data.Domain.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "domain is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ if !data.Domain.IsNull() && !data.Domain.IsUnknown() {
+ desiredDomain = data.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 149, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 655: resources_core.FormatString(data.ResourceRealm),
+ 656: resources_core.FormatString(data.Domain),
+ 657: resources_core.BuildJSON(map[string]string{"userCount": fmt.Sprintf("%d", data.ValoOptions.UserCount.ValueInt64()), "requestApproveMaxPeriod": fmt.Sprintf("%d", data.ValoOptions.RequestApproveMaxPeriod.ValueInt64()), "ffOnlineEditor": fmt.Sprintf("%v", data.ValoOptions.FfOnlineEditor.ValueBool()), "ffWatermark": fmt.Sprintf("%v", data.ValoOptions.FfWatermark.ValueBool()), "ffUserImport": fmt.Sprintf("%v", data.ValoOptions.FfUserImport.ValueBool()), "ffThemes": fmt.Sprintf("%v", data.ValoOptions.FfThemes.ValueBool()), "ffPublicLink": fmt.Sprintf("%v", data.ValoOptions.FfPublicLink.ValueBool()), "ffLogging": fmt.Sprintf("%v", data.ValoOptions.FfLogging.ValueBool())}),
+ 658: resources_core.FormatInt64(data.S3Storage),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 149, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 149, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "s3Storage", Field: "S3Storage", Type: "int64"},
+ {Code: "valoOptions", Field: "ValoOptions", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *ValoTenantResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state ValoTenantModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 149, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "s3Storage", Field: "S3Storage", Type: "int64"},
+ {Code: "valoOptions", Field: "ValoOptions", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *ValoTenantResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan ValoTenantModel
+ var state ValoTenantModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.S3Storage.IsNull() != state.S3Storage.IsNull() || plan.S3Storage.IsUnknown() != state.S3Storage.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.S3Storage.IsNull() && !plan.S3Storage.IsUnknown() && !state.S3Storage.IsNull() && !state.S3Storage.IsUnknown() {
+ if plan.S3Storage.ValueInt64() != state.S3Storage.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 659: resources_core.BuildJSON(map[string]string{"ffUserImport": fmt.Sprintf("%v", plan.ValoOptions.FfUserImport.ValueBool()), "ffThemes": fmt.Sprintf("%v", plan.ValoOptions.FfThemes.ValueBool()), "ffPublicLink": fmt.Sprintf("%v", plan.ValoOptions.FfPublicLink.ValueBool()), "ffLogging": fmt.Sprintf("%v", plan.ValoOptions.FfLogging.ValueBool()), "userCount": fmt.Sprintf("%d", plan.ValoOptions.UserCount.ValueInt64()), "requestApproveMaxPeriod": fmt.Sprintf("%d", plan.ValoOptions.RequestApproveMaxPeriod.ValueInt64()), "ffOnlineEditor": fmt.Sprintf("%v", plan.ValoOptions.FfOnlineEditor.ValueBool()), "ffWatermark": fmt.Sprintf("%v", plan.ValoOptions.FfWatermark.ValueBool())}),
+ 660: resources_core.FormatInt64(plan.S3Storage),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 149, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "s3Storage", Field: "S3Storage", Type: "int64"},
+ {Code: "valoOptions", Field: "ValoOptions", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *ValoTenantResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state ValoTenantModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *ValoTenantResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *ValoTenantResource) 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
+}
diff --git a/generated/dev/go/150_k8s_sthutrval_cluster_resource.go b/generated/dev/go/150_k8s_sthutrval_cluster_resource.go
new file mode 100644
index 0000000..97d9772
--- /dev/null
+++ b/generated/dev/go/150_k8s_sthutrval_cluster_resource.go
@@ -0,0 +1,429 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: k8s_sthutrval_cluster
+// Service ID: 150
+
+var _ resource.Resource = &K8sSthutrvalClusterResource{}
+var _ resource.ResourceWithModifyPlan = &K8sSthutrvalClusterResource{}
+var _ resource.ResourceWithImportState = &K8sSthutrvalClusterResource{}
+
+type K8sSthutrvalClusterResource struct {
+ client *core.UniversalClient
+}
+
+// K8sSthutrvalClusterAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type K8sSthutrvalClusterAccessConfigurationModel struct {
+ NeedExternalAddressIngress types.Bool `tfsdk:"need_external_address_ingress" json:"needExternalAddressIngress"`
+ AccessIpListIngress types.String `tfsdk:"access_ip_list_ingress" json:"accessIpListIngress"`
+ NeedExternalAddressAPI types.Bool `tfsdk:"need_external_address_a_p_i" json:"needExternalAddressAPI"`
+ AccessIpListAPI types.String `tfsdk:"access_ip_list_a_p_i" json:"accessIpListAPI"`
+}
+
+// K8sSthutrvalClusterClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type K8sSthutrvalClusterClusterConfigurationModel struct {
+ AppVersion types.String `tfsdk:"app_version" json:"appVersion"`
+}
+
+// K8sSthutrvalClusterControlPlaneConfigurationModel — вложенная модель для map-fixed параметра controlPlaneConfiguration.
+type K8sSthutrvalClusterControlPlaneConfigurationModel struct {
+ SizingDisk types.Int64 `tfsdk:"sizing_disk" json:"sizingDisk"`
+ Count types.Int64 `tfsdk:"count" json:"count"`
+ SizingPolicy types.String `tfsdk:"sizing_policy" json:"sizingPolicy"`
+}
+
+// K8sSthutrvalClusterStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type K8sSthutrvalClusterStartupConfigurationModel struct {
+ ClusterName types.String `tfsdk:"cluster_name" json:"clusterName"`
+ VdcUid types.String `tfsdk:"vdc_uid" json:"vdcUid"`
+ NsxtUid types.String `tfsdk:"nsxt_uid" json:"nsxtUid"`
+}
+
+type K8sSthutrvalClusterModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AccessConfiguration *K8sSthutrvalClusterAccessConfigurationModel `tfsdk:"access_configuration"`
+ ClusterConfiguration *K8sSthutrvalClusterClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ ControlPlaneConfiguration *K8sSthutrvalClusterControlPlaneConfigurationModel `tfsdk:"control_plane_configuration"`
+ StartupConfiguration *K8sSthutrvalClusterStartupConfigurationModel `tfsdk:"startup_configuration"`
+ WorkerConfiguration types.String `tfsdk:"worker_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewK8sSthutrvalClusterResource() resource.Resource {
+ return &K8sSthutrvalClusterResource{}
+}
+
+func (r *K8sSthutrvalClusterResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_k8s_sthutrval_cluster"
+}
+
+func (r *K8sSthutrvalClusterResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "need_external_address_ingress": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Выделяет внешний IP для доступа к Ingress"},
+ "access_ip_list_ingress": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Access IP List для подключения к Ingress. Указывается массив. Если передан пустой массив, доступ выделяется всем"},
+ "need_external_address_a_p_i": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Выделяет внешний IP для доступа к kubernetes API. Пока нельзя выставить false"},
+ "access_ip_list_a_p_i": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Access IP List для подключения к Kubernetes API. Указывается массив. Если передан пустой массив, доступ выделяется всем"},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "app_version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("2.13.1")},
+ }},
+ "control_plane_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "sizing_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(50), MarkdownDescription: "Дисковая политика берётся из услуги VDC, первый в списке"},
+ "count": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ "sizing_policy": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Доступные sizing Policy можно обновлять через услугу `Виртуальный датацентр`"},
+ }},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "cluster_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("example-vlzeyhxy")},
+ "vdc_uid": schema.StringAttribute{Required: true},
+ "nsxt_uid": schema.StringAttribute{Required: true},
+ }},
+ "worker_configuration": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]")},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *K8sSthutrvalClusterResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *K8sSthutrvalClusterModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *K8sSthutrvalClusterModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan K8sSthutrvalClusterModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 750: resources_core.BuildJSON(map[string]string{"clusterName": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ClusterName.ValueString()), "vdcUid": fmt.Sprintf("\"%s\"", config.StartupConfiguration.VdcUid.ValueString()), "nsxtUid": fmt.Sprintf("\"%s\"", config.StartupConfiguration.NsxtUid.ValueString())}),
+ 751: resources_core.BuildJSON(map[string]string{"sizingDisk": fmt.Sprintf("%d", config.ControlPlaneConfiguration.SizingDisk.ValueInt64()), "count": fmt.Sprintf("%d", config.ControlPlaneConfiguration.Count.ValueInt64()), "sizingPolicy": fmt.Sprintf("\"%s\"", config.ControlPlaneConfiguration.SizingPolicy.ValueString())}),
+ 752: resources_core.FormatString(config.WorkerConfiguration),
+ 753: resources_core.BuildJSON(map[string]string{"needExternalAddressIngress": fmt.Sprintf("%v", config.AccessConfiguration.NeedExternalAddressIngress.ValueBool()), "accessIpListIngress": fmt.Sprintf("\"%s\"", config.AccessConfiguration.AccessIpListIngress.ValueString()), "needExternalAddressAPI": fmt.Sprintf("%v", config.AccessConfiguration.NeedExternalAddressAPI.ValueBool()), "accessIpListAPI": fmt.Sprintf("\"%s\"", config.AccessConfiguration.AccessIpListAPI.ValueString())}),
+ 755: resources_core.BuildJSON(map[string]string{"appVersion": fmt.Sprintf("\"%s\"", config.ClusterConfiguration.AppVersion.ValueString())}),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 150, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *K8sSthutrvalClusterResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data K8sSthutrvalClusterModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 150, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 750: resources_core.BuildJSON(map[string]string{"clusterName": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ClusterName.ValueString()), "vdcUid": fmt.Sprintf("\"%s\"", data.StartupConfiguration.VdcUid.ValueString()), "nsxtUid": fmt.Sprintf("\"%s\"", data.StartupConfiguration.NsxtUid.ValueString())}),
+ 751: resources_core.BuildJSON(map[string]string{"sizingDisk": fmt.Sprintf("%d", data.ControlPlaneConfiguration.SizingDisk.ValueInt64()), "count": fmt.Sprintf("%d", data.ControlPlaneConfiguration.Count.ValueInt64()), "sizingPolicy": fmt.Sprintf("\"%s\"", data.ControlPlaneConfiguration.SizingPolicy.ValueString())}),
+ 752: resources_core.FormatString(data.WorkerConfiguration),
+ 753: resources_core.BuildJSON(map[string]string{"needExternalAddressIngress": fmt.Sprintf("%v", data.AccessConfiguration.NeedExternalAddressIngress.ValueBool()), "accessIpListIngress": fmt.Sprintf("\"%s\"", data.AccessConfiguration.AccessIpListIngress.ValueString()), "needExternalAddressAPI": fmt.Sprintf("%v", data.AccessConfiguration.NeedExternalAddressAPI.ValueBool()), "accessIpListAPI": fmt.Sprintf("\"%s\"", data.AccessConfiguration.AccessIpListAPI.ValueString())}),
+ 755: resources_core.BuildJSON(map[string]string{"appVersion": fmt.Sprintf("\"%s\"", data.ClusterConfiguration.AppVersion.ValueString())}),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 150, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 150, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8sSthutrvalClusterResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state K8sSthutrvalClusterModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 150, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *K8sSthutrvalClusterResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan K8sSthutrvalClusterModel
+ var state K8sSthutrvalClusterModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.WorkerConfiguration.IsNull() != state.WorkerConfiguration.IsNull() || plan.WorkerConfiguration.IsUnknown() != state.WorkerConfiguration.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.WorkerConfiguration.IsNull() && !plan.WorkerConfiguration.IsUnknown() && !state.WorkerConfiguration.IsNull() && !state.WorkerConfiguration.IsUnknown() {
+ if !strings.EqualFold(plan.WorkerConfiguration.ValueString(), state.WorkerConfiguration.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 756: resources_core.BuildJSON(map[string]string{"appVersion": fmt.Sprintf("\"%s\"", plan.ClusterConfiguration.AppVersion.ValueString())}),
+ 757: resources_core.BuildJSON(map[string]string{"sizingPolicy": fmt.Sprintf("\"%s\"", plan.ControlPlaneConfiguration.SizingPolicy.ValueString()), "sizingDisk": fmt.Sprintf("%d", plan.ControlPlaneConfiguration.SizingDisk.ValueInt64()), "count": fmt.Sprintf("%d", plan.ControlPlaneConfiguration.Count.ValueInt64())}),
+ 758: resources_core.FormatString(plan.WorkerConfiguration),
+ 759: resources_core.BuildJSON(map[string]string{"needExternalAddressIngress": fmt.Sprintf("%v", plan.AccessConfiguration.NeedExternalAddressIngress.ValueBool()), "accessIpListIngress": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.AccessIpListIngress.ValueString()), "needExternalAddressAPI": fmt.Sprintf("%v", plan.AccessConfiguration.NeedExternalAddressAPI.ValueBool()), "accessIpListAPI": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.AccessIpListAPI.ValueString())}),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 150, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8sSthutrvalClusterResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state K8sSthutrvalClusterModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *K8sSthutrvalClusterResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *K8sSthutrvalClusterResource) 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
+}
diff --git a/generated/dev/go/150_k8s_sthutrval_cluster_user_resource.go b/generated/dev/go/150_k8s_sthutrval_cluster_user_resource.go
new file mode 100644
index 0000000..55faf0e
--- /dev/null
+++ b/generated/dev/go/150_k8s_sthutrval_cluster_user_resource.go
@@ -0,0 +1,343 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: k8s_sthutrval_cluster
+// Subresource: user
+
+var _ resource.Resource = &K8sSthutrvalClusterUserResource{}
+
+// K8sSthutrvalClusterUserResource manages a subresource via operations.
+type K8sSthutrvalClusterUserResource struct {
+ client *core.UniversalClient
+}
+
+type K8sSthutrvalClusterUserModel struct {
+ ID types.String `tfsdk:"id"`
+ K8sSthutrvalClusterID types.String `tfsdk:"k8s_sthutrval_cluster_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"`
+ Admins types.String `tfsdk:"admins"`
+}
+
+func NewK8sSthutrvalClusterUserResource() resource.Resource {
+ return &K8sSthutrvalClusterUserResource{}
+}
+
+func (r *K8sSthutrvalClusterUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_k8s_sthutrval_cluster_user"
+}
+
+func (r *K8sSthutrvalClusterUserResource) 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()}},
+ "k8s_sthutrval_cluster_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."},
+ "admins": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *K8sSthutrvalClusterUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan K8sSthutrvalClusterUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.K8sSthutrvalClusterID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "admins": resources_core.FormatString(plan.Admins),
+ }
+ identityCodes := []string{
+ "admins",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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, "user", 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{
+ "admins": resources_core.FormatString(plan.Admins),
+ })
+
+ 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, "create_user", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", 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, "user", 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, "user", 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, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *K8sSthutrvalClusterUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state K8sSthutrvalClusterUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8sSthutrvalClusterUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan K8sSthutrvalClusterUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state K8sSthutrvalClusterUserModel
+ 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.K8sSthutrvalClusterID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "admins": resources_core.FormatString(plan.Admins),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *K8sSthutrvalClusterUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state K8sSthutrvalClusterUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_user" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.K8sSthutrvalClusterID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "admins": resources_core.FormatString(state.Admins),
+ }
+ identityCodes := []string{
+ "admins",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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{
+ "admins": resources_core.FormatString(state.Admins),
+ })
+
+ 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, "delete_user", 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 *K8sSthutrvalClusterUserResource) 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
+}
diff --git a/generated/dev/go/151_k8s_openbao_kv_resource.go b/generated/dev/go/151_k8s_openbao_kv_resource.go
new file mode 100644
index 0000000..773a5c2
--- /dev/null
+++ b/generated/dev/go/151_k8s_openbao_kv_resource.go
@@ -0,0 +1,343 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: k8s_openbao
+// Subresource: kv
+
+var _ resource.Resource = &K8sOpenbaoKvResource{}
+
+// K8sOpenbaoKvResource manages a subresource via operations.
+type K8sOpenbaoKvResource struct {
+ client *core.UniversalClient
+}
+
+type K8sOpenbaoKvModel struct {
+ ID types.String `tfsdk:"id"`
+ K8sOpenbaoID types.String `tfsdk:"k8s_openbao_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"`
+ KvName types.String `tfsdk:"kv_name"`
+}
+
+func NewK8sOpenbaoKvResource() resource.Resource {
+ return &K8sOpenbaoKvResource{}
+}
+
+func (r *K8sOpenbaoKvResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_k8s_openbao_kv"
+}
+
+func (r *K8sOpenbaoKvResource) 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()}},
+ "k8s_openbao_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."},
+ "kv_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("kvname-ldnwybqu"), MarkdownDescription: "Будет создан `v2 тип движка`
Уникальное в рамках услуги Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *K8sOpenbaoKvResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan K8sOpenbaoKvModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.K8sOpenbaoID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "kvName": resources_core.FormatString(plan.KvName),
+ }
+ identityCodes := []string{
+ "kvName",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("kv", 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, "kv", 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{
+ "kvName": resources_core.FormatString(plan.KvName),
+ })
+
+ 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, "create_kv", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_kv", 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, "kv", 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, "kv", 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, "kv", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *K8sOpenbaoKvResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state K8sOpenbaoKvModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8sOpenbaoKvResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan K8sOpenbaoKvModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state K8sOpenbaoKvModel
+ 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.K8sOpenbaoID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "kvName": resources_core.FormatString(plan.KvName),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "kv", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *K8sOpenbaoKvResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state K8sOpenbaoKvModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_kv" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.K8sOpenbaoID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "kvName": resources_core.FormatString(state.KvName),
+ }
+ identityCodes := []string{
+ "kvName",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("kv", 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{
+ "kvName": resources_core.FormatString(state.KvName),
+ })
+
+ 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, "delete_kv", 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 *K8sOpenbaoKvResource) 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
+}
diff --git a/generated/dev/go/151_k8s_openbao_policy_resource.go b/generated/dev/go/151_k8s_openbao_policy_resource.go
new file mode 100644
index 0000000..668ed29
--- /dev/null
+++ b/generated/dev/go/151_k8s_openbao_policy_resource.go
@@ -0,0 +1,355 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: k8s_openbao
+// Subresource: policy
+
+var _ resource.Resource = &K8sOpenbaoPolicyResource{}
+
+// K8sOpenbaoPolicyResource manages a subresource via operations.
+type K8sOpenbaoPolicyResource struct {
+ client *core.UniversalClient
+}
+
+type K8sOpenbaoPolicyModel struct {
+ ID types.String `tfsdk:"id"`
+ K8sOpenbaoID types.String `tfsdk:"k8s_openbao_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"`
+ PolicyName types.String `tfsdk:"policy_name"`
+ PolicySettings types.String `tfsdk:"policy_settings"`
+}
+
+func NewK8sOpenbaoPolicyResource() resource.Resource {
+ return &K8sOpenbaoPolicyResource{}
+}
+
+func (r *K8sOpenbaoPolicyResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_k8s_openbao_policy"
+}
+
+func (r *K8sOpenbaoPolicyResource) 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()}},
+ "k8s_openbao_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."},
+ "policy_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("policy-re1w8vbp"), MarkdownDescription: "Сделано для дальнейшего удобного использования Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "policy_settings": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *K8sOpenbaoPolicyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan K8sOpenbaoPolicyModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if plan.PolicySettings.IsNull() || plan.PolicySettings.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "policy_settings is required.")
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.K8sOpenbaoID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "policyName": resources_core.FormatString(plan.PolicyName),
+ "policySettings": resources_core.FormatString(plan.PolicySettings),
+ }
+ identityCodes := []string{
+ "policyName",
+ "policySettings",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("policy", 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, "policy", 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{
+ "policySettings": resources_core.FormatString(plan.PolicySettings),
+ "policyName": resources_core.FormatString(plan.PolicyName),
+ })
+
+ 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, "create_policy", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_policy", 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, "policy", 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, "policy", 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, "policy", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *K8sOpenbaoPolicyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state K8sOpenbaoPolicyModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8sOpenbaoPolicyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan K8sOpenbaoPolicyModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state K8sOpenbaoPolicyModel
+ 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.K8sOpenbaoID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "policyName": resources_core.FormatString(plan.PolicyName),
+ "policySettings": resources_core.FormatString(plan.PolicySettings),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "policy", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *K8sOpenbaoPolicyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state K8sOpenbaoPolicyModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_policy" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.K8sOpenbaoID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "policyName": resources_core.FormatString(state.PolicyName),
+ "policySettings": resources_core.FormatString(state.PolicySettings),
+ }
+ identityCodes := []string{
+ "policyName",
+ "policySettings",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("policy", 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{
+ "policyName": resources_core.FormatString(state.PolicyName),
+ })
+
+ 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, "delete_policy", 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 *K8sOpenbaoPolicyResource) 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
+}
diff --git a/generated/dev/go/151_k8s_openbao_resource.go b/generated/dev/go/151_k8s_openbao_resource.go
new file mode 100644
index 0000000..821648e
--- /dev/null
+++ b/generated/dev/go/151_k8s_openbao_resource.go
@@ -0,0 +1,468 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: k8s_openbao
+// Service ID: 151
+
+var _ resource.Resource = &K8sOpenbaoResource{}
+var _ resource.ResourceWithModifyPlan = &K8sOpenbaoResource{}
+var _ resource.ResourceWithImportState = &K8sOpenbaoResource{}
+
+type K8sOpenbaoResource struct {
+ client *core.UniversalClient
+}
+
+type K8sOpenbaoModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AppVersion types.String `tfsdk:"app_version"`
+ Domain types.String `tfsdk:"domain"`
+ ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"`
+ ResourceDisk types.Int64 `tfsdk:"resource_disk"`
+ ResourceInstances types.Int64 `tfsdk:"resource_instances"`
+ ResourceMemory types.Int64 `tfsdk:"resource_memory"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewK8sOpenbaoResource() resource.Resource {
+ return &K8sOpenbaoResource{}
+}
+
+func (r *K8sOpenbaoResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_k8s_openbao"
+}
+
+func (r *K8sOpenbaoResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "app_version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("2.5.2"), MarkdownDescription: "Выбрать из списка"},
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("bao-pasczlik"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."},
+ "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Пример: `500`"},
+ "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Пример: `10`"},
+ "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(3), MarkdownDescription: "Пример: `3`"},
+ "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Пример: `2048`"},
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes в котором будет развёрнуто приложение Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *K8sOpenbaoResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *K8sOpenbaoModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *K8sOpenbaoModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan K8sOpenbaoModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.ResourceRealm.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+ if config.ResourceRealm.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 663: resources_core.FormatString(config.ResourceRealm),
+ 664: resources_core.FormatInt64(config.ResourceCPU),
+ 665: resources_core.FormatInt64(config.ResourceInstances),
+ 666: resources_core.FormatInt64(config.ResourceMemory),
+ 667: resources_core.FormatInt64(config.ResourceDisk),
+ 668: resources_core.FormatString(config.AppVersion),
+ 669: resources_core.FormatString(config.Domain),
+ }
+ desiredDomain := ""
+ if !config.Domain.IsNull() && !config.Domain.IsUnknown() {
+ desiredDomain = config.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 151, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *K8sOpenbaoResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data K8sOpenbaoModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ if !data.Domain.IsNull() && !data.Domain.IsUnknown() {
+ desiredDomain = data.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 151, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 663: resources_core.FormatString(data.ResourceRealm),
+ 664: resources_core.FormatInt64(data.ResourceCPU),
+ 665: resources_core.FormatInt64(data.ResourceInstances),
+ 666: resources_core.FormatInt64(data.ResourceMemory),
+ 667: resources_core.FormatInt64(data.ResourceDisk),
+ 668: resources_core.FormatString(data.AppVersion),
+ 669: resources_core.FormatString(data.Domain),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 151, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 151, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "appVersion", Field: "AppVersion", Type: "string"},
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8sOpenbaoResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state K8sOpenbaoModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 151, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "appVersion", Field: "AppVersion", Type: "string"},
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *K8sOpenbaoResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan K8sOpenbaoModel
+ var state K8sOpenbaoModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.ResourceInstances.IsNull() != state.ResourceInstances.IsNull() || plan.ResourceInstances.IsUnknown() != state.ResourceInstances.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() {
+ if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() {
+ if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() {
+ if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceDisk.IsNull() != state.ResourceDisk.IsNull() || plan.ResourceDisk.IsUnknown() != state.ResourceDisk.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() {
+ if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.AppVersion.IsNull() != state.AppVersion.IsNull() || plan.AppVersion.IsUnknown() != state.AppVersion.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.AppVersion.IsNull() && !plan.AppVersion.IsUnknown() && !state.AppVersion.IsNull() && !state.AppVersion.IsUnknown() {
+ if !strings.EqualFold(plan.AppVersion.ValueString(), state.AppVersion.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 670: resources_core.FormatInt64(plan.ResourceInstances),
+ 671: resources_core.FormatInt64(plan.ResourceCPU),
+ 672: resources_core.FormatInt64(plan.ResourceMemory),
+ 673: resources_core.FormatInt64(plan.ResourceDisk),
+ 674: resources_core.FormatString(plan.AppVersion),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 151, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "appVersion", Field: "AppVersion", Type: "string"},
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8sOpenbaoResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state K8sOpenbaoModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *K8sOpenbaoResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *K8sOpenbaoResource) 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
+}
diff --git a/generated/dev/go/151_k8s_openbao_secrets_resource.go b/generated/dev/go/151_k8s_openbao_secrets_resource.go
new file mode 100644
index 0000000..161164e
--- /dev/null
+++ b/generated/dev/go/151_k8s_openbao_secrets_resource.go
@@ -0,0 +1,368 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: k8s_openbao
+// Subresource: secrets
+
+var _ resource.Resource = &K8sOpenbaoSecretsResource{}
+
+// K8sOpenbaoSecretsResource manages a subresource via operations.
+type K8sOpenbaoSecretsResource struct {
+ client *core.UniversalClient
+}
+
+type K8sOpenbaoSecretsModel struct {
+ ID types.String `tfsdk:"id"`
+ K8sOpenbaoID types.String `tfsdk:"k8s_openbao_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"`
+ KvName types.String `tfsdk:"kv_name"`
+ SecretData types.String `tfsdk:"secret_data"`
+ SecretPath types.String `tfsdk:"secret_path"`
+}
+
+func NewK8sOpenbaoSecretsResource() resource.Resource {
+ return &K8sOpenbaoSecretsResource{}
+}
+
+func (r *K8sOpenbaoSecretsResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_k8s_openbao_secrets"
+}
+
+func (r *K8sOpenbaoSecretsResource) 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()}},
+ "k8s_openbao_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."},
+ "kv_name": schema.StringAttribute{Required: true, MarkdownDescription: "Выберите из списка Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "secret_data": schema.StringAttribute{Required: true, MarkdownDescription: "Хотя бы один секрет необходимо добавить Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "secret_path": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("path/to/mysecret"), MarkdownDescription: "Пример: `path` или `road/to/path` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *K8sOpenbaoSecretsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan K8sOpenbaoSecretsModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if plan.KvName.IsNull() || plan.KvName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "kv_name is required.")
+ return
+ }
+ if plan.SecretData.IsNull() || plan.SecretData.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "secret_data is required.")
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.K8sOpenbaoID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "kvName": resources_core.FormatString(plan.KvName),
+ "secretData": resources_core.FormatString(plan.SecretData),
+ "secretPath": resources_core.FormatString(plan.SecretPath),
+ }
+ identityCodes := []string{
+ "kvName",
+ "secretData",
+ "secretPath",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("secrets", 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, "secrets", 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{
+ "kvName": resources_core.FormatString(plan.KvName),
+ "secretPath": resources_core.FormatString(plan.SecretPath),
+ "secretData": resources_core.FormatString(plan.SecretData),
+ })
+
+ 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, "create_secrets", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_secrets", 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, "secrets", 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, "secrets", 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, "secrets", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *K8sOpenbaoSecretsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state K8sOpenbaoSecretsModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8sOpenbaoSecretsResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan K8sOpenbaoSecretsModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state K8sOpenbaoSecretsModel
+ 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.K8sOpenbaoID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "kvName": resources_core.FormatString(plan.KvName),
+ "secretData": resources_core.FormatString(plan.SecretData),
+ "secretPath": resources_core.FormatString(plan.SecretPath),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "secrets", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *K8sOpenbaoSecretsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state K8sOpenbaoSecretsModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_secrets" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.K8sOpenbaoID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "kvName": resources_core.FormatString(state.KvName),
+ "secretData": resources_core.FormatString(state.SecretData),
+ "secretPath": resources_core.FormatString(state.SecretPath),
+ }
+ identityCodes := []string{
+ "kvName",
+ "secretData",
+ "secretPath",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("secrets", 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{
+ "kvName": resources_core.FormatString(state.KvName),
+ "secretPath": resources_core.FormatString(state.SecretPath),
+ })
+
+ 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, "delete_secrets", 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 *K8sOpenbaoSecretsResource) 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
+}
diff --git a/generated/dev/go/153_nifi_resource.go b/generated/dev/go/153_nifi_resource.go
new file mode 100644
index 0000000..12049ba
--- /dev/null
+++ b/generated/dev/go/153_nifi_resource.go
@@ -0,0 +1,474 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: nifi
+// Service ID: 153
+
+var _ resource.Resource = &NifiResource{}
+var _ resource.ResourceWithModifyPlan = &NifiResource{}
+var _ resource.ResourceWithImportState = &NifiResource{}
+
+type NifiResource struct {
+ client *core.UniversalClient
+}
+
+type NifiModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AppVersion types.String `tfsdk:"app_version"`
+ Domain types.String `tfsdk:"domain"`
+ JdbcDrivers types.String `tfsdk:"jdbc_drivers"`
+ ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"`
+ ResourceDisk types.Int64 `tfsdk:"resource_disk"`
+ ResourceInstances types.Int64 `tfsdk:"resource_instances"`
+ ResourceMemory types.Int64 `tfsdk:"resource_memory"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewNifiResource() resource.Resource {
+ return &NifiResource{}
+}
+
+func (r *NifiResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_nifi"
+}
+
+func (r *NifiResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "app_version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("2.6.0"), MarkdownDescription: "Выбрать из списка Не изменяется после создания."},
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("nifi-lvu4xj7j"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."},
+ "jdbc_drivers": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `https://jdbc.postgresql.org/download/postgresql-42.7.8.jar` Не изменяется после создания."},
+ "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2000), MarkdownDescription: "Пример: `2000` Не изменяется после создания."},
+ "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Пример: `10` Не изменяется после создания."},
+ "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Пример: `1` Не изменяется после создания."},
+ "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(4096), MarkdownDescription: "Пример: `2048` Не изменяется после создания."},
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes в котором будет развёрнуто приложение Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *NifiResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *NifiModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *NifiModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan NifiModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.AppVersion.IsNull() && !plan.AppVersion.IsUnknown() && !state.AppVersion.IsNull() && !state.AppVersion.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.AppVersion.ValueString(), state.AppVersion.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить app_version", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.JdbcDrivers.IsNull() && !plan.JdbcDrivers.IsUnknown() && !state.JdbcDrivers.IsNull() && !state.JdbcDrivers.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.JdbcDrivers.ValueString(), state.JdbcDrivers.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить jdbc_drivers", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() {
+ if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_c_p_u", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() {
+ if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_disk", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() {
+ if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_instances", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() {
+ if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_memory", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.JdbcDrivers.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "jdbc_drivers is required.")
+ return
+ }
+ if config.JdbcDrivers.IsUnknown() {
+ return
+ }
+ if config.ResourceRealm.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+ if config.ResourceRealm.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 711: resources_core.FormatString(config.Domain),
+ 712: resources_core.FormatInt64(config.ResourceCPU),
+ 713: resources_core.FormatInt64(config.ResourceMemory),
+ 714: resources_core.FormatInt64(config.ResourceInstances),
+ 715: resources_core.FormatInt64(config.ResourceDisk),
+ 716: resources_core.FormatString(config.AppVersion),
+ 717: resources_core.FormatString(config.ResourceRealm),
+ 718: resources_core.FormatString(config.JdbcDrivers),
+ }
+ desiredDomain := ""
+ if !config.Domain.IsNull() && !config.Domain.IsUnknown() {
+ desiredDomain = config.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 153, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *NifiResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data NifiModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.JdbcDrivers.IsNull() || data.JdbcDrivers.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "jdbc_drivers is required.")
+ return
+ }
+ if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ if !data.Domain.IsNull() && !data.Domain.IsUnknown() {
+ desiredDomain = data.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 153, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 711: resources_core.FormatString(data.Domain),
+ 712: resources_core.FormatInt64(data.ResourceCPU),
+ 713: resources_core.FormatInt64(data.ResourceMemory),
+ 714: resources_core.FormatInt64(data.ResourceInstances),
+ 715: resources_core.FormatInt64(data.ResourceDisk),
+ 716: resources_core.FormatString(data.AppVersion),
+ 717: resources_core.FormatString(data.ResourceRealm),
+ 718: resources_core.FormatString(data.JdbcDrivers),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 153, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 153, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "appVersion", Field: "AppVersion", Type: "string"},
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "jdbcDrivers", Field: "JdbcDrivers", Type: "array-map-fixed"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *NifiResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state NifiModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 153, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "appVersion", Field: "AppVersion", Type: "string"},
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "jdbcDrivers", Field: "JdbcDrivers", Type: "array-map-fixed"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *NifiResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan NifiModel
+ var state NifiModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 153, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "appVersion", Field: "AppVersion", Type: "string"},
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "jdbcDrivers", Field: "JdbcDrivers", Type: "array-map-fixed"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *NifiResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state NifiModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *NifiResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *NifiResource) 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
+}
diff --git a/generated/dev/go/163_llm_ai_resource.go b/generated/dev/go/163_llm_ai_resource.go
new file mode 100644
index 0000000..22a1d70
--- /dev/null
+++ b/generated/dev/go/163_llm_ai_resource.go
@@ -0,0 +1,345 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: llm_ai
+// Service ID: 163
+
+var _ resource.Resource = &LlmAiResource{}
+var _ resource.ResourceWithModifyPlan = &LlmAiResource{}
+var _ resource.ResourceWithImportState = &LlmAiResource{}
+
+type LlmAiResource struct {
+ client *core.UniversalClient
+}
+
+type LlmAiModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewLlmAiResource() resource.Resource {
+ return &LlmAiResource{}
+}
+
+func (r *LlmAiResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_llm_ai"
+}
+
+func (r *LlmAiResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("llmAi.aillm.ru"), MarkdownDescription: "Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *LlmAiResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *LlmAiModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *LlmAiModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan LlmAiModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 826: resources_core.FormatString(config.ResourceRealm),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 163, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *LlmAiResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data LlmAiModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 163, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 826: resources_core.FormatString(data.ResourceRealm),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 163, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 163, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *LlmAiResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state LlmAiModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 163, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *LlmAiResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan LlmAiModel
+ var state LlmAiModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 163, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *LlmAiResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state LlmAiModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *LlmAiResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *LlmAiResource) 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
+}
diff --git a/generated/dev/go/163_llm_ai_secrets_resource.go b/generated/dev/go/163_llm_ai_secrets_resource.go
new file mode 100644
index 0000000..0525342
--- /dev/null
+++ b/generated/dev/go/163_llm_ai_secrets_resource.go
@@ -0,0 +1,401 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: llm_ai
+// Subresource: secrets
+
+var _ resource.Resource = &LlmAiSecretsResource{}
+
+// LlmAiSecretsResource manages a subresource via operations.
+type LlmAiSecretsResource struct {
+ client *core.UniversalClient
+}
+
+type LlmAiSecretsModel struct {
+ ID types.String `tfsdk:"id"`
+ LlmAiID types.String `tfsdk:"llm_ai_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"`
+ KeyName types.String `tfsdk:"key_name"`
+ Models types.String `tfsdk:"models"`
+ RpmLimit types.Int64 `tfsdk:"rpm_limit"`
+ TeamName types.String `tfsdk:"team_name"`
+ TpmLimit types.Int64 `tfsdk:"tpm_limit"`
+ VariablesKeys types.String `tfsdk:"variables_keys"`
+}
+
+func NewLlmAiSecretsResource() resource.Resource {
+ return &LlmAiSecretsResource{}
+}
+
+func (r *LlmAiSecretsResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_llm_ai_secrets"
+}
+
+func (r *LlmAiSecretsResource) 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()}},
+ "llm_ai_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."},
+ "key_name": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "models": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "rpm_limit": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(60), MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.Int64{int64planmodifier.RequiresReplace()}},
+ "team_name": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "tpm_limit": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100000), MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.Int64{int64planmodifier.RequiresReplace()}},
+ "variables_keys": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *LlmAiSecretsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan LlmAiSecretsModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if plan.KeyName.IsNull() || plan.KeyName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "key_name is required.")
+ return
+ }
+ if plan.Models.IsNull() || plan.Models.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "models is required.")
+ return
+ }
+ if plan.TeamName.IsNull() || plan.TeamName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "team_name is required.")
+ return
+ }
+ if plan.VariablesKeys.IsNull() || plan.VariablesKeys.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "variables_keys is required.")
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.LlmAiID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "keyName": resources_core.FormatString(plan.KeyName),
+ "models": resources_core.FormatString(plan.Models),
+ "rpmLimit": resources_core.FormatInt64(plan.RpmLimit),
+ "teamName": resources_core.FormatString(plan.TeamName),
+ "tpmLimit": resources_core.FormatInt64(plan.TpmLimit),
+ "variablesKeys": resources_core.FormatString(plan.VariablesKeys),
+ }
+ identityCodes := []string{
+ "keyName",
+ "models",
+ "rpmLimit",
+ "teamName",
+ "tpmLimit",
+ "variablesKeys",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("secrets", 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, "secrets", 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{
+ "models": resources_core.FormatString(plan.Models),
+ "rpmLimit": resources_core.FormatInt64(plan.RpmLimit),
+ "tpmLimit": resources_core.FormatInt64(plan.TpmLimit),
+ "keyName": resources_core.FormatString(plan.KeyName),
+ "teamName": resources_core.FormatString(plan.TeamName),
+ "variablesKeys": resources_core.FormatString(plan.VariablesKeys),
+ })
+
+ 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, "create_secrets", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_secrets", 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, "secrets", 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, "secrets", 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, "secrets", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *LlmAiSecretsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state LlmAiSecretsModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *LlmAiSecretsResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan LlmAiSecretsModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state LlmAiSecretsModel
+ 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.LlmAiID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "keyName": resources_core.FormatString(plan.KeyName),
+ "models": resources_core.FormatString(plan.Models),
+ "rpmLimit": resources_core.FormatInt64(plan.RpmLimit),
+ "teamName": resources_core.FormatString(plan.TeamName),
+ "tpmLimit": resources_core.FormatInt64(plan.TpmLimit),
+ "variablesKeys": resources_core.FormatString(plan.VariablesKeys),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "secrets", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *LlmAiSecretsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state LlmAiSecretsModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_secrets" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.LlmAiID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "keyName": resources_core.FormatString(state.KeyName),
+ "models": resources_core.FormatString(state.Models),
+ "rpmLimit": resources_core.FormatInt64(state.RpmLimit),
+ "teamName": resources_core.FormatString(state.TeamName),
+ "tpmLimit": resources_core.FormatInt64(state.TpmLimit),
+ "variablesKeys": resources_core.FormatString(state.VariablesKeys),
+ }
+ identityCodes := []string{
+ "keyName",
+ "models",
+ "rpmLimit",
+ "teamName",
+ "tpmLimit",
+ "variablesKeys",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("secrets", 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{
+ "keyName": resources_core.FormatString(state.KeyName),
+ "teamName": resources_core.FormatString(state.TeamName),
+ })
+
+ 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, "delete_secrets", 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 *LlmAiSecretsResource) 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
+}
diff --git a/generated/dev/go/163_llm_ai_team_resource.go b/generated/dev/go/163_llm_ai_team_resource.go
new file mode 100644
index 0000000..e411362
--- /dev/null
+++ b/generated/dev/go/163_llm_ai_team_resource.go
@@ -0,0 +1,365 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: llm_ai
+// Subresource: team
+
+var _ resource.Resource = &LlmAiTeamResource{}
+
+// LlmAiTeamResource manages a subresource via operations.
+type LlmAiTeamResource struct {
+ client *core.UniversalClient
+}
+
+type LlmAiTeamModel struct {
+ ID types.String `tfsdk:"id"`
+ LlmAiID types.String `tfsdk:"llm_ai_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"`
+ Models types.String `tfsdk:"models"`
+ RpmLimit types.Int64 `tfsdk:"rpm_limit"`
+ TeamName types.String `tfsdk:"team_name"`
+ TpmLimit types.Int64 `tfsdk:"tpm_limit"`
+}
+
+func NewLlmAiTeamResource() resource.Resource {
+ return &LlmAiTeamResource{}
+}
+
+func (r *LlmAiTeamResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_llm_ai_team"
+}
+
+func (r *LlmAiTeamResource) 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()}},
+ "llm_ai_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."},
+ "models": schema.StringAttribute{Required: true},
+ "rpm_limit": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(60)},
+ "team_name": schema.StringAttribute{Required: true, MarkdownDescription: "Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "tpm_limit": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100000)},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *LlmAiTeamResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan LlmAiTeamModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if plan.Models.IsNull() || plan.Models.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "models is required.")
+ return
+ }
+ if plan.TeamName.IsNull() || plan.TeamName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "team_name is required.")
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.LlmAiID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "teamName": resources_core.FormatString(plan.TeamName),
+ }
+ identityCodes := []string{
+ "teamName",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("team", 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, "team", 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{
+ "teamName": resources_core.FormatString(plan.TeamName),
+ "models": resources_core.FormatString(plan.Models),
+ "rpmLimit": resources_core.FormatInt64(plan.RpmLimit),
+ "tpmLimit": resources_core.FormatInt64(plan.TpmLimit),
+ })
+
+ 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, "create_team", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_team", 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, "team", 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, "team", 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, "team", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *LlmAiTeamResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state LlmAiTeamModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *LlmAiTeamResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan LlmAiTeamModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "modify_team" == "" {
+ var state LlmAiTeamModel
+ 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.LlmAiID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{
+ "teamName": resources_core.FormatString(plan.TeamName),
+ "models": resources_core.FormatString(plan.Models),
+ "rpmLimit": resources_core.FormatInt64(plan.RpmLimit),
+ "tpmLimit": resources_core.FormatInt64(plan.TpmLimit),
+ })
+
+ idParams := map[string]string{
+ "teamName": resources_core.FormatString(plan.TeamName),
+ }
+
+ 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, "modify_team", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "team", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *LlmAiTeamResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state LlmAiTeamModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_team" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.LlmAiID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "teamName": resources_core.FormatString(state.TeamName),
+ }
+ identityCodes := []string{
+ "teamName",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("team", 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{
+ "teamName": resources_core.FormatString(state.TeamName),
+ })
+
+ 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, "delete_team", 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 *LlmAiTeamResource) 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
+}
diff --git a/generated/dev/go/175_k8s_go_resource.go b/generated/dev/go/175_k8s_go_resource.go
new file mode 100644
index 0000000..f6cc30f
--- /dev/null
+++ b/generated/dev/go/175_k8s_go_resource.go
@@ -0,0 +1,422 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: k8s_go
+// Service ID: 175
+
+var _ resource.Resource = &K8sGoResource{}
+var _ resource.ResourceWithModifyPlan = &K8sGoResource{}
+var _ resource.ResourceWithImportState = &K8sGoResource{}
+
+type K8sGoResource struct {
+ client *core.UniversalClient
+}
+
+// K8sGoAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type K8sGoAccessConfigurationModel struct {
+ Domain types.String `tfsdk:"domain" json:"domain"`
+}
+
+// K8sGoAppConfigurationModel — вложенная модель для map-fixed параметра appConfiguration.
+type K8sGoAppConfigurationModel struct {
+ Version types.String `tfsdk:"version" json:"version"`
+ GitPath types.String `tfsdk:"git_path" json:"gitPath"`
+ HealthPath types.String `tfsdk:"health_path" json:"healthPath"`
+}
+
+// K8sGoClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type K8sGoClusterConfigurationModel struct {
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+}
+
+// K8sGoStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type K8sGoStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+}
+
+type K8sGoModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AccessConfiguration *K8sGoAccessConfigurationModel `tfsdk:"access_configuration"`
+ AppConfiguration *K8sGoAppConfigurationModel `tfsdk:"app_configuration"`
+ ClusterConfiguration *K8sGoClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ JsonEnv types.String `tfsdk:"json_env"`
+ StartupConfiguration *K8sGoStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewK8sGoResource() resource.Resource {
+ return &K8sGoResource{}
+}
+
+func (r *K8sGoResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_k8s_go"
+}
+
+func (r *K8sGoResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("lucee-utovgtao"), MarkdownDescription: "Если указан FQDN в формате: `myapp.example.com`, предварительно будет проверяться DNS-запись (А-запись **должна смотреть на IP Ingress кластера ресурсной платформы**)
Если указан в формате `myapp`, DNS-запись будет автоматически создана в служебной DNS-зоне"},
+ }},
+ "app_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("6.0")},
+ "git_path": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("https://github.com/xahys/testlucee"), MarkdownDescription: "Путь до репозитория приложения
Обязательно должен быть передан как URL: `https://....`
Должен быть сетевой доступ до URL для скачивания исходников"},
+ "health_path": schema.StringAttribute{Optional: true, MarkdownDescription: "Путь до healtcheck для запуска пода в Kubernetes, который отдаст 200 код
Пустое значение означает, что будет проверяться исключительно доступность приложения по порту"},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(200), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(256), MarkdownDescription: "Указывается в Megabytes"},
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ }},
+ "json_env": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("{}")},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"},
+ }},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *K8sGoResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *K8sGoModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *K8sGoModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan K8sGoModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 1046: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1047: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64())}),
+ 1048: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Domain.ValueString())}),
+ 1049: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.HealthPath.ValueString())}),
+ 1050: resources_core.FormatString(config.JsonEnv),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 175, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *K8sGoResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data K8sGoModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 175, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 1046: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1047: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64())}),
+ 1048: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Domain.ValueString())}),
+ 1049: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.HealthPath.ValueString())}),
+ 1050: resources_core.FormatString(data.JsonEnv),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 175, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 175, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8sGoResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state K8sGoModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 175, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *K8sGoResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan K8sGoModel
+ var state K8sGoModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.JsonEnv.IsNull() != state.JsonEnv.IsNull() || plan.JsonEnv.IsUnknown() != state.JsonEnv.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.JsonEnv.IsNull() && !plan.JsonEnv.IsUnknown() && !state.JsonEnv.IsNull() && !state.JsonEnv.IsUnknown() {
+ if !strings.EqualFold(plan.JsonEnv.ValueString(), state.JsonEnv.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 1051: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64())}),
+ 1052: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.HealthPath.ValueString())}),
+ 1053: resources_core.FormatString(plan.JsonEnv),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 175, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8sGoResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state K8sGoModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *K8sGoResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *K8sGoResource) 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
+}
diff --git a/generated/dev/go/19_vc_org_resource.go b/generated/dev/go/19_vc_org_resource.go
new file mode 100644
index 0000000..c2e768b
--- /dev/null
+++ b/generated/dev/go/19_vc_org_resource.go
@@ -0,0 +1,391 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_org
+// Service ID: 19
+
+var _ resource.Resource = &VcOrgResource{}
+var _ resource.ResourceWithModifyPlan = &VcOrgResource{}
+var _ resource.ResourceWithImportState = &VcOrgResource{}
+
+type VcOrgResource struct {
+ client *core.UniversalClient
+}
+
+type VcOrgModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ OrganizationType types.String `tfsdk:"organization_type"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ VIPConfigure types.String `tfsdk:"v_i_p_configure"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVcOrgResource() resource.Resource {
+ return &VcOrgResource{}
+}
+
+func (r *VcOrgResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_org"
+}
+
+func (r *VcOrgResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "organization_type": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("iaas"), MarkdownDescription: "Доступ к организации предоставляется только при выборе `iaas` Не изменяется после создания."},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("sandbox.nubes.ru"), MarkdownDescription: "Облако в котором будет развёрнут тенант Не изменяется после создания."},
+ "v_i_p_configure": schema.StringAttribute{Required: true},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcOrgResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VcOrgModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VcOrgModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VcOrgModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.OrganizationType.IsNull() && !plan.OrganizationType.IsUnknown() && !state.OrganizationType.IsNull() && !state.OrganizationType.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.OrganizationType.ValueString(), state.OrganizationType.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить organization_type", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.VIPConfigure.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "v_i_p_configure is required.")
+ return
+ }
+ if config.VIPConfigure.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 418: resources_core.FormatString(config.ResourceRealm),
+ 556: resources_core.FormatString(config.OrganizationType),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 19, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VcOrgResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VcOrgModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.VIPConfigure.IsNull() || data.VIPConfigure.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "v_i_p_configure is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 19, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 418: resources_core.FormatString(data.ResourceRealm),
+ 556: resources_core.FormatString(data.OrganizationType),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 19, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 19, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "organizationType", Field: "OrganizationType", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "vIPConfigure", Field: "VIPConfigure", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcOrgResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcOrgModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 19, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "organizationType", Field: "OrganizationType", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "vIPConfigure", Field: "VIPConfigure", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VcOrgResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcOrgModel
+ var state VcOrgModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.VIPConfigure.IsNull() != state.VIPConfigure.IsNull() || plan.VIPConfigure.IsUnknown() != state.VIPConfigure.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.VIPConfigure.IsNull() && !plan.VIPConfigure.IsUnknown() && !state.VIPConfigure.IsNull() && !state.VIPConfigure.IsUnknown() {
+ if !strings.EqualFold(plan.VIPConfigure.ValueString(), state.VIPConfigure.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 662: resources_core.FormatString(plan.VIPConfigure),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 19, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "organizationType", Field: "OrganizationType", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "vIPConfigure", Field: "VIPConfigure", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcOrgResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcOrgModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VcOrgResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VcOrgResource) 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
+}
diff --git a/generated/dev/go/19_vc_org_user_resource.go b/generated/dev/go/19_vc_org_user_resource.go
new file mode 100644
index 0000000..b18fcdd
--- /dev/null
+++ b/generated/dev/go/19_vc_org_user_resource.go
@@ -0,0 +1,343 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_org
+// Subresource: user
+
+var _ resource.Resource = &VcOrgUserResource{}
+
+// VcOrgUserResource manages a subresource via operations.
+type VcOrgUserResource struct {
+ client *core.UniversalClient
+}
+
+type VcOrgUserModel struct {
+ ID types.String `tfsdk:"id"`
+ VcOrgID types.String `tfsdk:"vc_org_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"`
+ Admins types.String `tfsdk:"admins"`
+}
+
+func NewVcOrgUserResource() resource.Resource {
+ return &VcOrgUserResource{}
+}
+
+func (r *VcOrgUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_org_user"
+}
+
+func (r *VcOrgUserResource) 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()}},
+ "vc_org_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."},
+ "admins": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcOrgUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan VcOrgUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.VcOrgID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "admins": resources_core.FormatString(plan.Admins),
+ }
+ identityCodes := []string{
+ "admins",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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, "user", 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{
+ "admins": resources_core.FormatString(plan.Admins),
+ })
+
+ 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, "create_user", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", 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, "user", 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, "user", 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, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *VcOrgUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcOrgUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcOrgUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcOrgUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state VcOrgUserModel
+ 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.VcOrgID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "admins": resources_core.FormatString(plan.Admins),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *VcOrgUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcOrgUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_user" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.VcOrgID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "admins": resources_core.FormatString(state.Admins),
+ }
+ identityCodes := []string{
+ "admins",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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{
+ "admins": resources_core.FormatString(state.Admins),
+ })
+
+ 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, "delete_user", 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 *VcOrgUserResource) 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
+}
diff --git a/generated/dev/go/1_dummy_resource.go b/generated/dev/go/1_dummy_resource.go
new file mode 100644
index 0000000..e2070da
--- /dev/null
+++ b/generated/dev/go/1_dummy_resource.go
@@ -0,0 +1,582 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: dummy
+// Service ID: 1
+
+var _ resource.Resource = &DummyResource{}
+var _ resource.ResourceWithModifyPlan = &DummyResource{}
+var _ resource.ResourceWithImportState = &DummyResource{}
+
+type DummyResource struct {
+ client *core.UniversalClient
+}
+
+// DummyMapExampleModel — вложенная модель для map-fixed параметра mapExample.
+type DummyMapExampleModel struct {
+ Subparam1 types.String `tfsdk:"subparam1" json:"subparam1"`
+ Secret types.String `tfsdk:"secret" json:"secret"`
+ Subparam2 types.String `tfsdk:"subparam2" json:"subparam2"`
+}
+
+// DummyMapFixedModel — вложенная модель для map-fixed параметра mapFixed.
+type DummyMapFixedModel struct {
+ Bol1 types.Bool `tfsdk:"bol1" json:"bol1"`
+ MinStr1 types.String `tfsdk:"min_str1" json:"minStr1"`
+ Param1 types.Int64 `tfsdk:"param1" json:"param1"`
+ Param2 types.String `tfsdk:"param2" json:"param2"`
+ Param3 types.Int64 `tfsdk:"param3" json:"param3"`
+}
+
+type DummyModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // git_revision: при изменении вызывает redeploy вместо modify.
+ // Опциональное поле — если не задано, modify работает как обычно.
+ GitRevision types.String `tfsdk:"git_revision"`
+ Arr types.String `tfsdk:"arr"`
+ ArrayMapFixedExample types.String `tfsdk:"array_map_fixed_example"`
+ Bodymessage types.String `tfsdk:"bodymessage"`
+ DurationMs types.Int64 `tfsdk:"duration_ms"`
+ FailAtStart types.Bool `tfsdk:"fail_at_start"`
+ FailInProgress types.Bool `tfsdk:"fail_in_progress"`
+ JsonExample types.String `tfsdk:"json_example"`
+ MapExample *DummyMapExampleModel `tfsdk:"map_example"`
+ MapFixed *DummyMapFixedModel `tfsdk:"map_fixed"`
+ NestedRefExample types.String `tfsdk:"nested_ref_example"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ WhereFail types.Int64 `tfsdk:"where_fail"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewDummyResource() resource.Resource {
+ return &DummyResource{}
+}
+
+func (r *DummyResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_dummy"
+}
+
+func (r *DummyResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "arr": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания."},
+ "array_map_fixed_example": schema.StringAttribute{Optional: true, MarkdownDescription: "Array of map-fixed Не изменяется после создания."},
+ "bodymessage": schema.StringAttribute{Optional: true, MarkdownDescription: "Дата, уходящая в Vault"},
+ "duration_ms": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "перед завершением подождать (sleep) столько миллисекунд"},
+ "fail_at_start": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "если true, завершить джоб, ничего не записывая в cmdb"},
+ "fail_in_progress": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "если true, корректно начать выполнение джоба, но в процессе завершить аварийно, ничего не записывая в cmdb"},
+ "json_example": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Для тестирования интерфейса", PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()}},
+ "map_example": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "subparam1": schema.StringAttribute{Optional: true, MarkdownDescription: "это для администратора"},
+ "secret": schema.StringAttribute{Optional: true, MarkdownDescription: "это секрет"},
+ "subparam2": schema.StringAttribute{Optional: true},
+ }},
+ "map_fixed": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "bol1": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Булевский, обязательный"},
+ "min_str1": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("abracadabra"), MarkdownDescription: "не меньше 10 символов"},
+ "param1": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Integer, secret"},
+ "param2": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("1"), MarkdownDescription: "Дескрипшен"},
+ "param3": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "целое положительное"},
+ }},
+ "nested_ref_example": schema.StringAttribute{Optional: true, MarkdownDescription: "Не изменяется после создания."},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("dummy"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ "where_fail": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "принимает 1,2,3. Работает с failInProgress. 1 - упасть в prepare. 2 - упасть внутри блока Data fill. 3 - упасть после vault write"},
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // При изменении git_revision вызывается redeploy вместо modify.
+ // Если не задано — modify работает без изменений.
+ "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *DummyResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *DummyModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *DummyModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan DummyModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.Arr.IsNull() && !plan.Arr.IsUnknown() && !state.Arr.IsNull() && !state.Arr.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Arr.ValueString(), state.Arr.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить arr", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ArrayMapFixedExample.IsNull() && !plan.ArrayMapFixedExample.IsUnknown() && !state.ArrayMapFixedExample.IsNull() && !state.ArrayMapFixedExample.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ArrayMapFixedExample.ValueString(), state.ArrayMapFixedExample.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить array_map_fixed_example", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.NestedRefExample.IsNull() && !plan.NestedRefExample.IsUnknown() && !state.NestedRefExample.IsNull() && !state.NestedRefExample.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.NestedRefExample.ValueString(), state.NestedRefExample.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить nested_ref_example", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.Arr.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "arr is required.")
+ return
+ }
+ if config.Arr.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 198: resources_core.FormatInt64(config.DurationMs),
+ 199: resources_core.FormatBool(config.FailAtStart),
+ 200: resources_core.FormatBool(config.FailInProgress),
+ 201: resources_core.FormatInt64(config.WhereFail),
+ 242: resources_core.FormatString(config.ResourceRealm),
+ 286: resources_core.FormatString(config.Bodymessage),
+ 321: resources_core.BuildJSON(map[string]string{"subparam1": fmt.Sprintf("\"%s\"", config.MapExample.Subparam1.ValueString()), "secret": fmt.Sprintf("\"%s\"", config.MapExample.Secret.ValueString()), "subparam2": fmt.Sprintf("\"%s\"", config.MapExample.Subparam2.ValueString())}),
+ 322: resources_core.FormatString(config.JsonExample),
+ 396: resources_core.FormatString(config.NestedRefExample),
+ 647: resources_core.BuildJSON(map[string]string{"bol1": fmt.Sprintf("%v", config.MapFixed.Bol1.ValueBool()), "minStr1": fmt.Sprintf("\"%s\"", config.MapFixed.MinStr1.ValueString()), "param1": fmt.Sprintf("%d", config.MapFixed.Param1.ValueInt64()), "param2": fmt.Sprintf("\"%s\"", config.MapFixed.Param2.ValueString()), "param3": fmt.Sprintf("%d", config.MapFixed.Param3.ValueInt64())}),
+ 654: resources_core.FormatString(config.ArrayMapFixedExample),
+ 863: resources_core.FormatString(config.Arr),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 1, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *DummyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data DummyModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.Arr.IsNull() || data.Arr.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "arr is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 1, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 198: resources_core.FormatInt64(data.DurationMs),
+ 199: resources_core.FormatBool(data.FailAtStart),
+ 200: resources_core.FormatBool(data.FailInProgress),
+ 201: resources_core.FormatInt64(data.WhereFail),
+ 242: resources_core.FormatString(data.ResourceRealm),
+ 286: resources_core.FormatString(data.Bodymessage),
+ 321: resources_core.BuildJSON(map[string]string{"subparam1": fmt.Sprintf("\"%s\"", data.MapExample.Subparam1.ValueString()), "secret": fmt.Sprintf("\"%s\"", data.MapExample.Secret.ValueString()), "subparam2": fmt.Sprintf("\"%s\"", data.MapExample.Subparam2.ValueString())}),
+ 322: resources_core.FormatString(data.JsonExample),
+ 396: resources_core.FormatString(data.NestedRefExample),
+ 647: resources_core.BuildJSON(map[string]string{"bol1": fmt.Sprintf("%v", data.MapFixed.Bol1.ValueBool()), "minStr1": fmt.Sprintf("\"%s\"", data.MapFixed.MinStr1.ValueString()), "param1": fmt.Sprintf("%d", data.MapFixed.Param1.ValueInt64()), "param2": fmt.Sprintf("\"%s\"", data.MapFixed.Param2.ValueString()), "param3": fmt.Sprintf("%d", data.MapFixed.Param3.ValueInt64())}),
+ 654: resources_core.FormatString(data.ArrayMapFixedExample),
+ 863: resources_core.FormatString(data.Arr),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 1, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 1, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "arr", Field: "Arr", Type: "string"},
+ {Code: "arrayMapFixedExample", Field: "ArrayMapFixedExample", Type: "array-map-fixed"},
+ {Code: "bodymessage", Field: "Bodymessage", Type: "string"},
+ {Code: "durationMs", Field: "DurationMs", Type: "int64"},
+ {Code: "failAtStart", Field: "FailAtStart", Type: "bool"},
+ {Code: "failInProgress", Field: "FailInProgress", Type: "bool"},
+ {Code: "jsonExample", Field: "JsonExample", Type: "string"},
+ {Code: "mapExample", Field: "MapExample", Type: "string"},
+ {Code: "mapFixed", Field: "MapFixed", Type: "map-fixed"},
+ {Code: "nestedRefExample", Field: "NestedRefExample", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "whereFail", Field: "WhereFail", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *DummyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state DummyModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 1, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "arr", Field: "Arr", Type: "string"},
+ {Code: "arrayMapFixedExample", Field: "ArrayMapFixedExample", Type: "array-map-fixed"},
+ {Code: "bodymessage", Field: "Bodymessage", Type: "string"},
+ {Code: "durationMs", Field: "DurationMs", Type: "int64"},
+ {Code: "failAtStart", Field: "FailAtStart", Type: "bool"},
+ {Code: "failInProgress", Field: "FailInProgress", Type: "bool"},
+ {Code: "jsonExample", Field: "JsonExample", Type: "string"},
+ {Code: "mapExample", Field: "MapExample", Type: "string"},
+ {Code: "mapFixed", Field: "MapFixed", Type: "map-fixed"},
+ {Code: "nestedRefExample", Field: "NestedRefExample", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "whereFail", Field: "WhereFail", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *DummyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan DummyModel
+ var state DummyModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.DurationMs.IsNull() != state.DurationMs.IsNull() || plan.DurationMs.IsUnknown() != state.DurationMs.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.DurationMs.IsNull() && !plan.DurationMs.IsUnknown() && !state.DurationMs.IsNull() && !state.DurationMs.IsUnknown() {
+ if plan.DurationMs.ValueInt64() != state.DurationMs.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.FailAtStart.IsNull() != state.FailAtStart.IsNull() || plan.FailAtStart.IsUnknown() != state.FailAtStart.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.FailAtStart.IsNull() && !plan.FailAtStart.IsUnknown() && !state.FailAtStart.IsNull() && !state.FailAtStart.IsUnknown() {
+ if plan.FailAtStart.ValueBool() != state.FailAtStart.ValueBool() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.FailInProgress.IsNull() != state.FailInProgress.IsNull() || plan.FailInProgress.IsUnknown() != state.FailInProgress.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.FailInProgress.IsNull() && !plan.FailInProgress.IsUnknown() && !state.FailInProgress.IsNull() && !state.FailInProgress.IsUnknown() {
+ if plan.FailInProgress.ValueBool() != state.FailInProgress.ValueBool() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.WhereFail.IsNull() != state.WhereFail.IsNull() || plan.WhereFail.IsUnknown() != state.WhereFail.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.WhereFail.IsNull() && !plan.WhereFail.IsUnknown() && !state.WhereFail.IsNull() && !state.WhereFail.IsUnknown() {
+ if plan.WhereFail.ValueInt64() != state.WhereFail.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.Bodymessage.IsNull() != state.Bodymessage.IsNull() || plan.Bodymessage.IsUnknown() != state.Bodymessage.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.Bodymessage.IsNull() && !plan.Bodymessage.IsUnknown() && !state.Bodymessage.IsNull() && !state.Bodymessage.IsUnknown() {
+ if !strings.EqualFold(plan.Bodymessage.ValueString(), state.Bodymessage.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.JsonExample.IsNull() != state.JsonExample.IsNull() || plan.JsonExample.IsUnknown() != state.JsonExample.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.JsonExample.IsNull() && !plan.JsonExample.IsUnknown() && !state.JsonExample.IsNull() && !state.JsonExample.IsUnknown() {
+ if !strings.EqualFold(plan.JsonExample.ValueString(), state.JsonExample.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // Проверяем изменился ли git_revision.
+ // Если да — вызываем redeploy вместо modify.
+ // git_revision = "" (не задано) → обычный modify.
+ redeployRequested := false
+ if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() {
+ if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() {
+ redeployRequested = true
+ }
+ }
+
+ if !hasServiceParamChanges && !redeployRequested {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ if redeployRequested {
+ // Вызываем redeploy — пустой params (операция не требует CFS-параметров)
+ if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ } else {
+ params := map[int]string{
+ 287: resources_core.FormatInt64(plan.DurationMs),
+ 288: resources_core.FormatBool(plan.FailAtStart),
+ 289: resources_core.FormatBool(plan.FailInProgress),
+ 290: resources_core.FormatInt64(plan.WhereFail),
+ 291: resources_core.FormatString(plan.Bodymessage),
+ 373: resources_core.BuildJSON(map[string]string{"secret": fmt.Sprintf("\"%s\"", plan.MapExample.Secret.ValueString())}),
+ 374: resources_core.FormatString(plan.JsonExample),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 1, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "arr", Field: "Arr", Type: "string"},
+ {Code: "arrayMapFixedExample", Field: "ArrayMapFixedExample", Type: "array-map-fixed"},
+ {Code: "bodymessage", Field: "Bodymessage", Type: "string"},
+ {Code: "durationMs", Field: "DurationMs", Type: "int64"},
+ {Code: "failAtStart", Field: "FailAtStart", Type: "bool"},
+ {Code: "failInProgress", Field: "FailInProgress", Type: "bool"},
+ {Code: "jsonExample", Field: "JsonExample", Type: "string"},
+ {Code: "mapExample", Field: "MapExample", Type: "string"},
+ {Code: "mapFixed", Field: "MapFixed", Type: "map-fixed"},
+ {Code: "nestedRefExample", Field: "NestedRefExample", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "whereFail", Field: "WhereFail", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *DummyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state DummyModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *DummyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *DummyResource) 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
+}
diff --git a/generated/dev/go/20_vc_org_saas_resource.go b/generated/dev/go/20_vc_org_saas_resource.go
new file mode 100644
index 0000000..83cdd6e
--- /dev/null
+++ b/generated/dev/go/20_vc_org_saas_resource.go
@@ -0,0 +1,348 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_org_saas
+// Service ID: 20
+
+var _ resource.Resource = &VcOrgSaasResource{}
+var _ resource.ResourceWithModifyPlan = &VcOrgSaasResource{}
+var _ resource.ResourceWithImportState = &VcOrgSaasResource{}
+
+type VcOrgSaasResource struct {
+ client *core.UniversalClient
+}
+
+type VcOrgSaasModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVcOrgSaasResource() resource.Resource {
+ return &VcOrgSaasResource{}
+}
+
+func (r *VcOrgSaasResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_org_saas"
+}
+
+func (r *VcOrgSaasResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("sandbox.nubes.ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcOrgSaasResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VcOrgSaasModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VcOrgSaasModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VcOrgSaasModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 185: resources_core.FormatString(config.ResourceRealm),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 20, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VcOrgSaasResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VcOrgSaasModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 20, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 185: resources_core.FormatString(data.ResourceRealm),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 20, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 20, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcOrgSaasResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcOrgSaasModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 20, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VcOrgSaasResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcOrgSaasModel
+ var state VcOrgSaasModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 20, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcOrgSaasResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcOrgSaasModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VcOrgSaasResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VcOrgSaasResource) 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
+}
diff --git a/generated/dev/go/21_vc_vdc_resource.go b/generated/dev/go/21_vc_vdc_resource.go
new file mode 100644
index 0000000..21d0d3d
--- /dev/null
+++ b/generated/dev/go/21_vc_vdc_resource.go
@@ -0,0 +1,573 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_vdc
+// Service ID: 21
+
+var _ resource.Resource = &VcVdcResource{}
+var _ resource.ResourceWithModifyPlan = &VcVdcResource{}
+var _ resource.ResourceWithImportState = &VcVdcResource{}
+
+type VcVdcResource struct {
+ client *core.UniversalClient
+}
+
+type VcVdcModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ CpuAllocated types.Int64 `tfsdk:"cpu_allocated"`
+ CpuGuaranteed types.Int64 `tfsdk:"cpu_guaranteed"`
+ MemAllocated types.Int64 `tfsdk:"mem_allocated"`
+ NetworkProvider types.String `tfsdk:"network_provider"`
+ OrganizationUid types.String `tfsdk:"organization_uid"`
+ ProviderVdc types.String `tfsdk:"provider_vdc"`
+ StorageConfig types.String `tfsdk:"storage_config"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVcVdcResource() resource.Resource {
+ return &VcVdcResource{}
+}
+
+func (r *VcVdcResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_vdc"
+}
+
+func (r *VcVdcResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "cpu_allocated": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(80), MarkdownDescription: "шт"},
+ "cpu_guaranteed": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(20), MarkdownDescription: "% резервирования Не изменяется после создания."},
+ "mem_allocated": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(200), MarkdownDescription: "gb"},
+ "network_provider": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания."},
+ "organization_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Для создания VDC предварительно необходимо создать организацию через услугу `Организация в Cloud Director` Не изменяется после создания."},
+ "provider_vdc": schema.StringAttribute{Required: true, MarkdownDescription: "Приписка `2.8` - Скорость vCPU - 2800MHz
И так далее...
Не изменяется после создания."},
+ "storage_config": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Первая политика в списке указывается как "По умолчанию""},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcVdcResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VcVdcModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VcVdcModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VcVdcModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.CpuGuaranteed.IsNull() && !plan.CpuGuaranteed.IsUnknown() && !state.CpuGuaranteed.IsNull() && !state.CpuGuaranteed.IsUnknown() {
+ if plan.CpuGuaranteed.ValueInt64() != state.CpuGuaranteed.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить cpu_guaranteed", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.NetworkProvider.IsNull() && !plan.NetworkProvider.IsUnknown() && !state.NetworkProvider.IsNull() && !state.NetworkProvider.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.NetworkProvider.ValueString(), state.NetworkProvider.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить network_provider", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.OrganizationUid.IsNull() && !plan.OrganizationUid.IsUnknown() && !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.OrganizationUid.ValueString(), state.OrganizationUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить organization_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ProviderVdc.IsNull() && !plan.ProviderVdc.IsUnknown() && !state.ProviderVdc.IsNull() && !state.ProviderVdc.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ProviderVdc.ValueString(), state.ProviderVdc.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить provider_vdc", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.NetworkProvider.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "network_provider is required.")
+ return
+ }
+ if config.NetworkProvider.IsUnknown() {
+ return
+ }
+ if config.OrganizationUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "organization_uid is required.")
+ return
+ }
+ if config.OrganizationUid.IsUnknown() {
+ return
+ }
+ if config.ProviderVdc.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "provider_vdc is required.")
+ return
+ }
+ if config.ProviderVdc.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.OrganizationUid.IsNull() && !config.OrganizationUid.IsUnknown() {
+ resolvedOrganizationUid, err := r.client.ResolveRefSvcParamValue(ctx, 19, config.OrganizationUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve organization_uid", err.Error())
+ } else if resolvedOrganizationUid != "" && resolvedOrganizationUid != config.OrganizationUid.ValueString() {
+ config.OrganizationUid = types.StringValue(resolvedOrganizationUid)
+ }
+ }
+ params := map[int]string{
+ 30: resources_core.FormatString(config.OrganizationUid),
+ 335: resources_core.FormatString(config.ProviderVdc),
+ 361: resources_core.FormatString(config.StorageConfig),
+ 397: resources_core.FormatInt64(config.CpuGuaranteed),
+ 557: resources_core.FormatInt64(config.CpuAllocated),
+ 558: resources_core.FormatInt64(config.MemAllocated),
+ 746: resources_core.FormatString(config.NetworkProvider),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 21, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VcVdcResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VcVdcModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.NetworkProvider.IsNull() || data.NetworkProvider.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "network_provider is required.")
+ return
+ }
+ if data.OrganizationUid.IsNull() || data.OrganizationUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "organization_uid is required.")
+ return
+ }
+ if data.ProviderVdc.IsNull() || data.ProviderVdc.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "provider_vdc is required.")
+ return
+ }
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalOrganizationUid := data.OrganizationUid
+ if !data.OrganizationUid.IsNull() && !data.OrganizationUid.IsUnknown() {
+ resolvedOrganizationUid, err := r.client.ResolveRefSvcParamValue(ctx, 19, data.OrganizationUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedOrganizationUid != "" && resolvedOrganizationUid != data.OrganizationUid.ValueString() {
+ data.OrganizationUid = types.StringValue(resolvedOrganizationUid)
+ }
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 21, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 30: resources_core.FormatString(data.OrganizationUid),
+ 335: resources_core.FormatString(data.ProviderVdc),
+ 361: resources_core.FormatString(data.StorageConfig),
+ 397: resources_core.FormatInt64(data.CpuGuaranteed),
+ 557: resources_core.FormatInt64(data.CpuAllocated),
+ 558: resources_core.FormatInt64(data.MemAllocated),
+ 746: resources_core.FormatString(data.NetworkProvider),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 21, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 21, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "cpuAllocated", Field: "CpuAllocated", Type: "int64"},
+ {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"},
+ {Code: "memAllocated", Field: "MemAllocated", Type: "int64"},
+ {Code: "networkProvider", Field: "NetworkProvider", Type: "string"},
+ {Code: "organizationUid", Field: "OrganizationUid", Type: "string"},
+ {Code: "providerVdc", Field: "ProviderVdc", Type: "string"},
+ {Code: "storageConfig", Field: "StorageConfig", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() {
+ resolvedOrganizationUid, err := r.client.ResolveRefSvcParamValue(ctx, 19, state.OrganizationUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve organization_uid", err.Error())
+ } else if resolvedOrganizationUid != "" && resolvedOrganizationUid != state.OrganizationUid.ValueString() {
+ state.OrganizationUid = types.StringValue(resolvedOrganizationUid)
+ }
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+ if !originalOrganizationUid.IsNull() && !originalOrganizationUid.IsUnknown() && !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() {
+ if strings.EqualFold(state.OrganizationUid.ValueString(), originalOrganizationUid.ValueString()) {
+ state.OrganizationUid = originalOrganizationUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcVdcResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcVdcModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 21, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "cpuAllocated", Field: "CpuAllocated", Type: "int64"},
+ {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"},
+ {Code: "memAllocated", Field: "MemAllocated", Type: "int64"},
+ {Code: "networkProvider", Field: "NetworkProvider", Type: "string"},
+ {Code: "organizationUid", Field: "OrganizationUid", Type: "string"},
+ {Code: "providerVdc", Field: "ProviderVdc", Type: "string"},
+ {Code: "storageConfig", Field: "StorageConfig", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !newState.OrganizationUid.IsNull() && !newState.OrganizationUid.IsUnknown() {
+ resolvedOrganizationUid, err := r.client.ResolveRefSvcParamValue(ctx, 19, newState.OrganizationUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve organization_uid", err.Error())
+ } else if resolvedOrganizationUid != "" && resolvedOrganizationUid != newState.OrganizationUid.ValueString() {
+ newState.OrganizationUid = types.StringValue(resolvedOrganizationUid)
+ }
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+ if !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() && !newState.OrganizationUid.IsNull() && !newState.OrganizationUid.IsUnknown() {
+ if strings.EqualFold(newState.OrganizationUid.ValueString(), state.OrganizationUid.ValueString()) {
+ newState.OrganizationUid = state.OrganizationUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VcVdcResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcVdcModel
+ var state VcVdcModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.CpuAllocated.IsNull() != state.CpuAllocated.IsNull() || plan.CpuAllocated.IsUnknown() != state.CpuAllocated.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.CpuAllocated.IsNull() && !plan.CpuAllocated.IsUnknown() && !state.CpuAllocated.IsNull() && !state.CpuAllocated.IsUnknown() {
+ if plan.CpuAllocated.ValueInt64() != state.CpuAllocated.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.MemAllocated.IsNull() != state.MemAllocated.IsNull() || plan.MemAllocated.IsUnknown() != state.MemAllocated.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.MemAllocated.IsNull() && !plan.MemAllocated.IsUnknown() && !state.MemAllocated.IsNull() && !state.MemAllocated.IsUnknown() {
+ if plan.MemAllocated.ValueInt64() != state.MemAllocated.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.StorageConfig.IsNull() != state.StorageConfig.IsNull() || plan.StorageConfig.IsUnknown() != state.StorageConfig.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.StorageConfig.IsNull() && !plan.StorageConfig.IsUnknown() && !state.StorageConfig.IsNull() && !state.StorageConfig.IsUnknown() {
+ if !strings.EqualFold(plan.StorageConfig.ValueString(), state.StorageConfig.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 560: resources_core.FormatInt64(plan.CpuAllocated),
+ 561: resources_core.FormatInt64(plan.MemAllocated),
+ 562: resources_core.FormatString(plan.StorageConfig),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 21, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "cpuAllocated", Field: "CpuAllocated", Type: "int64"},
+ {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"},
+ {Code: "memAllocated", Field: "MemAllocated", Type: "int64"},
+ {Code: "networkProvider", Field: "NetworkProvider", Type: "string"},
+ {Code: "organizationUid", Field: "OrganizationUid", Type: "string"},
+ {Code: "providerVdc", Field: "ProviderVdc", Type: "string"},
+ {Code: "storageConfig", Field: "StorageConfig", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() {
+ resolvedOrganizationUid, err := r.client.ResolveRefSvcParamValue(ctx, 19, state.OrganizationUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve organization_uid", err.Error())
+ } else if resolvedOrganizationUid != "" && resolvedOrganizationUid != state.OrganizationUid.ValueString() {
+ state.OrganizationUid = types.StringValue(resolvedOrganizationUid)
+ }
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+ if !plan.OrganizationUid.IsNull() && !plan.OrganizationUid.IsUnknown() && !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() {
+ if strings.EqualFold(state.OrganizationUid.ValueString(), plan.OrganizationUid.ValueString()) {
+ state.OrganizationUid = plan.OrganizationUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcVdcResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcVdcModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VcVdcResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VcVdcResource) 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
+}
diff --git a/generated/dev/go/22_vc_nsxt_resource.go b/generated/dev/go/22_vc_nsxt_resource.go
new file mode 100644
index 0000000..58189cc
--- /dev/null
+++ b/generated/dev/go/22_vc_nsxt_resource.go
@@ -0,0 +1,614 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_nsxt
+// Service ID: 22
+
+var _ resource.Resource = &VcNsxtResource{}
+var _ resource.ResourceWithModifyPlan = &VcNsxtResource{}
+var _ resource.ResourceWithImportState = &VcNsxtResource{}
+
+type VcNsxtResource struct {
+ client *core.UniversalClient
+}
+
+type VcNsxtModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ IpSpaceName types.String `tfsdk:"ip_space_name"`
+ NeedEnableAVI types.Bool `tfsdk:"need_enable_a_v_i"`
+ QosProfile types.String `tfsdk:"qos_profile"`
+ VdcGroupUid types.String `tfsdk:"vdc_group_uid"`
+ VdcType types.String `tfsdk:"vdc_type"`
+ VdcUid types.String `tfsdk:"vdc_uid"`
+ VirtualServicesCount types.Int64 `tfsdk:"virtual_services_count"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVcNsxtResource() resource.Resource {
+ return &VcNsxtResource{}
+}
+
+func (r *VcNsxtResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_nsxt"
+}
+
+func (r *VcNsxtResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "ip_space_name": schema.StringAttribute{Optional: true, MarkdownDescription: "Имя ip Space для внешнего IP"},
+ "need_enable_a_v_i": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включение Load Balancer"},
+ "qos_profile": schema.StringAttribute{Optional: true, MarkdownDescription: "Параметр пока не работает
Должен возвращать поле из ресурсной платформы типа vc из .vcd.hardware.gatewayQoSProfiles
Как по идее должен работать.
Нужно заполнить или CFS vdcUid, или vdcGroupUid
Идеально конечно проверять `if (params.vdcType == "vdc" && params.vdcUid != "")` или `if (params.vdcType == "vdcGroup" && params.vdcGroupUid != "")`
Далее надо пойти по стейту vdc -> org -> resPlatform, взять gatewayQoSProfiles
Или пойти по стейту vdcgroup -> vdc -> org -> resPlatform, взять gatewayQoSProfiles
Регулярку могу написать (Виталя)"},
+ "vdc_group_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID Услуги `Группа виртуальных датацентров (groupvDC)` Не изменяется после создания."},
+ "vdc_type": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("vdc"), MarkdownDescription: "Тип родительской услуги Не изменяется после создания."},
+ "vdc_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID Услуги `Виртуальный датацентр (vDC)` Не изменяется после создания."},
+ "virtual_services_count": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Кол-во виртуальных сервисов, которые **резервируются** на AVI"},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcNsxtResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VcNsxtModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VcNsxtModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VcNsxtModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.VdcGroupUid.IsNull() && !plan.VdcGroupUid.IsUnknown() && !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VdcGroupUid.ValueString(), state.VdcGroupUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vdc_group_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VdcType.IsNull() && !plan.VdcType.IsUnknown() && !state.VdcType.IsNull() && !state.VdcType.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VdcType.ValueString(), state.VdcType.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vdc_type", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VdcUid.ValueString(), state.VdcUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vdc_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.VdcGroupUid.IsNull() && !config.VdcGroupUid.IsUnknown() {
+ resolvedVdcGroupUid, err := r.client.ResolveRefSvcParamValue(ctx, 29, config.VdcGroupUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_group_uid", err.Error())
+ } else if resolvedVdcGroupUid != "" && resolvedVdcGroupUid != config.VdcGroupUid.ValueString() {
+ config.VdcGroupUid = types.StringValue(resolvedVdcGroupUid)
+ }
+ }
+ if !config.VdcUid.IsNull() && !config.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, config.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != config.VdcUid.ValueString() {
+ config.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ params := map[int]string{
+ 8: resources_core.FormatString(config.VdcUid),
+ 340: resources_core.FormatBool(config.NeedEnableAVI),
+ 341: resources_core.FormatInt64(config.VirtualServicesCount),
+ 621: resources_core.FormatString(config.VdcType),
+ 622: resources_core.FormatString(config.VdcGroupUid),
+ 825: resources_core.FormatString(config.QosProfile),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 22, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VcNsxtResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VcNsxtModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalVdcGroupUid := data.VdcGroupUid
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalVdcUid := data.VdcUid
+ if !data.VdcGroupUid.IsNull() && !data.VdcGroupUid.IsUnknown() {
+ resolvedVdcGroupUid, err := r.client.ResolveRefSvcParamValue(ctx, 29, data.VdcGroupUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedVdcGroupUid != "" && resolvedVdcGroupUid != data.VdcGroupUid.ValueString() {
+ data.VdcGroupUid = types.StringValue(resolvedVdcGroupUid)
+ }
+ }
+ if !data.VdcUid.IsNull() && !data.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, data.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedVdcUid != "" && resolvedVdcUid != data.VdcUid.ValueString() {
+ data.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 22, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 8: resources_core.FormatString(data.VdcUid),
+ 340: resources_core.FormatBool(data.NeedEnableAVI),
+ 341: resources_core.FormatInt64(data.VirtualServicesCount),
+ 621: resources_core.FormatString(data.VdcType),
+ 622: resources_core.FormatString(data.VdcGroupUid),
+ 825: resources_core.FormatString(data.QosProfile),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 22, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 22, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"},
+ {Code: "qosProfile", Field: "QosProfile", Type: "string"},
+ {Code: "vdcGroupUid", Field: "VdcGroupUid", Type: "string"},
+ {Code: "vdcType", Field: "VdcType", Type: "string"},
+ {Code: "vdcUid", Field: "VdcUid", Type: "string"},
+ {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() {
+ resolvedVdcGroupUid, err := r.client.ResolveRefSvcParamValue(ctx, 29, state.VdcGroupUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_group_uid", err.Error())
+ } else if resolvedVdcGroupUid != "" && resolvedVdcGroupUid != state.VdcGroupUid.ValueString() {
+ state.VdcGroupUid = types.StringValue(resolvedVdcGroupUid)
+ }
+ }
+ if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() {
+ state.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+ if !originalVdcGroupUid.IsNull() && !originalVdcGroupUid.IsUnknown() && !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() {
+ if strings.EqualFold(state.VdcGroupUid.ValueString(), originalVdcGroupUid.ValueString()) {
+ state.VdcGroupUid = originalVdcGroupUid
+ }
+ }
+ if !originalVdcUid.IsNull() && !originalVdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ if strings.EqualFold(state.VdcUid.ValueString(), originalVdcUid.ValueString()) {
+ state.VdcUid = originalVdcUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcNsxtResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcNsxtModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 22, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"},
+ {Code: "qosProfile", Field: "QosProfile", Type: "string"},
+ {Code: "vdcGroupUid", Field: "VdcGroupUid", Type: "string"},
+ {Code: "vdcType", Field: "VdcType", Type: "string"},
+ {Code: "vdcUid", Field: "VdcUid", Type: "string"},
+ {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !newState.VdcGroupUid.IsNull() && !newState.VdcGroupUid.IsUnknown() {
+ resolvedVdcGroupUid, err := r.client.ResolveRefSvcParamValue(ctx, 29, newState.VdcGroupUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_group_uid", err.Error())
+ } else if resolvedVdcGroupUid != "" && resolvedVdcGroupUid != newState.VdcGroupUid.ValueString() {
+ newState.VdcGroupUid = types.StringValue(resolvedVdcGroupUid)
+ }
+ }
+ if !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, newState.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != newState.VdcUid.ValueString() {
+ newState.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+ if !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() && !newState.VdcGroupUid.IsNull() && !newState.VdcGroupUid.IsUnknown() {
+ if strings.EqualFold(newState.VdcGroupUid.ValueString(), state.VdcGroupUid.ValueString()) {
+ newState.VdcGroupUid = state.VdcGroupUid
+ }
+ }
+ if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() && !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() {
+ if strings.EqualFold(newState.VdcUid.ValueString(), state.VdcUid.ValueString()) {
+ newState.VdcUid = state.VdcUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VcNsxtResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcNsxtModel
+ var state VcNsxtModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.NeedEnableAVI.IsNull() != state.NeedEnableAVI.IsNull() || plan.NeedEnableAVI.IsUnknown() != state.NeedEnableAVI.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.NeedEnableAVI.IsNull() && !plan.NeedEnableAVI.IsUnknown() && !state.NeedEnableAVI.IsNull() && !state.NeedEnableAVI.IsUnknown() {
+ if plan.NeedEnableAVI.ValueBool() != state.NeedEnableAVI.ValueBool() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.VirtualServicesCount.IsNull() != state.VirtualServicesCount.IsNull() || plan.VirtualServicesCount.IsUnknown() != state.VirtualServicesCount.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.VirtualServicesCount.IsNull() && !plan.VirtualServicesCount.IsUnknown() && !state.VirtualServicesCount.IsNull() && !state.VirtualServicesCount.IsUnknown() {
+ if plan.VirtualServicesCount.ValueInt64() != state.VirtualServicesCount.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.IpSpaceName.IsNull() != state.IpSpaceName.IsNull() || plan.IpSpaceName.IsUnknown() != state.IpSpaceName.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.IpSpaceName.IsNull() && !plan.IpSpaceName.IsUnknown() && !state.IpSpaceName.IsNull() && !state.IpSpaceName.IsUnknown() {
+ if !strings.EqualFold(plan.IpSpaceName.ValueString(), state.IpSpaceName.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.QosProfile.IsNull() != state.QosProfile.IsNull() || plan.QosProfile.IsUnknown() != state.QosProfile.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.QosProfile.IsNull() && !plan.QosProfile.IsUnknown() && !state.QosProfile.IsNull() && !state.QosProfile.IsUnknown() {
+ if !strings.EqualFold(plan.QosProfile.ValueString(), state.QosProfile.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 368: resources_core.FormatBool(plan.NeedEnableAVI),
+ 369: resources_core.FormatInt64(plan.VirtualServicesCount),
+ 372: resources_core.FormatString(plan.IpSpaceName),
+ 856: resources_core.FormatString(plan.QosProfile),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 22, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"},
+ {Code: "qosProfile", Field: "QosProfile", Type: "string"},
+ {Code: "vdcGroupUid", Field: "VdcGroupUid", Type: "string"},
+ {Code: "vdcType", Field: "VdcType", Type: "string"},
+ {Code: "vdcUid", Field: "VdcUid", Type: "string"},
+ {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() {
+ resolvedVdcGroupUid, err := r.client.ResolveRefSvcParamValue(ctx, 29, state.VdcGroupUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_group_uid", err.Error())
+ } else if resolvedVdcGroupUid != "" && resolvedVdcGroupUid != state.VdcGroupUid.ValueString() {
+ state.VdcGroupUid = types.StringValue(resolvedVdcGroupUid)
+ }
+ }
+ if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() {
+ state.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+ if !plan.VdcGroupUid.IsNull() && !plan.VdcGroupUid.IsUnknown() && !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() {
+ if strings.EqualFold(state.VdcGroupUid.ValueString(), plan.VdcGroupUid.ValueString()) {
+ state.VdcGroupUid = plan.VdcGroupUid
+ }
+ }
+ if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ if strings.EqualFold(state.VdcUid.ValueString(), plan.VdcUid.ValueString()) {
+ state.VdcUid = plan.VdcUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcNsxtResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcNsxtModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VcNsxtResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VcNsxtResource) 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
+}
diff --git a/generated/dev/go/23_vc_vm_resource.go b/generated/dev/go/23_vc_vm_resource.go
new file mode 100644
index 0000000..d39e2e1
--- /dev/null
+++ b/generated/dev/go/23_vc_vm_resource.go
@@ -0,0 +1,581 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_vm
+// Service ID: 23
+
+var _ resource.Resource = &VcVmResource{}
+var _ resource.ResourceWithModifyPlan = &VcVmResource{}
+var _ resource.ResourceWithImportState = &VcVmResource{}
+
+type VcVmResource struct {
+ client *core.UniversalClient
+}
+
+type VcVmModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AccessIpList types.String `tfsdk:"access_ip_list"`
+ IpSpaceName types.String `tfsdk:"ip_space_name"`
+ TemplateName types.String `tfsdk:"template_name"`
+ VappUid types.String `tfsdk:"vapp_uid"`
+ VmCpu types.String `tfsdk:"vm_cpu"`
+ VmDisksize types.String `tfsdk:"vm_disksize"`
+ VmName types.String `tfsdk:"vm_name"`
+ VmRam types.String `tfsdk:"vm_ram"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVcVmResource() resource.Resource {
+ return &VcVmResource{}
+}
+
+func (r *VcVmResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_vm"
+}
+
+func (r *VcVmResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_ip_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("79.111.119.78/32 185.247.187.1/32"), MarkdownDescription: "Вставить содержимое из 'curl ifconfig.io'
IP адреса в CIDR формате через пробел для настройки аксес листов"},
+ "ip_space_name": schema.StringAttribute{Required: true, MarkdownDescription: "Брать из свойств ресурсной платформы
.vcd.ipSpace
Имя IP-space для выделения внешнего IP (IP выделяется обязательно) Не изменяется после создания."},
+ "template_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("RockyLinux_9-16G"), MarkdownDescription: "ОС Не изменяется после создания."},
+ "vapp_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "InstanceUid vApp. vApp должен быть создан Не изменяется после создания."},
+ "vm_cpu": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("2"), MarkdownDescription: "CPU"},
+ "vm_disksize": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("10"), MarkdownDescription: "Размер диска под сервис (GB)"},
+ "vm_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("vm0001"), MarkdownDescription: "Имя ВМ. Не поддерживает спец-символы Не изменяется после создания."},
+ "vm_ram": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("2"), MarkdownDescription: "RAM (GB)"},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcVmResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VcVmModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VcVmModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VcVmModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.IpSpaceName.IsNull() && !plan.IpSpaceName.IsUnknown() && !state.IpSpaceName.IsNull() && !state.IpSpaceName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.IpSpaceName.ValueString(), state.IpSpaceName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить ip_space_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.TemplateName.IsNull() && !plan.TemplateName.IsUnknown() && !state.TemplateName.IsNull() && !state.TemplateName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.TemplateName.ValueString(), state.TemplateName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить template_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VappUid.IsNull() && !plan.VappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VappUid.ValueString(), state.VappUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vapp_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VmName.IsNull() && !plan.VmName.IsUnknown() && !state.VmName.IsNull() && !state.VmName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VmName.ValueString(), state.VmName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vm_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.IpSpaceName.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.")
+ return
+ }
+ if config.IpSpaceName.IsUnknown() {
+ return
+ }
+ if config.VappUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "vapp_uid is required.")
+ return
+ }
+ if config.VappUid.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.VappUid.IsNull() && !config.VappUid.IsUnknown() {
+ resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, config.VappUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error())
+ } else if resolvedVappUid != "" && resolvedVappUid != config.VappUid.ValueString() {
+ config.VappUid = types.StringValue(resolvedVappUid)
+ }
+ }
+ params := map[int]string{
+ 33: resources_core.FormatString(config.VappUid),
+ 35: resources_core.FormatString(config.VmName),
+ 36: resources_core.FormatString(config.VmCpu),
+ 37: resources_core.FormatString(config.VmRam),
+ 38: resources_core.FormatString(config.VmDisksize),
+ 176: resources_core.FormatString(config.TemplateName),
+ 177: resources_core.FormatString(config.AccessIpList),
+ 406: resources_core.FormatString(config.IpSpaceName),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 23, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VcVmResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VcVmModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.IpSpaceName.IsNull() || data.IpSpaceName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.")
+ return
+ }
+ if data.VappUid.IsNull() || data.VappUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "vapp_uid is required.")
+ return
+ }
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalVappUid := data.VappUid
+ if !data.VappUid.IsNull() && !data.VappUid.IsUnknown() {
+ resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, data.VappUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedVappUid != "" && resolvedVappUid != data.VappUid.ValueString() {
+ data.VappUid = types.StringValue(resolvedVappUid)
+ }
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 23, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 33: resources_core.FormatString(data.VappUid),
+ 35: resources_core.FormatString(data.VmName),
+ 36: resources_core.FormatString(data.VmCpu),
+ 37: resources_core.FormatString(data.VmRam),
+ 38: resources_core.FormatString(data.VmDisksize),
+ 176: resources_core.FormatString(data.TemplateName),
+ 177: resources_core.FormatString(data.AccessIpList),
+ 406: resources_core.FormatString(data.IpSpaceName),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 23, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 23, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessIpList", Field: "AccessIpList", Type: "string"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "templateName", Field: "TemplateName", Type: "string"},
+ {Code: "vappUid", Field: "VappUid", Type: "string"},
+ {Code: "vmCpu", Field: "VmCpu", Type: "string"},
+ {Code: "vmDisksize", Field: "VmDisksize", Type: "string"},
+ {Code: "vmName", Field: "VmName", Type: "string"},
+ {Code: "vmRam", Field: "VmRam", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() {
+ resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, state.VappUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error())
+ } else if resolvedVappUid != "" && resolvedVappUid != state.VappUid.ValueString() {
+ state.VappUid = types.StringValue(resolvedVappUid)
+ }
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+ if !originalVappUid.IsNull() && !originalVappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() {
+ if strings.EqualFold(state.VappUid.ValueString(), originalVappUid.ValueString()) {
+ state.VappUid = originalVappUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcVmResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcVmModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 23, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessIpList", Field: "AccessIpList", Type: "string"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "templateName", Field: "TemplateName", Type: "string"},
+ {Code: "vappUid", Field: "VappUid", Type: "string"},
+ {Code: "vmCpu", Field: "VmCpu", Type: "string"},
+ {Code: "vmDisksize", Field: "VmDisksize", Type: "string"},
+ {Code: "vmName", Field: "VmName", Type: "string"},
+ {Code: "vmRam", Field: "VmRam", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !newState.VappUid.IsNull() && !newState.VappUid.IsUnknown() {
+ resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, newState.VappUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error())
+ } else if resolvedVappUid != "" && resolvedVappUid != newState.VappUid.ValueString() {
+ newState.VappUid = types.StringValue(resolvedVappUid)
+ }
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+ if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() && !newState.VappUid.IsNull() && !newState.VappUid.IsUnknown() {
+ if strings.EqualFold(newState.VappUid.ValueString(), state.VappUid.ValueString()) {
+ newState.VappUid = state.VappUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VcVmResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcVmModel
+ var state VcVmModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.VmCpu.IsNull() != state.VmCpu.IsNull() || plan.VmCpu.IsUnknown() != state.VmCpu.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.VmCpu.IsNull() && !plan.VmCpu.IsUnknown() && !state.VmCpu.IsNull() && !state.VmCpu.IsUnknown() {
+ if !strings.EqualFold(plan.VmCpu.ValueString(), state.VmCpu.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.VmRam.IsNull() != state.VmRam.IsNull() || plan.VmRam.IsUnknown() != state.VmRam.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.VmRam.IsNull() && !plan.VmRam.IsUnknown() && !state.VmRam.IsNull() && !state.VmRam.IsUnknown() {
+ if !strings.EqualFold(plan.VmRam.ValueString(), state.VmRam.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.VmDisksize.IsNull() != state.VmDisksize.IsNull() || plan.VmDisksize.IsUnknown() != state.VmDisksize.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.VmDisksize.IsNull() && !plan.VmDisksize.IsUnknown() && !state.VmDisksize.IsNull() && !state.VmDisksize.IsUnknown() {
+ if !strings.EqualFold(plan.VmDisksize.ValueString(), state.VmDisksize.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.AccessIpList.IsNull() != state.AccessIpList.IsNull() || plan.AccessIpList.IsUnknown() != state.AccessIpList.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.AccessIpList.IsNull() && !plan.AccessIpList.IsUnknown() && !state.AccessIpList.IsNull() && !state.AccessIpList.IsUnknown() {
+ if !strings.EqualFold(plan.AccessIpList.ValueString(), state.AccessIpList.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 186: resources_core.FormatString(plan.VmCpu),
+ 187: resources_core.FormatString(plan.VmRam),
+ 188: resources_core.FormatString(plan.VmDisksize),
+ 189: resources_core.FormatString(plan.AccessIpList),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 23, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessIpList", Field: "AccessIpList", Type: "string"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "templateName", Field: "TemplateName", Type: "string"},
+ {Code: "vappUid", Field: "VappUid", Type: "string"},
+ {Code: "vmCpu", Field: "VmCpu", Type: "string"},
+ {Code: "vmDisksize", Field: "VmDisksize", Type: "string"},
+ {Code: "vmName", Field: "VmName", Type: "string"},
+ {Code: "vmRam", Field: "VmRam", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() {
+ resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, state.VappUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error())
+ } else if resolvedVappUid != "" && resolvedVappUid != state.VappUid.ValueString() {
+ state.VappUid = types.StringValue(resolvedVappUid)
+ }
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+ if !plan.VappUid.IsNull() && !plan.VappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() {
+ if strings.EqualFold(state.VappUid.ValueString(), plan.VappUid.ValueString()) {
+ state.VappUid = plan.VappUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcVmResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcVmModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VcVmResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VcVmResource) 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
+}
diff --git a/generated/dev/go/24_vc_nat_resource.go b/generated/dev/go/24_vc_nat_resource.go
new file mode 100644
index 0000000..20fe4bd
--- /dev/null
+++ b/generated/dev/go/24_vc_nat_resource.go
@@ -0,0 +1,322 @@
+package resources_gen
+
+import (
+ "context"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_nat
+// Service ID: 24
+
+var _ resource.Resource = &VcNatResource{}
+var _ resource.ResourceWithModifyPlan = &VcNatResource{}
+var _ resource.ResourceWithImportState = &VcNatResource{}
+
+type VcNatResource struct {
+ client *core.UniversalClient
+}
+
+type VcNatModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVcNatResource() resource.Resource {
+ return &VcNatResource{}
+}
+
+func (r *VcNatResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_nat"
+}
+
+func (r *VcNatResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcNatResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VcNatModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VcNatModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VcNatModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{}
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 24, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VcNatResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VcNatModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 24, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{}
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 24, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 24, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{})
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcNatResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcNatModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 24, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{})
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VcNatResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcNatModel
+ var state VcNatModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 24, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{})
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcNatResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcNatModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VcNatResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VcNatResource) 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
+}
diff --git a/generated/dev/go/25_vcexternalip_resource.go b/generated/dev/go/25_vcexternalip_resource.go
new file mode 100644
index 0000000..c375621
--- /dev/null
+++ b/generated/dev/go/25_vcexternalip_resource.go
@@ -0,0 +1,604 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vcexternalip
+// Service ID: 25
+
+var _ resource.Resource = &VcexternalipResource{}
+var _ resource.ResourceWithModifyPlan = &VcexternalipResource{}
+var _ resource.ResourceWithImportState = &VcexternalipResource{}
+
+type VcexternalipResource struct {
+ client *core.UniversalClient
+}
+
+type VcexternalipModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ DnatCreate types.Bool `tfsdk:"dnat_create"`
+ FromServiceCloudEdgeName types.String `tfsdk:"from_service_cloud_edge_name"`
+ FromServiceCloudEdgeScope types.String `tfsdk:"from_service_cloud_edge_scope"`
+ FromServiceCloudOrgName types.String `tfsdk:"from_service_cloud_org_name"`
+ FromServiceCloudVdcName types.String `tfsdk:"from_service_cloud_vdc_name"`
+ FromServiceCloudVmwareUrl types.String `tfsdk:"from_service_cloud_vmware_url"`
+ FromServiceNamespace types.String `tfsdk:"from_service_namespace"`
+ FromServiceVdcGroupName types.String `tfsdk:"from_service_vdc_group_name"`
+ InternalAddrAccess types.String `tfsdk:"internal_addr_access"`
+ InternalPortAccess types.String `tfsdk:"internal_port_access"`
+ IpSpaceName types.String `tfsdk:"ip_space_name"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ ServiceUid types.String `tfsdk:"service_uid"`
+ SnatCreate types.Bool `tfsdk:"snat_create"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVcexternalipResource() resource.Resource {
+ return &VcexternalipResource{}
+}
+
+func (r *VcexternalipResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vcexternalip"
+}
+
+func (r *VcexternalipResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "dnat_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Необходимо создать DNAT-правило."},
+ "from_service_cloud_edge_name": schema.StringAttribute{Optional: true, MarkdownDescription: "Edge составной услуги (указывать, если resourceRealm связан с CloudDirector) Не изменяется после создания."},
+ "from_service_cloud_edge_scope": schema.StringAttribute{Optional: true, MarkdownDescription: "Не изменяется после создания."},
+ "from_service_cloud_org_name": schema.StringAttribute{Optional: true, MarkdownDescription: "Org составной услуги (указывать, если resourceRealm связан с CloudDirector) Не изменяется после создания."},
+ "from_service_cloud_vdc_name": schema.StringAttribute{Optional: true, MarkdownDescription: "Vdc составной услуги (указывать, если resourceRealm связан с CloudDirector) Не изменяется после создания."},
+ "from_service_cloud_vmware_url": schema.StringAttribute{Optional: true, MarkdownDescription: "Api-Url составной услуги (указывать, если resourceRealm связан с CloudDirector) Не изменяется после создания."},
+ "from_service_namespace": schema.StringAttribute{Required: true, MarkdownDescription: "Namespace составной услуги (для отображения правил FW) Не изменяется после создания."},
+ "from_service_vdc_group_name": schema.StringAttribute{Optional: true, MarkdownDescription: "Не изменяется после создания."},
+ "internal_addr_access": schema.StringAttribute{Required: true, MarkdownDescription: "Внутренний адрес для связки ната. Требует dnatCreate=true или snatCreate=true"},
+ "internal_port_access": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("5432"), MarkdownDescription: "Порт для связки ната с адресом. Несколько значений можно передавать через пробел. Если передать all, то dnat будет на все порты. Требует dnatCreate=true
"},
+ "ip_space_name": schema.StringAttribute{Required: true, MarkdownDescription: "Тип ip Space (internet-ipv4-v1 / internet-antiddos-v1 / internet-no-antiddos-v1)"},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("sandbox.nubes.ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ "service_uid": schema.StringAttribute{Required: true},
+ "snat_create": schema.BoolAttribute{Optional: true, MarkdownDescription: "Необходимо создать SNAT-правило.
"},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcexternalipResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VcexternalipModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VcexternalipModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VcexternalipModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.FromServiceCloudEdgeName.IsNull() && !plan.FromServiceCloudEdgeName.IsUnknown() && !state.FromServiceCloudEdgeName.IsNull() && !state.FromServiceCloudEdgeName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.FromServiceCloudEdgeName.ValueString(), state.FromServiceCloudEdgeName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить from_service_cloud_edge_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.FromServiceCloudEdgeScope.IsNull() && !plan.FromServiceCloudEdgeScope.IsUnknown() && !state.FromServiceCloudEdgeScope.IsNull() && !state.FromServiceCloudEdgeScope.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.FromServiceCloudEdgeScope.ValueString(), state.FromServiceCloudEdgeScope.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить from_service_cloud_edge_scope", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.FromServiceCloudOrgName.IsNull() && !plan.FromServiceCloudOrgName.IsUnknown() && !state.FromServiceCloudOrgName.IsNull() && !state.FromServiceCloudOrgName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.FromServiceCloudOrgName.ValueString(), state.FromServiceCloudOrgName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить from_service_cloud_org_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.FromServiceCloudVdcName.IsNull() && !plan.FromServiceCloudVdcName.IsUnknown() && !state.FromServiceCloudVdcName.IsNull() && !state.FromServiceCloudVdcName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.FromServiceCloudVdcName.ValueString(), state.FromServiceCloudVdcName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить from_service_cloud_vdc_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.FromServiceCloudVmwareUrl.IsNull() && !plan.FromServiceCloudVmwareUrl.IsUnknown() && !state.FromServiceCloudVmwareUrl.IsNull() && !state.FromServiceCloudVmwareUrl.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.FromServiceCloudVmwareUrl.ValueString(), state.FromServiceCloudVmwareUrl.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить from_service_cloud_vmware_url", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.FromServiceNamespace.IsNull() && !plan.FromServiceNamespace.IsUnknown() && !state.FromServiceNamespace.IsNull() && !state.FromServiceNamespace.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.FromServiceNamespace.ValueString(), state.FromServiceNamespace.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить from_service_namespace", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.FromServiceVdcGroupName.IsNull() && !plan.FromServiceVdcGroupName.IsUnknown() && !state.FromServiceVdcGroupName.IsNull() && !state.FromServiceVdcGroupName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.FromServiceVdcGroupName.ValueString(), state.FromServiceVdcGroupName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить from_service_vdc_group_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.FromServiceNamespace.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "from_service_namespace is required.")
+ return
+ }
+ if config.FromServiceNamespace.IsUnknown() {
+ return
+ }
+ if config.InternalAddrAccess.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "internal_addr_access is required.")
+ return
+ }
+ if config.InternalAddrAccess.IsUnknown() {
+ return
+ }
+ if config.IpSpaceName.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.")
+ return
+ }
+ if config.IpSpaceName.IsUnknown() {
+ return
+ }
+ if config.ServiceUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "service_uid is required.")
+ return
+ }
+ if config.ServiceUid.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 140: resources_core.FormatString(config.ServiceUid),
+ 141: resources_core.FormatBool(config.DnatCreate),
+ 142: resources_core.FormatString(config.InternalPortAccess),
+ 143: resources_core.FormatBool(config.SnatCreate),
+ 144: resources_core.FormatString(config.InternalAddrAccess),
+ 324: resources_core.FormatString(config.FromServiceNamespace),
+ 325: resources_core.FormatString(config.FromServiceCloudEdgeName),
+ 326: resources_core.FormatString(config.FromServiceCloudVdcName),
+ 327: resources_core.FormatString(config.FromServiceCloudOrgName),
+ 328: resources_core.FormatString(config.FromServiceCloudVmwareUrl),
+ 336: resources_core.FormatString(config.IpSpaceName),
+ 488: resources_core.FormatString(config.ResourceRealm),
+ 624: resources_core.FormatString(config.FromServiceCloudEdgeScope),
+ 626: resources_core.FormatString(config.FromServiceVdcGroupName),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 25, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VcexternalipResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VcexternalipModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.FromServiceNamespace.IsNull() || data.FromServiceNamespace.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "from_service_namespace is required.")
+ return
+ }
+ if data.InternalAddrAccess.IsNull() || data.InternalAddrAccess.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "internal_addr_access is required.")
+ return
+ }
+ if data.IpSpaceName.IsNull() || data.IpSpaceName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.")
+ return
+ }
+ if data.ServiceUid.IsNull() || data.ServiceUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "service_uid is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 25, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 140: resources_core.FormatString(data.ServiceUid),
+ 141: resources_core.FormatBool(data.DnatCreate),
+ 142: resources_core.FormatString(data.InternalPortAccess),
+ 143: resources_core.FormatBool(data.SnatCreate),
+ 144: resources_core.FormatString(data.InternalAddrAccess),
+ 324: resources_core.FormatString(data.FromServiceNamespace),
+ 325: resources_core.FormatString(data.FromServiceCloudEdgeName),
+ 326: resources_core.FormatString(data.FromServiceCloudVdcName),
+ 327: resources_core.FormatString(data.FromServiceCloudOrgName),
+ 328: resources_core.FormatString(data.FromServiceCloudVmwareUrl),
+ 336: resources_core.FormatString(data.IpSpaceName),
+ 488: resources_core.FormatString(data.ResourceRealm),
+ 624: resources_core.FormatString(data.FromServiceCloudEdgeScope),
+ 626: resources_core.FormatString(data.FromServiceVdcGroupName),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 25, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 25, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "dnatCreate", Field: "DnatCreate", Type: "bool"},
+ {Code: "fromServiceCloudEdgeName", Field: "FromServiceCloudEdgeName", Type: "string"},
+ {Code: "fromServiceCloudEdgeScope", Field: "FromServiceCloudEdgeScope", Type: "string"},
+ {Code: "fromServiceCloudOrgName", Field: "FromServiceCloudOrgName", Type: "string"},
+ {Code: "fromServiceCloudVdcName", Field: "FromServiceCloudVdcName", Type: "string"},
+ {Code: "fromServiceCloudVmwareUrl", Field: "FromServiceCloudVmwareUrl", Type: "string"},
+ {Code: "fromServiceNamespace", Field: "FromServiceNamespace", Type: "string"},
+ {Code: "fromServiceVdcGroupName", Field: "FromServiceVdcGroupName", Type: "string"},
+ {Code: "internalAddrAccess", Field: "InternalAddrAccess", Type: "string"},
+ {Code: "internalPortAccess", Field: "InternalPortAccess", Type: "string"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "serviceUid", Field: "ServiceUid", Type: "string"},
+ {Code: "snatCreate", Field: "SnatCreate", Type: "bool"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcexternalipResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcexternalipModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 25, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "dnatCreate", Field: "DnatCreate", Type: "bool"},
+ {Code: "fromServiceCloudEdgeName", Field: "FromServiceCloudEdgeName", Type: "string"},
+ {Code: "fromServiceCloudEdgeScope", Field: "FromServiceCloudEdgeScope", Type: "string"},
+ {Code: "fromServiceCloudOrgName", Field: "FromServiceCloudOrgName", Type: "string"},
+ {Code: "fromServiceCloudVdcName", Field: "FromServiceCloudVdcName", Type: "string"},
+ {Code: "fromServiceCloudVmwareUrl", Field: "FromServiceCloudVmwareUrl", Type: "string"},
+ {Code: "fromServiceNamespace", Field: "FromServiceNamespace", Type: "string"},
+ {Code: "fromServiceVdcGroupName", Field: "FromServiceVdcGroupName", Type: "string"},
+ {Code: "internalAddrAccess", Field: "InternalAddrAccess", Type: "string"},
+ {Code: "internalPortAccess", Field: "InternalPortAccess", Type: "string"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "serviceUid", Field: "ServiceUid", Type: "string"},
+ {Code: "snatCreate", Field: "SnatCreate", Type: "bool"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VcexternalipResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcexternalipModel
+ var state VcexternalipModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.ServiceUid.IsNull() != state.ServiceUid.IsNull() || plan.ServiceUid.IsUnknown() != state.ServiceUid.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ServiceUid.IsNull() && !plan.ServiceUid.IsUnknown() && !state.ServiceUid.IsNull() && !state.ServiceUid.IsUnknown() {
+ if !strings.EqualFold(plan.ServiceUid.ValueString(), state.ServiceUid.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.IpSpaceName.IsNull() != state.IpSpaceName.IsNull() || plan.IpSpaceName.IsUnknown() != state.IpSpaceName.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.IpSpaceName.IsNull() && !plan.IpSpaceName.IsUnknown() && !state.IpSpaceName.IsNull() && !state.IpSpaceName.IsUnknown() {
+ if !strings.EqualFold(plan.IpSpaceName.ValueString(), state.IpSpaceName.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.DnatCreate.IsNull() != state.DnatCreate.IsNull() || plan.DnatCreate.IsUnknown() != state.DnatCreate.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.DnatCreate.IsNull() && !plan.DnatCreate.IsUnknown() && !state.DnatCreate.IsNull() && !state.DnatCreate.IsUnknown() {
+ if plan.DnatCreate.ValueBool() != state.DnatCreate.ValueBool() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.SnatCreate.IsNull() != state.SnatCreate.IsNull() || plan.SnatCreate.IsUnknown() != state.SnatCreate.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.SnatCreate.IsNull() && !plan.SnatCreate.IsUnknown() && !state.SnatCreate.IsNull() && !state.SnatCreate.IsUnknown() {
+ if plan.SnatCreate.ValueBool() != state.SnatCreate.ValueBool() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.InternalPortAccess.IsNull() != state.InternalPortAccess.IsNull() || plan.InternalPortAccess.IsUnknown() != state.InternalPortAccess.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.InternalPortAccess.IsNull() && !plan.InternalPortAccess.IsUnknown() && !state.InternalPortAccess.IsNull() && !state.InternalPortAccess.IsUnknown() {
+ if !strings.EqualFold(plan.InternalPortAccess.ValueString(), state.InternalPortAccess.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.InternalAddrAccess.IsNull() != state.InternalAddrAccess.IsNull() || plan.InternalAddrAccess.IsUnknown() != state.InternalAddrAccess.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.InternalAddrAccess.IsNull() && !plan.InternalAddrAccess.IsUnknown() && !state.InternalAddrAccess.IsNull() && !state.InternalAddrAccess.IsUnknown() {
+ if !strings.EqualFold(plan.InternalAddrAccess.ValueString(), state.InternalAddrAccess.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 627: resources_core.FormatString(plan.ServiceUid),
+ 628: resources_core.FormatString(plan.IpSpaceName),
+ 629: resources_core.FormatBool(plan.DnatCreate),
+ 630: resources_core.FormatBool(plan.SnatCreate),
+ 631: resources_core.FormatString(plan.InternalPortAccess),
+ 632: resources_core.FormatString(plan.InternalAddrAccess),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 25, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "dnatCreate", Field: "DnatCreate", Type: "bool"},
+ {Code: "fromServiceCloudEdgeName", Field: "FromServiceCloudEdgeName", Type: "string"},
+ {Code: "fromServiceCloudEdgeScope", Field: "FromServiceCloudEdgeScope", Type: "string"},
+ {Code: "fromServiceCloudOrgName", Field: "FromServiceCloudOrgName", Type: "string"},
+ {Code: "fromServiceCloudVdcName", Field: "FromServiceCloudVdcName", Type: "string"},
+ {Code: "fromServiceCloudVmwareUrl", Field: "FromServiceCloudVmwareUrl", Type: "string"},
+ {Code: "fromServiceNamespace", Field: "FromServiceNamespace", Type: "string"},
+ {Code: "fromServiceVdcGroupName", Field: "FromServiceVdcGroupName", Type: "string"},
+ {Code: "internalAddrAccess", Field: "InternalAddrAccess", Type: "string"},
+ {Code: "internalPortAccess", Field: "InternalPortAccess", Type: "string"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "serviceUid", Field: "ServiceUid", Type: "string"},
+ {Code: "snatCreate", Field: "SnatCreate", Type: "bool"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcexternalipResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcexternalipModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VcexternalipResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VcexternalipResource) 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
+}
diff --git a/generated/dev/go/26_vapp_resource.go b/generated/dev/go/26_vapp_resource.go
new file mode 100644
index 0000000..e210b7f
--- /dev/null
+++ b/generated/dev/go/26_vapp_resource.go
@@ -0,0 +1,571 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vapp
+// Service ID: 26
+
+var _ resource.Resource = &VappResource{}
+var _ resource.ResourceWithModifyPlan = &VappResource{}
+var _ resource.ResourceWithImportState = &VappResource{}
+
+type VappResource struct {
+ client *core.UniversalClient
+}
+
+type VappModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ NsxtUid types.String `tfsdk:"nsxt_uid"`
+ VappName types.String `tfsdk:"vapp_name"`
+ VdcUid types.String `tfsdk:"vdc_uid"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVappResource() resource.Resource {
+ return &VappResource{}
+}
+
+func (r *VappResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vapp"
+}
+
+func (r *VappResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "nsxt_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID Услуги `Сетевой шлюз периметра (Edge)` Не изменяется после создания."},
+ "vapp_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("9pv2jybu"), MarkdownDescription: "Уникальное в рамках организации Не изменяется после создания."},
+ "vdc_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID Услуги `Виртуальный датацентр (vDC)` Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VappResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VappModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VappModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VappModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.NsxtUid.IsNull() && !plan.NsxtUid.IsUnknown() && !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.NsxtUid.ValueString(), state.NsxtUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить nsxt_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VappName.IsNull() && !plan.VappName.IsUnknown() && !state.VappName.IsNull() && !state.VappName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VappName.ValueString(), state.VappName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vapp_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VdcUid.ValueString(), state.VdcUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vdc_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.NsxtUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "nsxt_uid is required.")
+ return
+ }
+ if config.NsxtUid.IsUnknown() {
+ return
+ }
+ if config.VdcUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "vdc_uid is required.")
+ return
+ }
+ if config.VdcUid.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.NsxtUid.IsNull() && !config.NsxtUid.IsUnknown() {
+ resolvedNsxtUid, err := r.client.ResolveRefSvcParamValue(ctx, 22, config.NsxtUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve nsxt_uid", err.Error())
+ } else if resolvedNsxtUid != "" && resolvedNsxtUid != config.NsxtUid.ValueString() {
+ config.NsxtUid = types.StringValue(resolvedNsxtUid)
+ }
+ }
+ if !config.VdcUid.IsNull() && !config.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, config.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != config.VdcUid.ValueString() {
+ config.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ params := map[int]string{
+ 190: resources_core.FormatString(config.NsxtUid),
+ 191: resources_core.FormatString(config.VappName),
+ 623: resources_core.FormatString(config.VdcUid),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 26, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VappResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VappModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.NsxtUid.IsNull() || data.NsxtUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "nsxt_uid is required.")
+ return
+ }
+ if data.VdcUid.IsNull() || data.VdcUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "vdc_uid is required.")
+ return
+ }
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalNsxtUid := data.NsxtUid
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalVdcUid := data.VdcUid
+ if !data.NsxtUid.IsNull() && !data.NsxtUid.IsUnknown() {
+ resolvedNsxtUid, err := r.client.ResolveRefSvcParamValue(ctx, 22, data.NsxtUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedNsxtUid != "" && resolvedNsxtUid != data.NsxtUid.ValueString() {
+ data.NsxtUid = types.StringValue(resolvedNsxtUid)
+ }
+ }
+ if !data.VdcUid.IsNull() && !data.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, data.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedVdcUid != "" && resolvedVdcUid != data.VdcUid.ValueString() {
+ data.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 26, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 190: resources_core.FormatString(data.NsxtUid),
+ 191: resources_core.FormatString(data.VappName),
+ 623: resources_core.FormatString(data.VdcUid),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 26, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 26, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "nsxtUid", Field: "NsxtUid", Type: "string"},
+ {Code: "vappName", Field: "VappName", Type: "string"},
+ {Code: "vdcUid", Field: "VdcUid", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() {
+ resolvedNsxtUid, err := r.client.ResolveRefSvcParamValue(ctx, 22, state.NsxtUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve nsxt_uid", err.Error())
+ } else if resolvedNsxtUid != "" && resolvedNsxtUid != state.NsxtUid.ValueString() {
+ state.NsxtUid = types.StringValue(resolvedNsxtUid)
+ }
+ }
+ if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() {
+ state.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+ if !originalNsxtUid.IsNull() && !originalNsxtUid.IsUnknown() && !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() {
+ if strings.EqualFold(state.NsxtUid.ValueString(), originalNsxtUid.ValueString()) {
+ state.NsxtUid = originalNsxtUid
+ }
+ }
+ if !originalVdcUid.IsNull() && !originalVdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ if strings.EqualFold(state.VdcUid.ValueString(), originalVdcUid.ValueString()) {
+ state.VdcUid = originalVdcUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VappResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VappModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 26, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "nsxtUid", Field: "NsxtUid", Type: "string"},
+ {Code: "vappName", Field: "VappName", Type: "string"},
+ {Code: "vdcUid", Field: "VdcUid", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !newState.NsxtUid.IsNull() && !newState.NsxtUid.IsUnknown() {
+ resolvedNsxtUid, err := r.client.ResolveRefSvcParamValue(ctx, 22, newState.NsxtUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve nsxt_uid", err.Error())
+ } else if resolvedNsxtUid != "" && resolvedNsxtUid != newState.NsxtUid.ValueString() {
+ newState.NsxtUid = types.StringValue(resolvedNsxtUid)
+ }
+ }
+ if !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, newState.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != newState.VdcUid.ValueString() {
+ newState.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+ if !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() && !newState.NsxtUid.IsNull() && !newState.NsxtUid.IsUnknown() {
+ if strings.EqualFold(newState.NsxtUid.ValueString(), state.NsxtUid.ValueString()) {
+ newState.NsxtUid = state.NsxtUid
+ }
+ }
+ if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() && !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() {
+ if strings.EqualFold(newState.VdcUid.ValueString(), state.VdcUid.ValueString()) {
+ newState.VdcUid = state.VdcUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VappResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VappModel
+ var state VappModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 26, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "nsxtUid", Field: "NsxtUid", Type: "string"},
+ {Code: "vappName", Field: "VappName", Type: "string"},
+ {Code: "vdcUid", Field: "VdcUid", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() {
+ resolvedNsxtUid, err := r.client.ResolveRefSvcParamValue(ctx, 22, state.NsxtUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve nsxt_uid", err.Error())
+ } else if resolvedNsxtUid != "" && resolvedNsxtUid != state.NsxtUid.ValueString() {
+ state.NsxtUid = types.StringValue(resolvedNsxtUid)
+ }
+ }
+ if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() {
+ state.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+ if !plan.NsxtUid.IsNull() && !plan.NsxtUid.IsUnknown() && !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() {
+ if strings.EqualFold(state.NsxtUid.ValueString(), plan.NsxtUid.ValueString()) {
+ state.NsxtUid = plan.NsxtUid
+ }
+ }
+ if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ if strings.EqualFold(state.VdcUid.ValueString(), plan.VdcUid.ValueString()) {
+ state.VdcUid = plan.VdcUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VappResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VappModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VappResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VappResource) 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
+}
diff --git a/generated/dev/go/27_vc_vm_v2_resource.go b/generated/dev/go/27_vc_vm_v2_resource.go
new file mode 100644
index 0000000..6a05554
--- /dev/null
+++ b/generated/dev/go/27_vc_vm_v2_resource.go
@@ -0,0 +1,350 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_vm_v2
+// Service ID: 27
+
+var _ resource.Resource = &VcVmV2Resource{}
+var _ resource.ResourceWithModifyPlan = &VcVmV2Resource{}
+var _ resource.ResourceWithImportState = &VcVmV2Resource{}
+
+type VcVmV2Resource struct {
+ client *core.UniversalClient
+}
+
+type VcVmV2Model struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ ServerListJson types.String `tfsdk:"server_list_json"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVcVmV2Resource() resource.Resource {
+ return &VcVmV2Resource{}
+}
+
+func (r *VcVmV2Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_vm_v2"
+}
+
+func (r *VcVmV2Resource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "server_list_json": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Необходимо изменить vappUid и ipSpaceName
Пример конфигурации взять по ссылке ниже
Объект серверов вида [{}] (Map(Object))
Более подробнее смотреть в README.md (https://gitea.mgmt.nubes.ru/cloudServices/vcVmV2)", PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()}},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcVmV2Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VcVmV2Model
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VcVmV2Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VcVmV2Model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 301: resources_core.FormatString(config.ServerListJson),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 27, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VcVmV2Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VcVmV2Model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 27, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 301: resources_core.FormatString(data.ServerListJson),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 27, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 27, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "serverListJson", Field: "ServerListJson", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcVmV2Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcVmV2Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 27, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "serverListJson", Field: "ServerListJson", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VcVmV2Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcVmV2Model
+ var state VcVmV2Model
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.ServerListJson.IsNull() != state.ServerListJson.IsNull() || plan.ServerListJson.IsUnknown() != state.ServerListJson.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ServerListJson.IsNull() && !plan.ServerListJson.IsUnknown() && !state.ServerListJson.IsNull() && !state.ServerListJson.IsUnknown() {
+ if !strings.EqualFold(plan.ServerListJson.ValueString(), state.ServerListJson.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 302: resources_core.FormatString(plan.ServerListJson),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 27, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "serverListJson", Field: "ServerListJson", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcVmV2Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcVmV2Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VcVmV2Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VcVmV2Resource) 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
+}
diff --git a/generated/dev/go/28_vc_vm_v3_resource.go b/generated/dev/go/28_vc_vm_v3_resource.go
new file mode 100644
index 0000000..61fe97c
--- /dev/null
+++ b/generated/dev/go/28_vc_vm_v3_resource.go
@@ -0,0 +1,697 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_vm_v3
+// Service ID: 28
+
+var _ resource.Resource = &VcVmV3Resource{}
+var _ resource.ResourceWithModifyPlan = &VcVmV3Resource{}
+var _ resource.ResourceWithImportState = &VcVmV3Resource{}
+
+type VcVmV3Resource struct {
+ client *core.UniversalClient
+}
+
+type VcVmV3Model struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // git_revision: при изменении вызывает redeploy вместо modify.
+ // Опциональное поле — если не задано, modify работает как обычно.
+ GitRevision types.String `tfsdk:"git_revision"`
+ AccessIpList types.String `tfsdk:"access_ip_list"`
+ AccessPortList types.String `tfsdk:"access_port_list"`
+ CloudInit types.String `tfsdk:"cloud_init"`
+ ImageVm types.String `tfsdk:"image_vm"`
+ IpSpaceName types.String `tfsdk:"ip_space_name"`
+ UserLogin types.String `tfsdk:"user_login"`
+ UserPublicKey types.String `tfsdk:"user_public_key"`
+ VappUid types.String `tfsdk:"vapp_uid"`
+ VmCpu types.Int64 `tfsdk:"vm_cpu"`
+ VmDisk types.Int64 `tfsdk:"vm_disk"`
+ VmName types.String `tfsdk:"vm_name"`
+ VmRam types.Int64 `tfsdk:"vm_ram"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVcVmV3Resource() resource.Resource {
+ return &VcVmV3Resource{}
+}
+
+func (r *VcVmV3Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_vm_v3"
+}
+
+func (r *VcVmV3Resource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_ip_list": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Формат массива
Каким адресам будет предоставлен доступ до вм
Требует выделенного внешнего IP
Если ничего не передано, то выставлено ["0.0.0.0/0"]", PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()}},
+ "access_port_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[\"22\"]"), MarkdownDescription: "По каким портам открыть доступ к ВМ.
Порты 22, 10050 проставляются автоматически
Пока доступно:
22 - SSH,
80 - HTTP,
443 - HTTPS,
10050 - ZABBIX"},
+ "cloud_init": schema.StringAttribute{Optional: true, MarkdownDescription: "Можно передать создание дополнительных пользователей.
На текущий момент не реализовано выполнения в cloud-init, установки пакетов, выполнения команд и записи файлов. Не изменяется после создания."},
+ "image_vm": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания."},
+ "ip_space_name": schema.StringAttribute{Required: true},
+ "user_login": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("myuser"), MarkdownDescription: "Учётка SSH Не изменяется после создания."},
+ "user_public_key": schema.StringAttribute{Required: true, MarkdownDescription: "Публичная часть SSH-Ключа для пользователя Не изменяется после создания."},
+ "vapp_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UI display_name for service_id=26 (mapped to UUID in core) Не изменяется после создания."},
+ "vm_cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "шт"},
+ "vm_disk": schema.Int64Attribute{Optional: true, MarkdownDescription: "Основной диск у vm зависит от образа (см. документацию)
Если необходимо дополнительное место для vm, введите колличество Gb в эту форму"},
+ "vm_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("web01"), MarkdownDescription: "Не изменяется после создания."},
+ "vm_ram": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Gb"},
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // При изменении git_revision вызывается redeploy вместо modify.
+ // Если не задано — modify работает без изменений.
+ "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcVmV3Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VcVmV3Model
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VcVmV3Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VcVmV3Model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.CloudInit.IsNull() && !plan.CloudInit.IsUnknown() && !state.CloudInit.IsNull() && !state.CloudInit.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.CloudInit.ValueString(), state.CloudInit.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить cloud_init", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ImageVm.IsNull() && !plan.ImageVm.IsUnknown() && !state.ImageVm.IsNull() && !state.ImageVm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ImageVm.ValueString(), state.ImageVm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить image_vm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.UserLogin.IsNull() && !plan.UserLogin.IsUnknown() && !state.UserLogin.IsNull() && !state.UserLogin.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.UserLogin.ValueString(), state.UserLogin.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить user_login", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.UserPublicKey.IsNull() && !plan.UserPublicKey.IsUnknown() && !state.UserPublicKey.IsNull() && !state.UserPublicKey.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.UserPublicKey.ValueString(), state.UserPublicKey.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить user_public_key", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VappUid.IsNull() && !plan.VappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VappUid.ValueString(), state.VappUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vapp_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VmName.IsNull() && !plan.VmName.IsUnknown() && !state.VmName.IsNull() && !state.VmName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VmName.ValueString(), state.VmName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vm_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.ImageVm.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "image_vm is required.")
+ return
+ }
+ if config.ImageVm.IsUnknown() {
+ return
+ }
+ if config.IpSpaceName.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.")
+ return
+ }
+ if config.IpSpaceName.IsUnknown() {
+ return
+ }
+ if config.UserPublicKey.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "user_public_key is required.")
+ return
+ }
+ if config.UserPublicKey.IsUnknown() {
+ return
+ }
+ if config.VappUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "vapp_uid is required.")
+ return
+ }
+ if config.VappUid.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.VappUid.IsNull() && !config.VappUid.IsUnknown() {
+ resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, config.VappUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error())
+ } else if resolvedVappUid != "" && resolvedVappUid != config.VappUid.ValueString() {
+ config.VappUid = types.StringValue(resolvedVappUid)
+ }
+ }
+ params := map[int]string{
+ 407: resources_core.FormatString(config.VappUid),
+ 408: resources_core.FormatString(config.VmName),
+ 409: resources_core.FormatInt64(config.VmCpu),
+ 410: resources_core.FormatInt64(config.VmRam),
+ 411: resources_core.FormatInt64(config.VmDisk),
+ 412: resources_core.FormatString(config.IpSpaceName),
+ 413: resources_core.FormatString(config.AccessIpList),
+ 414: resources_core.FormatString(config.ImageVm),
+ 415: resources_core.FormatString(config.CloudInit),
+ 416: resources_core.FormatString(config.UserLogin),
+ 417: resources_core.FormatString(config.UserPublicKey),
+ 448: resources_core.FormatString(config.AccessPortList),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 28, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VcVmV3Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VcVmV3Model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.ImageVm.IsNull() || data.ImageVm.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "image_vm is required.")
+ return
+ }
+ if data.IpSpaceName.IsNull() || data.IpSpaceName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.")
+ return
+ }
+ if data.UserPublicKey.IsNull() || data.UserPublicKey.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "user_public_key is required.")
+ return
+ }
+ if data.VappUid.IsNull() || data.VappUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "vapp_uid is required.")
+ return
+ }
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalVappUid := data.VappUid
+ if !data.VappUid.IsNull() && !data.VappUid.IsUnknown() {
+ resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, data.VappUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedVappUid != "" && resolvedVappUid != data.VappUid.ValueString() {
+ data.VappUid = types.StringValue(resolvedVappUid)
+ }
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 28, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 407: resources_core.FormatString(data.VappUid),
+ 408: resources_core.FormatString(data.VmName),
+ 409: resources_core.FormatInt64(data.VmCpu),
+ 410: resources_core.FormatInt64(data.VmRam),
+ 411: resources_core.FormatInt64(data.VmDisk),
+ 412: resources_core.FormatString(data.IpSpaceName),
+ 413: resources_core.FormatString(data.AccessIpList),
+ 414: resources_core.FormatString(data.ImageVm),
+ 415: resources_core.FormatString(data.CloudInit),
+ 416: resources_core.FormatString(data.UserLogin),
+ 417: resources_core.FormatString(data.UserPublicKey),
+ 448: resources_core.FormatString(data.AccessPortList),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 28, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 28, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessIpList", Field: "AccessIpList", Type: "string"},
+ {Code: "accessPortList", Field: "AccessPortList", Type: "string"},
+ {Code: "cloudInit", Field: "CloudInit", Type: "string"},
+ {Code: "imageVm", Field: "ImageVm", Type: "string"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "userLogin", Field: "UserLogin", Type: "string"},
+ {Code: "userPublicKey", Field: "UserPublicKey", Type: "string"},
+ {Code: "vappUid", Field: "VappUid", Type: "string"},
+ {Code: "vmCpu", Field: "VmCpu", Type: "int64"},
+ {Code: "vmDisk", Field: "VmDisk", Type: "int64"},
+ {Code: "vmName", Field: "VmName", Type: "string"},
+ {Code: "vmRam", Field: "VmRam", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() {
+ resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, state.VappUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error())
+ } else if resolvedVappUid != "" && resolvedVappUid != state.VappUid.ValueString() {
+ state.VappUid = types.StringValue(resolvedVappUid)
+ }
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+ if !originalVappUid.IsNull() && !originalVappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() {
+ if strings.EqualFold(state.VappUid.ValueString(), originalVappUid.ValueString()) {
+ state.VappUid = originalVappUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcVmV3Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcVmV3Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 28, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessIpList", Field: "AccessIpList", Type: "string"},
+ {Code: "accessPortList", Field: "AccessPortList", Type: "string"},
+ {Code: "cloudInit", Field: "CloudInit", Type: "string"},
+ {Code: "imageVm", Field: "ImageVm", Type: "string"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "userLogin", Field: "UserLogin", Type: "string"},
+ {Code: "userPublicKey", Field: "UserPublicKey", Type: "string"},
+ {Code: "vappUid", Field: "VappUid", Type: "string"},
+ {Code: "vmCpu", Field: "VmCpu", Type: "int64"},
+ {Code: "vmDisk", Field: "VmDisk", Type: "int64"},
+ {Code: "vmName", Field: "VmName", Type: "string"},
+ {Code: "vmRam", Field: "VmRam", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !newState.VappUid.IsNull() && !newState.VappUid.IsUnknown() {
+ resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, newState.VappUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error())
+ } else if resolvedVappUid != "" && resolvedVappUid != newState.VappUid.ValueString() {
+ newState.VappUid = types.StringValue(resolvedVappUid)
+ }
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+ if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() && !newState.VappUid.IsNull() && !newState.VappUid.IsUnknown() {
+ if strings.EqualFold(newState.VappUid.ValueString(), state.VappUid.ValueString()) {
+ newState.VappUid = state.VappUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VcVmV3Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcVmV3Model
+ var state VcVmV3Model
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.VmCpu.IsNull() != state.VmCpu.IsNull() || plan.VmCpu.IsUnknown() != state.VmCpu.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.VmCpu.IsNull() && !plan.VmCpu.IsUnknown() && !state.VmCpu.IsNull() && !state.VmCpu.IsUnknown() {
+ if plan.VmCpu.ValueInt64() != state.VmCpu.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.VmRam.IsNull() != state.VmRam.IsNull() || plan.VmRam.IsUnknown() != state.VmRam.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.VmRam.IsNull() && !plan.VmRam.IsUnknown() && !state.VmRam.IsNull() && !state.VmRam.IsUnknown() {
+ if plan.VmRam.ValueInt64() != state.VmRam.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.VmDisk.IsNull() != state.VmDisk.IsNull() || plan.VmDisk.IsUnknown() != state.VmDisk.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.VmDisk.IsNull() && !plan.VmDisk.IsUnknown() && !state.VmDisk.IsNull() && !state.VmDisk.IsUnknown() {
+ if plan.VmDisk.ValueInt64() != state.VmDisk.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.IpSpaceName.IsNull() != state.IpSpaceName.IsNull() || plan.IpSpaceName.IsUnknown() != state.IpSpaceName.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.IpSpaceName.IsNull() && !plan.IpSpaceName.IsUnknown() && !state.IpSpaceName.IsNull() && !state.IpSpaceName.IsUnknown() {
+ if !strings.EqualFold(plan.IpSpaceName.ValueString(), state.IpSpaceName.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.AccessIpList.IsNull() != state.AccessIpList.IsNull() || plan.AccessIpList.IsUnknown() != state.AccessIpList.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.AccessIpList.IsNull() && !plan.AccessIpList.IsUnknown() && !state.AccessIpList.IsNull() && !state.AccessIpList.IsUnknown() {
+ if !strings.EqualFold(plan.AccessIpList.ValueString(), state.AccessIpList.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.AccessPortList.IsNull() != state.AccessPortList.IsNull() || plan.AccessPortList.IsUnknown() != state.AccessPortList.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.AccessPortList.IsNull() && !plan.AccessPortList.IsUnknown() && !state.AccessPortList.IsNull() && !state.AccessPortList.IsUnknown() {
+ if !strings.EqualFold(plan.AccessPortList.ValueString(), state.AccessPortList.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // Проверяем изменился ли git_revision.
+ // Если да — вызываем redeploy вместо modify.
+ // git_revision = "" (не задано) → обычный modify.
+ redeployRequested := false
+ if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() {
+ if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() {
+ redeployRequested = true
+ }
+ }
+
+ if !hasServiceParamChanges && !redeployRequested {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ if redeployRequested {
+ // Вызываем redeploy — пустой params (операция не требует CFS-параметров)
+ if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ } else {
+ params := map[int]string{
+ 493: resources_core.FormatInt64(plan.VmCpu),
+ 494: resources_core.FormatInt64(plan.VmRam),
+ 495: resources_core.FormatInt64(plan.VmDisk),
+ 496: resources_core.FormatString(plan.IpSpaceName),
+ 497: resources_core.FormatString(plan.AccessIpList),
+ 498: resources_core.FormatString(plan.AccessPortList),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 28, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessIpList", Field: "AccessIpList", Type: "string"},
+ {Code: "accessPortList", Field: "AccessPortList", Type: "string"},
+ {Code: "cloudInit", Field: "CloudInit", Type: "string"},
+ {Code: "imageVm", Field: "ImageVm", Type: "string"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "userLogin", Field: "UserLogin", Type: "string"},
+ {Code: "userPublicKey", Field: "UserPublicKey", Type: "string"},
+ {Code: "vappUid", Field: "VappUid", Type: "string"},
+ {Code: "vmCpu", Field: "VmCpu", Type: "int64"},
+ {Code: "vmDisk", Field: "VmDisk", Type: "int64"},
+ {Code: "vmName", Field: "VmName", Type: "string"},
+ {Code: "vmRam", Field: "VmRam", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() {
+ resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, state.VappUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error())
+ } else if resolvedVappUid != "" && resolvedVappUid != state.VappUid.ValueString() {
+ state.VappUid = types.StringValue(resolvedVappUid)
+ }
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+ if !plan.VappUid.IsNull() && !plan.VappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() {
+ if strings.EqualFold(state.VappUid.ValueString(), plan.VappUid.ValueString()) {
+ state.VappUid = plan.VappUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcVmV3Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcVmV3Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VcVmV3Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VcVmV3Resource) 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
+}
diff --git a/generated/dev/go/29_vc_vdc_group_resource.go b/generated/dev/go/29_vc_vdc_group_resource.go
new file mode 100644
index 0000000..41864ef
--- /dev/null
+++ b/generated/dev/go/29_vc_vdc_group_resource.go
@@ -0,0 +1,449 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_vdc_group
+// Service ID: 29
+
+var _ resource.Resource = &VcVdcGroupResource{}
+var _ resource.ResourceWithModifyPlan = &VcVdcGroupResource{}
+var _ resource.ResourceWithImportState = &VcVdcGroupResource{}
+
+type VcVdcGroupResource struct {
+ client *core.UniversalClient
+}
+
+type VcVdcGroupModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ VdcUid types.String `tfsdk:"vdc_uid"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVcVdcGroupResource() resource.Resource {
+ return &VcVdcGroupResource{}
+}
+
+func (r *VcVdcGroupResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_vdc_group"
+}
+
+func (r *VcVdcGroupResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "vdc_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UI display_name for service_id=21 (mapped to UUID in core) Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcVdcGroupResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VcVdcGroupModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VcVdcGroupModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VcVdcGroupModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VdcUid.ValueString(), state.VdcUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vdc_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.VdcUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "vdc_uid is required.")
+ return
+ }
+ if config.VdcUid.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.VdcUid.IsNull() && !config.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, config.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != config.VdcUid.ValueString() {
+ config.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ params := map[int]string{
+ 618: resources_core.FormatString(config.VdcUid),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 29, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VcVdcGroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VcVdcGroupModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.VdcUid.IsNull() || data.VdcUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "vdc_uid is required.")
+ return
+ }
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalVdcUid := data.VdcUid
+ if !data.VdcUid.IsNull() && !data.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, data.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedVdcUid != "" && resolvedVdcUid != data.VdcUid.ValueString() {
+ data.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 29, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 618: resources_core.FormatString(data.VdcUid),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 29, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 29, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "vdcUid", Field: "VdcUid", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() {
+ state.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+ if !originalVdcUid.IsNull() && !originalVdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ if strings.EqualFold(state.VdcUid.ValueString(), originalVdcUid.ValueString()) {
+ state.VdcUid = originalVdcUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcVdcGroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcVdcGroupModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 29, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "vdcUid", Field: "VdcUid", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, newState.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != newState.VdcUid.ValueString() {
+ newState.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+ if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() && !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() {
+ if strings.EqualFold(newState.VdcUid.ValueString(), state.VdcUid.ValueString()) {
+ newState.VdcUid = state.VdcUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VcVdcGroupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcVdcGroupModel
+ var state VcVdcGroupModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 29, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "vdcUid", Field: "VdcUid", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() {
+ state.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+ if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ if strings.EqualFold(state.VdcUid.ValueString(), plan.VdcUid.ValueString()) {
+ state.VdcUid = plan.VdcUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcVdcGroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcVdcGroupModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VcVdcGroupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VcVdcGroupResource) 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
+}
diff --git a/generated/dev/go/29_vc_vdc_group_vdc_resource.go b/generated/dev/go/29_vc_vdc_group_vdc_resource.go
new file mode 100644
index 0000000..3e3e8ef
--- /dev/null
+++ b/generated/dev/go/29_vc_vdc_group_vdc_resource.go
@@ -0,0 +1,326 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_vdc_group
+// Subresource: vdc
+
+var _ resource.Resource = &VcVdcGroupVdcResource{}
+
+// VcVdcGroupVdcResource manages a subresource via operations.
+type VcVdcGroupVdcResource struct {
+ client *core.UniversalClient
+}
+
+type VcVdcGroupVdcModel struct {
+ ID types.String `tfsdk:"id"`
+ VcVdcGroupID types.String `tfsdk:"vc_vdc_group_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"`
+}
+
+func NewVcVdcGroupVdcResource() resource.Resource {
+ return &VcVdcGroupVdcResource{}
+}
+
+func (r *VcVdcGroupVdcResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_vdc_group_vdc"
+}
+
+func (r *VcVdcGroupVdcResource) 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()}},
+ "vc_vdc_group_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."},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcVdcGroupVdcResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan VcVdcGroupVdcModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.VcVdcGroupID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{}
+ identityCodes := []string{}
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("vdc", 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, "vdc", 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{})
+
+ 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, "", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", 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, "vdc", 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, "vdc", 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, "vdc", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *VcVdcGroupVdcResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcVdcGroupVdcModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcVdcGroupVdcResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcVdcGroupVdcModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state VcVdcGroupVdcModel
+ 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.VcVdcGroupID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{}
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "vdc", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *VcVdcGroupVdcResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcVdcGroupVdcModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.VcVdcGroupID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{}
+ identityCodes := []string{}
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("vdc", 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{})
+
+ 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, "", 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 *VcVdcGroupVdcResource) 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
+}
diff --git a/generated/dev/go/2_template_resource.go b/generated/dev/go/2_template_resource.go
new file mode 100644
index 0000000..d3e71ae
--- /dev/null
+++ b/generated/dev/go/2_template_resource.go
@@ -0,0 +1,397 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: template
+// Service ID: 2
+
+var _ resource.Resource = &TemplateResource{}
+var _ resource.ResourceWithModifyPlan = &TemplateResource{}
+var _ resource.ResourceWithImportState = &TemplateResource{}
+
+type TemplateResource struct {
+ client *core.UniversalClient
+}
+
+// TemplateAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type TemplateAccessConfigurationModel struct {
+ SlaveAccessList types.String `tfsdk:"slave_access_list" json:"slaveAccessList"`
+ Type types.String `tfsdk:"type" json:"type"`
+ Domain types.String `tfsdk:"domain" json:"domain"`
+ MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"`
+ MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"`
+ SlaveIpSpace types.String `tfsdk:"slave_ip_space" json:"slaveIpSpace"`
+}
+
+// TemplateClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type TemplateClusterConfigurationModel struct {
+ Disk types.Int64 `tfsdk:"disk" json:"disk"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+}
+
+// TemplateStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type TemplateStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+}
+
+type TemplateModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AccessConfiguration *TemplateAccessConfigurationModel `tfsdk:"access_configuration"`
+ ClusterConfiguration *TemplateClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ StartupConfiguration *TemplateStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewTemplateResource() resource.Resource {
+ return &TemplateResource{}
+}
+
+func (r *TemplateResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_template"
+}
+
+func (r *TemplateResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "slave_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]")},
+ "type": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("ingress")},
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("abracadabra-mgonny3n")},
+ "master_ip_space": schema.StringAttribute{Required: true},
+ "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]")},
+ "slave_ip_space": schema.StringAttribute{Required: true},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(5)},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(128)},
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100)},
+ }},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Required: true},
+ }},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *TemplateResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *TemplateModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *TemplateModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan TemplateModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 813: resources_core.BuildJSON(map[string]string{"disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64())}),
+ 814: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}),
+ 815: resources_core.BuildJSON(map[string]string{"slaveAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveAccessList.ValueString()), "type": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Type.ValueString()), "domain": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Domain.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveIpSpace.ValueString())}),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 2, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *TemplateResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data TemplateModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 2, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 813: resources_core.BuildJSON(map[string]string{"disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64())}),
+ 814: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}),
+ 815: resources_core.BuildJSON(map[string]string{"slaveAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveAccessList.ValueString()), "type": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Type.ValueString()), "domain": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Domain.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveIpSpace.ValueString())}),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 2, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 2, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *TemplateResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state TemplateModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 2, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan TemplateModel
+ var state TemplateModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 816: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64())}),
+ 817: resources_core.BuildJSON(map[string]string{"masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveAccessList.ValueString()), "type": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.Type.ValueString()), "domain": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.Domain.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString())}),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 2, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *TemplateResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state TemplateModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *TemplateResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *TemplateResource) 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
+}
diff --git a/generated/dev/go/32_vmpostgre_resource.go b/generated/dev/go/32_vmpostgre_resource.go
new file mode 100644
index 0000000..5d39c90
--- /dev/null
+++ b/generated/dev/go/32_vmpostgre_resource.go
@@ -0,0 +1,514 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vmpostgre
+// Service ID: 32
+
+var _ resource.Resource = &VmpostgreResource{}
+var _ resource.ResourceWithModifyPlan = &VmpostgreResource{}
+var _ resource.ResourceWithImportState = &VmpostgreResource{}
+
+type VmpostgreResource struct {
+ client *core.UniversalClient
+}
+
+type VmpostgreModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ UserDb types.String `tfsdk:"user_db"`
+ UserLogin types.String `tfsdk:"user_login"`
+ UserPassword types.String `tfsdk:"user_password"`
+ VmUid types.String `tfsdk:"vm_uid"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVmpostgreResource() resource.Resource {
+ return &VmpostgreResource{}
+}
+
+func (r *VmpostgreResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vmpostgre"
+}
+
+func (r *VmpostgreResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("sandbox.nubes.ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ "user_db": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("servicedb"), MarkdownDescription: "Наименование Базы данных
Не изменяется после создания."},
+ "user_login": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("serviceuser"), MarkdownDescription: "Пользователь базы данных
Не изменяется после создания."},
+ "user_password": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("fj29gm48fm!d374mc9"), MarkdownDescription: "Пароль от доступа к Базе данных
Не изменяется после создания."},
+ "vm_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "InstanceUid ВМ. ВМ должна быть создана
Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VmpostgreResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VmpostgreModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VmpostgreModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VmpostgreModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.UserDb.IsNull() && !plan.UserDb.IsUnknown() && !state.UserDb.IsNull() && !state.UserDb.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.UserDb.ValueString(), state.UserDb.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить user_db", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.UserLogin.IsNull() && !plan.UserLogin.IsUnknown() && !state.UserLogin.IsNull() && !state.UserLogin.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.UserLogin.ValueString(), state.UserLogin.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить user_login", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.UserPassword.IsNull() && !plan.UserPassword.IsUnknown() && !state.UserPassword.IsNull() && !state.UserPassword.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.UserPassword.ValueString(), state.UserPassword.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить user_password", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VmUid.IsNull() && !plan.VmUid.IsUnknown() && !state.VmUid.IsNull() && !state.VmUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VmUid.ValueString(), state.VmUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vm_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.VmUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "vm_uid is required.")
+ return
+ }
+ if config.VmUid.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.VmUid.IsNull() && !config.VmUid.IsUnknown() {
+ resolvedVmUid, err := r.client.ResolveRefSvcParamValue(ctx, 23, config.VmUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vm_uid", err.Error())
+ } else if resolvedVmUid != "" && resolvedVmUid != config.VmUid.ValueString() {
+ config.VmUid = types.StringValue(resolvedVmUid)
+ }
+ }
+ params := map[int]string{
+ 45: resources_core.FormatString(config.VmUid),
+ 46: resources_core.FormatString(config.UserDb),
+ 47: resources_core.FormatString(config.UserLogin),
+ 48: resources_core.FormatString(config.UserPassword),
+ 278: resources_core.FormatString(config.ResourceRealm),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 32, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VmpostgreResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VmpostgreModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.VmUid.IsNull() || data.VmUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "vm_uid is required.")
+ return
+ }
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalVmUid := data.VmUid
+ if !data.VmUid.IsNull() && !data.VmUid.IsUnknown() {
+ resolvedVmUid, err := r.client.ResolveRefSvcParamValue(ctx, 23, data.VmUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedVmUid != "" && resolvedVmUid != data.VmUid.ValueString() {
+ data.VmUid = types.StringValue(resolvedVmUid)
+ }
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 32, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 45: resources_core.FormatString(data.VmUid),
+ 46: resources_core.FormatString(data.UserDb),
+ 47: resources_core.FormatString(data.UserLogin),
+ 48: resources_core.FormatString(data.UserPassword),
+ 278: resources_core.FormatString(data.ResourceRealm),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 32, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 32, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "userDb", Field: "UserDb", Type: "string"},
+ {Code: "userLogin", Field: "UserLogin", Type: "string"},
+ {Code: "userPassword", Field: "UserPassword", Type: "string"},
+ {Code: "vmUid", Field: "VmUid", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.VmUid.IsNull() && !state.VmUid.IsUnknown() {
+ resolvedVmUid, err := r.client.ResolveRefSvcParamValue(ctx, 23, state.VmUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vm_uid", err.Error())
+ } else if resolvedVmUid != "" && resolvedVmUid != state.VmUid.ValueString() {
+ state.VmUid = types.StringValue(resolvedVmUid)
+ }
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+ if !originalVmUid.IsNull() && !originalVmUid.IsUnknown() && !state.VmUid.IsNull() && !state.VmUid.IsUnknown() {
+ if strings.EqualFold(state.VmUid.ValueString(), originalVmUid.ValueString()) {
+ state.VmUid = originalVmUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VmpostgreResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VmpostgreModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 32, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "userDb", Field: "UserDb", Type: "string"},
+ {Code: "userLogin", Field: "UserLogin", Type: "string"},
+ {Code: "userPassword", Field: "UserPassword", Type: "string"},
+ {Code: "vmUid", Field: "VmUid", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !newState.VmUid.IsNull() && !newState.VmUid.IsUnknown() {
+ resolvedVmUid, err := r.client.ResolveRefSvcParamValue(ctx, 23, newState.VmUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vm_uid", err.Error())
+ } else if resolvedVmUid != "" && resolvedVmUid != newState.VmUid.ValueString() {
+ newState.VmUid = types.StringValue(resolvedVmUid)
+ }
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+ if !state.VmUid.IsNull() && !state.VmUid.IsUnknown() && !newState.VmUid.IsNull() && !newState.VmUid.IsUnknown() {
+ if strings.EqualFold(newState.VmUid.ValueString(), state.VmUid.ValueString()) {
+ newState.VmUid = state.VmUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VmpostgreResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VmpostgreModel
+ var state VmpostgreModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 32, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "userDb", Field: "UserDb", Type: "string"},
+ {Code: "userLogin", Field: "UserLogin", Type: "string"},
+ {Code: "userPassword", Field: "UserPassword", Type: "string"},
+ {Code: "vmUid", Field: "VmUid", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.VmUid.IsNull() && !state.VmUid.IsUnknown() {
+ resolvedVmUid, err := r.client.ResolveRefSvcParamValue(ctx, 23, state.VmUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vm_uid", err.Error())
+ } else if resolvedVmUid != "" && resolvedVmUid != state.VmUid.ValueString() {
+ state.VmUid = types.StringValue(resolvedVmUid)
+ }
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+ if !plan.VmUid.IsNull() && !plan.VmUid.IsUnknown() && !state.VmUid.IsNull() && !state.VmUid.IsUnknown() {
+ if strings.EqualFold(state.VmUid.ValueString(), plan.VmUid.ValueString()) {
+ state.VmUid = plan.VmUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VmpostgreResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VmpostgreModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VmpostgreResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VmpostgreResource) 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
+}
diff --git a/generated/dev/go/50_nextcloud_backup_resource.go b/generated/dev/go/50_nextcloud_backup_resource.go
new file mode 100644
index 0000000..feb1f0e
--- /dev/null
+++ b/generated/dev/go/50_nextcloud_backup_resource.go
@@ -0,0 +1,344 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: nextcloud
+// Subresource: backup
+
+var _ resource.Resource = &NextcloudBackupResource{}
+
+// NextcloudBackupResource manages a subresource via operations.
+type NextcloudBackupResource struct {
+ client *core.UniversalClient
+}
+
+type NextcloudBackupModel struct {
+ ID types.String `tfsdk:"id"`
+ NextcloudID types.String `tfsdk:"nextcloud_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"`
+ StartupConfiguration types.String `tfsdk:"startup_configuration"`
+}
+
+func NewNextcloudBackupResource() resource.Resource {
+ return &NextcloudBackupResource{}
+}
+
+func (r *NextcloudBackupResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_nextcloud_backup"
+}
+
+func (r *NextcloudBackupResource) 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()}},
+ "nextcloud_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."},
+ "startup_configuration": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *NextcloudBackupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan NextcloudBackupModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if plan.StartupConfiguration.IsNull() || plan.StartupConfiguration.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "startup_configuration is required.")
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.NextcloudID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "startupConfiguration": resources_core.FormatString(plan.StartupConfiguration),
+ }
+ identityCodes := []string{
+ "startupConfiguration",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("backup", 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, "backup", 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{
+ "startupConfiguration": resources_core.FormatString(plan.StartupConfiguration),
+ })
+
+ 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, "create_backup", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_backup", 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, "backup", 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, "backup", 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, "backup", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *NextcloudBackupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state NextcloudBackupModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *NextcloudBackupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan NextcloudBackupModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state NextcloudBackupModel
+ 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.NextcloudID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "startupConfiguration": resources_core.FormatString(plan.StartupConfiguration),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *NextcloudBackupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state NextcloudBackupModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.NextcloudID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "startupConfiguration": resources_core.FormatString(state.StartupConfiguration),
+ }
+ identityCodes := []string{
+ "startupConfiguration",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("backup", 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{})
+
+ 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, "", 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 *NextcloudBackupResource) 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
+}
diff --git a/generated/dev/go/50_nextcloud_resource.go b/generated/dev/go/50_nextcloud_resource.go
new file mode 100644
index 0000000..3e5c608
--- /dev/null
+++ b/generated/dev/go/50_nextcloud_resource.go
@@ -0,0 +1,414 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: nextcloud
+// Service ID: 50
+
+var _ resource.Resource = &NextcloudResource{}
+var _ resource.ResourceWithModifyPlan = &NextcloudResource{}
+var _ resource.ResourceWithImportState = &NextcloudResource{}
+
+type NextcloudResource struct {
+ client *core.UniversalClient
+}
+
+// NextcloudAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type NextcloudAccessConfigurationModel struct {
+ Domain types.String `tfsdk:"domain" json:"domain"`
+}
+
+// NextcloudAppConfigurationModel — вложенная модель для map-fixed параметра appConfiguration.
+type NextcloudAppConfigurationModel struct {
+ Version types.String `tfsdk:"version" json:"version"`
+ DocumentServer types.String `tfsdk:"document_server" json:"documentServer"`
+ LogLevel types.String `tfsdk:"log_level" json:"logLevel"`
+}
+
+// NextcloudClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type NextcloudClusterConfigurationModel struct {
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+ Disk types.Int64 `tfsdk:"disk" json:"disk"`
+}
+
+// NextcloudStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type NextcloudStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+ PsqlEnabled types.Bool `tfsdk:"psql_enabled" json:"psqlEnabled"`
+ RedisEnabled types.Bool `tfsdk:"redis_enabled" json:"redisEnabled"`
+ PsqlUid types.String `tfsdk:"psql_uid" json:"psqlUid"`
+ RedisUid types.String `tfsdk:"redis_uid" json:"redisUid"`
+}
+
+type NextcloudModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AccessConfiguration *NextcloudAccessConfigurationModel `tfsdk:"access_configuration"`
+ AppConfiguration *NextcloudAppConfigurationModel `tfsdk:"app_configuration"`
+ ClusterConfiguration *NextcloudClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ StartupConfiguration *NextcloudStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewNextcloudResource() resource.Resource {
+ return &NextcloudResource{}
+}
+
+func (r *NextcloudResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_nextcloud"
+}
+
+func (r *NextcloudResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("nextcloud-xcq3u70l"), MarkdownDescription: "Если указан FQDN в формате: `myapp.example.com`, предварительно будет проверяться DNS-запись (А-запись **должна смотреть на IP Ingress кластера ресурсной платформы**)
Если указан в формате `myapp`, DNS-запись будет автоматически создана в служебной DNS-зоне"},
+ }},
+ "app_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("33.0.4")},
+ "document_server": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("collabora"), MarkdownDescription: "Выбор сервера документов (onlyoffice)/(collabora). onlyoffice пока не доступен
Размер пода collabora:
1.8 ядра. 2Gi ОЗУ"},
+ "log_level": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("INFO"), MarkdownDescription: "Включает отображение логов самого nextcloud
Логи уходят в /dev/stdout контейнера
`NONE` отключает логирование"},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1000), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1024), MarkdownDescription: "Указывается в Megabytes"},
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Указывается в GIGAbytes
На данном диске хранятся данные Nextcloud"},
+ }},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"},
+ "psql_enabled": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Инстанс обязан быть развёрнут на той же ресурсной платформе, где и будет располагаться Nextcloud"},
+ "redis_enabled": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Инстанс обязан быть развёрнут на той же ресурсной платформе, где и будет располагаться Nextcloud"},
+ "psql_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Необходимо выбирать, когда выбран параметр `Подключить PostgreSQL из Личного кабинета`"},
+ "redis_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Необходимо выбирать, когда выбран параметр `Подключить Redis из Личного кабинета`"},
+ }},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *NextcloudResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *NextcloudModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *NextcloudModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan NextcloudModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 1055: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString()), "psqlEnabled": fmt.Sprintf("%v", config.StartupConfiguration.PsqlEnabled.ValueBool()), "redisEnabled": fmt.Sprintf("%v", config.StartupConfiguration.RedisEnabled.ValueBool()), "psqlUid": fmt.Sprintf("\"%s\"", config.StartupConfiguration.PsqlUid.ValueString()), "redisUid": fmt.Sprintf("\"%s\"", config.StartupConfiguration.RedisUid.ValueString())}),
+ 1056: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64())}),
+ 1057: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Domain.ValueString())}),
+ 1058: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.AppConfiguration.Version.ValueString()), "documentServer": fmt.Sprintf("\"%s\"", config.AppConfiguration.DocumentServer.ValueString()), "logLevel": fmt.Sprintf("\"%s\"", config.AppConfiguration.LogLevel.ValueString())}),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 50, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *NextcloudResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data NextcloudModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 50, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 1055: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString()), "psqlEnabled": fmt.Sprintf("%v", data.StartupConfiguration.PsqlEnabled.ValueBool()), "redisEnabled": fmt.Sprintf("%v", data.StartupConfiguration.RedisEnabled.ValueBool()), "psqlUid": fmt.Sprintf("\"%s\"", data.StartupConfiguration.PsqlUid.ValueString()), "redisUid": fmt.Sprintf("\"%s\"", data.StartupConfiguration.RedisUid.ValueString())}),
+ 1056: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64())}),
+ 1057: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Domain.ValueString())}),
+ 1058: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.AppConfiguration.Version.ValueString()), "documentServer": fmt.Sprintf("\"%s\"", data.AppConfiguration.DocumentServer.ValueString()), "logLevel": fmt.Sprintf("\"%s\"", data.AppConfiguration.LogLevel.ValueString())}),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 50, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 50, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *NextcloudResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state NextcloudModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 50, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *NextcloudResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan NextcloudModel
+ var state NextcloudModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 1059: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64())}),
+ 1060: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.AppConfiguration.Version.ValueString()), "documentServer": fmt.Sprintf("\"%s\"", plan.AppConfiguration.DocumentServer.ValueString()), "logLevel": fmt.Sprintf("\"%s\"", plan.AppConfiguration.LogLevel.ValueString())}),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 50, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *NextcloudResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state NextcloudModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *NextcloudResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *NextcloudResource) 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
+}
diff --git a/generated/dev/go/81_superset_resource.go b/generated/dev/go/81_superset_resource.go
new file mode 100644
index 0000000..94602e6
--- /dev/null
+++ b/generated/dev/go/81_superset_resource.go
@@ -0,0 +1,447 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: superset
+// Service ID: 81
+
+var _ resource.Resource = &SupersetResource{}
+var _ resource.ResourceWithModifyPlan = &SupersetResource{}
+var _ resource.ResourceWithImportState = &SupersetResource{}
+
+type SupersetResource struct {
+ client *core.UniversalClient
+}
+
+type SupersetModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ Domain types.String `tfsdk:"domain"`
+ Emails types.String `tfsdk:"emails"`
+ ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"`
+ ResourceDisk types.Int64 `tfsdk:"resource_disk"`
+ ResourceInstances types.Int64 `tfsdk:"resource_instances"`
+ ResourceMemory types.Int64 `tfsdk:"resource_memory"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewSupersetResource() resource.Resource {
+ return &SupersetResource{}
+}
+
+func (r *SupersetResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_superset"
+}
+
+func (r *SupersetResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("superset-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."},
+ "emails": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("testuser1"), MarkdownDescription: "Учётная запись, которой будут выданы админские права. В формате email [ну не совсем. Для доменных учетных записей - без доменного суффикса]. Обязана находиться в Keycloak. Можно передавать несколько через пробел Не изменяется после создания."},
+ "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Квота ядра пода (Указывается в mili). 1000 Mili = 1 ядро Не изменяется после создания."},
+ "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Квота диска (Указывается в GIGAbytes) Не изменяется после создания."},
+ "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов Не изменяется после создания."},
+ "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Квота памяти пода (Указывается в Megabytes) Не изменяется после создания."},
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *SupersetResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *SupersetModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *SupersetModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan SupersetModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.Emails.IsNull() && !plan.Emails.IsUnknown() && !state.Emails.IsNull() && !state.Emails.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Emails.ValueString(), state.Emails.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить emails", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() {
+ if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_c_p_u", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() {
+ if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_disk", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() {
+ if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_instances", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() {
+ if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_memory", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.ResourceRealm.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+ if config.ResourceRealm.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 14: resources_core.FormatString(config.Domain),
+ 20: resources_core.FormatString(config.Emails),
+ 96: resources_core.FormatInt64(config.ResourceCPU),
+ 97: resources_core.FormatInt64(config.ResourceMemory),
+ 98: resources_core.FormatInt64(config.ResourceDisk),
+ 109: resources_core.FormatString(config.ResourceRealm),
+ 227: resources_core.FormatInt64(config.ResourceInstances),
+ }
+ desiredDomain := ""
+ if !config.Domain.IsNull() && !config.Domain.IsUnknown() {
+ desiredDomain = config.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 81, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *SupersetResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data SupersetModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ if !data.Domain.IsNull() && !data.Domain.IsUnknown() {
+ desiredDomain = data.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 81, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 14: resources_core.FormatString(data.Domain),
+ 20: resources_core.FormatString(data.Emails),
+ 96: resources_core.FormatInt64(data.ResourceCPU),
+ 97: resources_core.FormatInt64(data.ResourceMemory),
+ 98: resources_core.FormatInt64(data.ResourceDisk),
+ 109: resources_core.FormatString(data.ResourceRealm),
+ 227: resources_core.FormatInt64(data.ResourceInstances),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 81, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 81, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "emails", Field: "Emails", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *SupersetResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state SupersetModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 81, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "emails", Field: "Emails", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *SupersetResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan SupersetModel
+ var state SupersetModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 81, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "emails", Field: "Emails", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *SupersetResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state SupersetModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *SupersetResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *SupersetResource) 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
+}
diff --git a/generated/dev/go/82_harbor_resource.go b/generated/dev/go/82_harbor_resource.go
new file mode 100644
index 0000000..b53b4c3
--- /dev/null
+++ b/generated/dev/go/82_harbor_resource.go
@@ -0,0 +1,482 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: harbor
+// Service ID: 82
+
+var _ resource.Resource = &HarborResource{}
+var _ resource.ResourceWithModifyPlan = &HarborResource{}
+var _ resource.ResourceWithImportState = &HarborResource{}
+
+type HarborResource struct {
+ client *core.UniversalClient
+}
+
+type HarborModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ Domain types.String `tfsdk:"domain"`
+ ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"`
+ ResourceInstances types.Int64 `tfsdk:"resource_instances"`
+ ResourceMemory types.Int64 `tfsdk:"resource_memory"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ S3Uid types.String `tfsdk:"s3_uid"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewHarborResource() resource.Resource {
+ return &HarborResource{}
+}
+
+func (r *HarborResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_harbor"
+}
+
+func (r *HarborResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("harbor-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."},
+ "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1000), MarkdownDescription: "Квота ядра пода (Указывается в mili). 1000Mili = 1 Ядро"},
+ "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов"},
+ "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1024), MarkdownDescription: "Квота памяти пода (Указывается в Megabytes)"},
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ "s3_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID экземпляра услуги S3 Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *HarborResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *HarborModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *HarborModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan HarborModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.S3Uid.IsNull() && !plan.S3Uid.IsUnknown() && !state.S3Uid.IsNull() && !state.S3Uid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.S3Uid.ValueString(), state.S3Uid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить s3_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.ResourceRealm.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+ if config.ResourceRealm.IsUnknown() {
+ return
+ }
+ if config.S3Uid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "s3_uid is required.")
+ return
+ }
+ if config.S3Uid.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.S3Uid.IsNull() && !config.S3Uid.IsUnknown() {
+ resolvedS3Uid, err := r.client.ResolveRefSvcParamValue(ctx, 12, config.S3Uid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve s3_uid", err.Error())
+ } else if resolvedS3Uid != "" && resolvedS3Uid != config.S3Uid.ValueString() {
+ config.S3Uid = types.StringValue(resolvedS3Uid)
+ }
+ }
+ params := map[int]string{
+ 16: resources_core.FormatString(config.Domain),
+ 110: resources_core.FormatString(config.ResourceRealm),
+ 228: resources_core.FormatInt64(config.ResourceCPU),
+ 229: resources_core.FormatInt64(config.ResourceMemory),
+ 230: resources_core.FormatInt64(config.ResourceInstances),
+ 231: resources_core.FormatString(config.S3Uid),
+ }
+ desiredDomain := ""
+ if !config.Domain.IsNull() && !config.Domain.IsUnknown() {
+ desiredDomain = config.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 82, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *HarborResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data HarborModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+ if data.S3Uid.IsNull() || data.S3Uid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "s3_uid is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ if !data.Domain.IsNull() && !data.Domain.IsUnknown() {
+ desiredDomain = data.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 82, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 16: resources_core.FormatString(data.Domain),
+ 110: resources_core.FormatString(data.ResourceRealm),
+ 228: resources_core.FormatInt64(data.ResourceCPU),
+ 229: resources_core.FormatInt64(data.ResourceMemory),
+ 230: resources_core.FormatInt64(data.ResourceInstances),
+ 231: resources_core.FormatString(data.S3Uid),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 82, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 82, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *HarborResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state HarborModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 82, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *HarborResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan HarborModel
+ var state HarborModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.ResourceInstances.IsNull() != state.ResourceInstances.IsNull() || plan.ResourceInstances.IsUnknown() != state.ResourceInstances.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() {
+ if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() {
+ if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() {
+ if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 822: resources_core.FormatInt64(plan.ResourceInstances),
+ 823: resources_core.FormatInt64(plan.ResourceCPU),
+ 824: resources_core.FormatInt64(plan.ResourceMemory),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 82, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *HarborResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state HarborModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *HarborResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *HarborResource) 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
+}
diff --git a/generated/dev/go/86_k8s_velero_resource.go b/generated/dev/go/86_k8s_velero_resource.go
new file mode 100644
index 0000000..1a180ac
--- /dev/null
+++ b/generated/dev/go/86_k8s_velero_resource.go
@@ -0,0 +1,401 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: k8s_velero
+// Service ID: 86
+
+var _ resource.Resource = &K8sVeleroResource{}
+var _ resource.ResourceWithModifyPlan = &K8sVeleroResource{}
+var _ resource.ResourceWithImportState = &K8sVeleroResource{}
+
+type K8sVeleroResource struct {
+ client *core.UniversalClient
+}
+
+// K8sVeleroBackupConfigurationModel — вложенная модель для map-fixed параметра backupConfiguration.
+type K8sVeleroBackupConfigurationModel struct {
+ S3BucketUid types.String `tfsdk:"s3_bucket_uid" json:"s3BucketUid"`
+}
+
+// K8sVeleroClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type K8sVeleroClusterConfigurationModel struct {
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+}
+
+// K8sVeleroNodeAgentConfigurationModel — вложенная модель для map-fixed параметра nodeAgentConfiguration.
+type K8sVeleroNodeAgentConfigurationModel struct {
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+}
+
+// K8sVeleroStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type K8sVeleroStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+}
+
+type K8sVeleroModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ BackupConfiguration *K8sVeleroBackupConfigurationModel `tfsdk:"backup_configuration"`
+ ClusterConfiguration *K8sVeleroClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ NodeAgentConfiguration *K8sVeleroNodeAgentConfigurationModel `tfsdk:"node_agent_configuration"`
+ StartupConfiguration *K8sVeleroStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewK8sVeleroResource() resource.Resource {
+ return &K8sVeleroResource{}
+}
+
+func (r *K8sVeleroResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_k8s_velero"
+}
+
+func (r *K8sVeleroResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "backup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "s3_bucket_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Экземпляр S3-Бакета обязателен
Учётная запись будет создана автоматически, если её нету"},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2048), MarkdownDescription: "Указывается в Megabytes"},
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1000), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ }},
+ "node_agent_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2000), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2048), MarkdownDescription: "Указывается в Megabytes"},
+ }},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Required: true},
+ }},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *K8sVeleroResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *K8sVeleroModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *K8sVeleroModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan K8sVeleroModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 962: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64())}),
+ 963: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}),
+ 964: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.NodeAgentConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.NodeAgentConfiguration.Memory.ValueInt64())}),
+ 1076: resources_core.BuildJSON(map[string]string{"s3BucketUid": fmt.Sprintf("\"%s\"", config.BackupConfiguration.S3BucketUid.ValueString())}),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 86, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *K8sVeleroResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data K8sVeleroModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 86, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 962: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64())}),
+ 963: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}),
+ 964: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.NodeAgentConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.NodeAgentConfiguration.Memory.ValueInt64())}),
+ 1076: resources_core.BuildJSON(map[string]string{"s3BucketUid": fmt.Sprintf("\"%s\"", data.BackupConfiguration.S3BucketUid.ValueString())}),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 86, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 86, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "nodeAgentConfiguration", Field: "NodeAgentConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8sVeleroResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state K8sVeleroModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 86, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "nodeAgentConfiguration", Field: "NodeAgentConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *K8sVeleroResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan K8sVeleroModel
+ var state K8sVeleroModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 965: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64())}),
+ 966: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.NodeAgentConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.NodeAgentConfiguration.Memory.ValueInt64())}),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 86, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "nodeAgentConfiguration", Field: "NodeAgentConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8sVeleroResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state K8sVeleroModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *K8sVeleroResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *K8sVeleroResource) 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
+}
diff --git a/generated/dev/go/87_k8svalkey_resource.go b/generated/dev/go/87_k8svalkey_resource.go
new file mode 100644
index 0000000..1ac11ca
--- /dev/null
+++ b/generated/dev/go/87_k8svalkey_resource.go
@@ -0,0 +1,423 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: k8svalkey
+// Service ID: 87
+
+var _ resource.Resource = &K8svalkeyResource{}
+var _ resource.ResourceWithModifyPlan = &K8svalkeyResource{}
+var _ resource.ResourceWithImportState = &K8svalkeyResource{}
+
+type K8svalkeyResource struct {
+ client *core.UniversalClient
+}
+
+// K8svalkeyAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type K8svalkeyAccessConfigurationModel struct {
+ MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"`
+ MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"`
+}
+
+// K8svalkeyClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type K8svalkeyClusterConfigurationModel struct {
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+ Shards types.Int64 `tfsdk:"shards" json:"shards"`
+ Disk types.Int64 `tfsdk:"disk" json:"disk"`
+}
+
+// K8svalkeyStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type K8svalkeyStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+}
+
+// K8svalkeyValkeyConfigurationModel — вложенная модель для map-fixed параметра valkeyConfiguration.
+type K8svalkeyValkeyConfigurationModel struct {
+ Version types.String `tfsdk:"version" json:"version"`
+}
+
+type K8svalkeyModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AccessConfiguration *K8svalkeyAccessConfigurationModel `tfsdk:"access_configuration"`
+ ClusterConfiguration *K8svalkeyClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ StartupConfiguration *K8svalkeyStartupConfigurationModel `tfsdk:"startup_configuration"`
+ ValkeyConf types.String `tfsdk:"valkey_conf"`
+ ValkeyConfiguration *K8svalkeyValkeyConfigurationModel `tfsdk:"valkey_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewK8svalkeyResource() resource.Resource {
+ return &K8svalkeyResource{}
+}
+
+func (r *K8svalkeyResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_k8svalkey"
+}
+
+func (r *K8svalkeyResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "master_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"},
+ "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Указывается в Megabytes"},
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ "shards": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Указывается в GIGAbytes"},
+ }},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"},
+ }},
+ "valkey_conf": schema.StringAttribute{Optional: true, MarkdownDescription: "Не изменяется после создания."},
+ "valkey_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("0.1.0")},
+ }},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *K8svalkeyResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *K8svalkeyModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *K8svalkeyModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan K8svalkeyModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.ValkeyConf.IsNull() && !plan.ValkeyConf.IsUnknown() && !state.ValkeyConf.IsNull() && !state.ValkeyConf.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ValkeyConf.ValueString(), state.ValkeyConf.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить valkey_conf", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 1066: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1067: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "shards": fmt.Sprintf("%d", config.ClusterConfiguration.Shards.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64())}),
+ 1068: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString())}),
+ 1071: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.ValkeyConfiguration.Version.ValueString())}),
+ 1072: resources_core.FormatString(config.ValkeyConf),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 87, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *K8svalkeyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data K8svalkeyModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 87, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 1066: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1067: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "shards": fmt.Sprintf("%d", data.ClusterConfiguration.Shards.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64())}),
+ 1068: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString())}),
+ 1071: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.ValkeyConfiguration.Version.ValueString())}),
+ 1072: resources_core.FormatString(data.ValkeyConf),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 87, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 87, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ {Code: "valkeyConf", Field: "ValkeyConf", Type: "array-map-fixed"},
+ {Code: "valkeyConfiguration", Field: "ValkeyConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8svalkeyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state K8svalkeyModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 87, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ {Code: "valkeyConf", Field: "ValkeyConf", Type: "array-map-fixed"},
+ {Code: "valkeyConfiguration", Field: "ValkeyConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *K8svalkeyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan K8svalkeyModel
+ var state K8svalkeyModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 1069: resources_core.BuildJSON(map[string]string{"shards": fmt.Sprintf("%d", plan.ClusterConfiguration.Shards.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64())}),
+ 1070: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString())}),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 87, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ {Code: "valkeyConf", Field: "ValkeyConf", Type: "array-map-fixed"},
+ {Code: "valkeyConfiguration", Field: "ValkeyConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8svalkeyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state K8svalkeyModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *K8svalkeyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *K8svalkeyResource) 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
+}
diff --git a/generated/dev/go/87_k8svalkey_user_resource.go b/generated/dev/go/87_k8svalkey_user_resource.go
new file mode 100644
index 0000000..3dbe8e0
--- /dev/null
+++ b/generated/dev/go/87_k8svalkey_user_resource.go
@@ -0,0 +1,358 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: k8svalkey
+// Subresource: user
+
+var _ resource.Resource = &K8svalkeyUserResource{}
+
+// K8svalkeyUserResource manages a subresource via operations.
+type K8svalkeyUserResource struct {
+ client *core.UniversalClient
+}
+
+type K8svalkeyUserModel struct {
+ ID types.String `tfsdk:"id"`
+ K8svalkeyID types.String `tfsdk:"k8svalkey_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"`
+ Permissions types.String `tfsdk:"permissions"`
+ Username types.String `tfsdk:"username"`
+}
+
+func NewK8svalkeyUserResource() resource.Resource {
+ return &K8svalkeyUserResource{}
+}
+
+func (r *K8svalkeyUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_k8svalkey_user"
+}
+
+func (r *K8svalkeyUserResource) 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()}},
+ "k8svalkey_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."},
+ "permissions": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "username": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *K8svalkeyUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan K8svalkeyUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if plan.Permissions.IsNull() || plan.Permissions.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "permissions is required.")
+ return
+ }
+ if plan.Username.IsNull() || plan.Username.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "username is required.")
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.K8svalkeyID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "permissions": resources_core.FormatString(plan.Permissions),
+ "username": resources_core.FormatString(plan.Username),
+ }
+ identityCodes := []string{
+ "permissions",
+ "username",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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, "user", 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{
+ "username": resources_core.FormatString(plan.Username),
+ "permissions": resources_core.FormatString(plan.Permissions),
+ })
+
+ 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, "create_user", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", 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, "user", 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, "user", 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, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *K8svalkeyUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state K8svalkeyUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8svalkeyUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan K8svalkeyUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state K8svalkeyUserModel
+ 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.K8svalkeyID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "permissions": resources_core.FormatString(plan.Permissions),
+ "username": resources_core.FormatString(plan.Username),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *K8svalkeyUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state K8svalkeyUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_user" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.K8svalkeyID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "permissions": resources_core.FormatString(state.Permissions),
+ "username": resources_core.FormatString(state.Username),
+ }
+ identityCodes := []string{
+ "permissions",
+ "username",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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{
+ "username": resources_core.FormatString(state.Username),
+ })
+
+ 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, "delete_user", 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 *K8svalkeyUserResource) 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
+}
diff --git a/generated/dev/go/88_k8s_ziti_controller_resource.go b/generated/dev/go/88_k8s_ziti_controller_resource.go
new file mode 100644
index 0000000..234875e
--- /dev/null
+++ b/generated/dev/go/88_k8s_ziti_controller_resource.go
@@ -0,0 +1,431 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: k8s_ziti_controller
+// Service ID: 88
+
+var _ resource.Resource = &K8sZitiControllerResource{}
+var _ resource.ResourceWithModifyPlan = &K8sZitiControllerResource{}
+var _ resource.ResourceWithImportState = &K8sZitiControllerResource{}
+
+type K8sZitiControllerResource struct {
+ client *core.UniversalClient
+}
+
+type K8sZitiControllerModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ Domain types.String `tfsdk:"domain"`
+ ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"`
+ ResourceDisk types.Int64 `tfsdk:"resource_disk"`
+ ResourceInstances types.Int64 `tfsdk:"resource_instances"`
+ ResourceMemory types.Int64 `tfsdk:"resource_memory"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewK8sZitiControllerResource() resource.Resource {
+ return &K8sZitiControllerResource{}
+}
+
+func (r *K8sZitiControllerResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_k8s_ziti_controller"
+}
+
+func (r *K8sZitiControllerResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8sZitiController-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."},
+ "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(300), MarkdownDescription: "Квота ядра пода (milicores) Не изменяется после создания."},
+ "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Квота диска (Указывается в GIGAbytes) Не изменяется после создания."},
+ "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов Не изменяется после создания."},
+ "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Квота памяти пода (MBytes) Не изменяется после создания."},
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *K8sZitiControllerResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *K8sZitiControllerModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *K8sZitiControllerModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan K8sZitiControllerModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() {
+ if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_c_p_u", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() {
+ if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_disk", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() {
+ if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_instances", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() {
+ if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_memory", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.ResourceRealm.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+ if config.ResourceRealm.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 294: resources_core.FormatInt64(config.ResourceCPU),
+ 295: resources_core.FormatInt64(config.ResourceMemory),
+ 296: resources_core.FormatInt64(config.ResourceDisk),
+ 297: resources_core.FormatInt64(config.ResourceInstances),
+ 298: resources_core.FormatString(config.ResourceRealm),
+ 299: resources_core.FormatString(config.Domain),
+ }
+ desiredDomain := ""
+ if !config.Domain.IsNull() && !config.Domain.IsUnknown() {
+ desiredDomain = config.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 88, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *K8sZitiControllerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data K8sZitiControllerModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ if !data.Domain.IsNull() && !data.Domain.IsUnknown() {
+ desiredDomain = data.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 88, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 294: resources_core.FormatInt64(data.ResourceCPU),
+ 295: resources_core.FormatInt64(data.ResourceMemory),
+ 296: resources_core.FormatInt64(data.ResourceDisk),
+ 297: resources_core.FormatInt64(data.ResourceInstances),
+ 298: resources_core.FormatString(data.ResourceRealm),
+ 299: resources_core.FormatString(data.Domain),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 88, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 88, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8sZitiControllerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state K8sZitiControllerModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 88, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *K8sZitiControllerResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan K8sZitiControllerModel
+ var state K8sZitiControllerModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 88, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8sZitiControllerResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state K8sZitiControllerModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *K8sZitiControllerResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *K8sZitiControllerResource) 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
+}
diff --git a/generated/dev/go/89_flask_resource.go b/generated/dev/go/89_flask_resource.go
new file mode 100644
index 0000000..f6624df
--- /dev/null
+++ b/generated/dev/go/89_flask_resource.go
@@ -0,0 +1,449 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: flask
+// Service ID: 89
+
+var _ resource.Resource = &FlaskResource{}
+var _ resource.ResourceWithModifyPlan = &FlaskResource{}
+var _ resource.ResourceWithImportState = &FlaskResource{}
+
+type FlaskResource struct {
+ client *core.UniversalClient
+}
+
+// FlaskAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type FlaskAccessConfigurationModel struct {
+ Domain types.String `tfsdk:"domain" json:"domain"`
+}
+
+// FlaskAppConfigurationModel — вложенная модель для map-fixed параметра appConfiguration.
+type FlaskAppConfigurationModel struct {
+ Version types.String `tfsdk:"version" json:"version"`
+ GitPath types.String `tfsdk:"git_path" json:"gitPath"`
+ HealthPath types.String `tfsdk:"health_path" json:"healthPath"`
+}
+
+// FlaskClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type FlaskClusterConfigurationModel struct {
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+}
+
+// FlaskStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type FlaskStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+}
+
+type FlaskModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // git_revision: при изменении вызывает redeploy вместо modify.
+ // Опциональное поле — если не задано, modify работает как обычно.
+ GitRevision types.String `tfsdk:"git_revision"`
+ AccessConfiguration *FlaskAccessConfigurationModel `tfsdk:"access_configuration"`
+ AppConfiguration *FlaskAppConfigurationModel `tfsdk:"app_configuration"`
+ ClusterConfiguration *FlaskClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ JsonEnv types.String `tfsdk:"json_env"`
+ StartupConfiguration *FlaskStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewFlaskResource() resource.Resource {
+ return &FlaskResource{}
+}
+
+func (r *FlaskResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_flask"
+}
+
+func (r *FlaskResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("flask-y0lfavjf"), MarkdownDescription: "Если указан FQDN в формате: `myapp.example.com`, предварительно будет проверяться DNS-запись (А-запись **должна смотреть на IP Ingress кластера ресурсной платформы**)
Если указан в формате `myapp`, DNS-запись будет автоматически создана в служебной DNS-зоне"},
+ }},
+ "app_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("3.12")},
+ "git_path": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("https://github.com/Foxyhhd/Baldurs-Gate-test.git"), MarkdownDescription: "Путь до репозитория приложения
Обязательно должен быть передан как URL: `https://....`
Должен быть сетевой доступ до URL для скачивания исходников"},
+ "health_path": schema.StringAttribute{Optional: true, MarkdownDescription: "Путь до healtcheck для запуска пода в Kubernetes, который отдаст 200 код
Пустое значение означает, что будет проверяться исключительно доступность приложения по порту"},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(200), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(256), MarkdownDescription: "Указывается в Megabytes"},
+ }},
+ "json_env": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("{}")},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"},
+ }},
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // При изменении git_revision вызывается redeploy вместо modify.
+ // Если не задано — modify работает без изменений.
+ "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *FlaskResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *FlaskModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *FlaskModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan FlaskModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 1037: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1038: resources_core.BuildJSON(map[string]string{"replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64())}),
+ 1039: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Domain.ValueString())}),
+ 1040: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.HealthPath.ValueString())}),
+ 1041: resources_core.FormatString(config.JsonEnv),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 89, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *FlaskResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data FlaskModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 89, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 1037: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1038: resources_core.BuildJSON(map[string]string{"replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64())}),
+ 1039: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Domain.ValueString())}),
+ 1040: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.HealthPath.ValueString())}),
+ 1041: resources_core.FormatString(data.JsonEnv),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 89, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 89, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *FlaskResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state FlaskModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 89, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *FlaskResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan FlaskModel
+ var state FlaskModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.JsonEnv.IsNull() != state.JsonEnv.IsNull() || plan.JsonEnv.IsUnknown() != state.JsonEnv.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.JsonEnv.IsNull() && !plan.JsonEnv.IsUnknown() && !state.JsonEnv.IsNull() && !state.JsonEnv.IsUnknown() {
+ if !strings.EqualFold(plan.JsonEnv.ValueString(), state.JsonEnv.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // Проверяем изменился ли git_revision.
+ // Если да — вызываем redeploy вместо modify.
+ // git_revision = "" (не задано) → обычный modify.
+ redeployRequested := false
+ if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() {
+ if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() {
+ redeployRequested = true
+ }
+ }
+
+ if !hasServiceParamChanges && !redeployRequested {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ if redeployRequested {
+ // Вызываем redeploy — пустой params (операция не требует CFS-параметров)
+ if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ } else {
+ params := map[int]string{
+ 1042: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64())}),
+ 1043: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.HealthPath.ValueString())}),
+ 1044: resources_core.FormatString(plan.JsonEnv),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 89, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *FlaskResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state FlaskModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *FlaskResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *FlaskResource) 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
+}
diff --git a/generated/dev/go/90_postgres_backup_resource.go b/generated/dev/go/90_postgres_backup_resource.go
new file mode 100644
index 0000000..957eb3e
--- /dev/null
+++ b/generated/dev/go/90_postgres_backup_resource.go
@@ -0,0 +1,326 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: postgres
+// Subresource: backup
+
+var _ resource.Resource = &PostgresBackupResource{}
+
+// PostgresBackupResource manages a subresource via operations.
+type PostgresBackupResource struct {
+ client *core.UniversalClient
+}
+
+type PostgresBackupModel struct {
+ ID types.String `tfsdk:"id"`
+ PostgresID types.String `tfsdk:"postgres_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"`
+}
+
+func NewPostgresBackupResource() resource.Resource {
+ return &PostgresBackupResource{}
+}
+
+func (r *PostgresBackupResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_postgres_backup"
+}
+
+func (r *PostgresBackupResource) 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()}},
+ "postgres_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."},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *PostgresBackupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan PostgresBackupModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.PostgresID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{}
+ identityCodes := []string{}
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("backup", 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, "backup", 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{})
+
+ 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, "create_backup", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_backup", 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, "backup", 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, "backup", 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, "backup", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *PostgresBackupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state PostgresBackupModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *PostgresBackupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan PostgresBackupModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state PostgresBackupModel
+ 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.PostgresID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{}
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *PostgresBackupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state PostgresBackupModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.PostgresID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{}
+ identityCodes := []string{}
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("backup", 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{})
+
+ 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, "", 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 *PostgresBackupResource) 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
+}
diff --git a/generated/dev/go/90_postgres_database_resource.go b/generated/dev/go/90_postgres_database_resource.go
new file mode 100644
index 0000000..7310720
--- /dev/null
+++ b/generated/dev/go/90_postgres_database_resource.go
@@ -0,0 +1,358 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: postgres
+// Subresource: database
+
+var _ resource.Resource = &PostgresDatabaseResource{}
+
+// PostgresDatabaseResource manages a subresource via operations.
+type PostgresDatabaseResource struct {
+ client *core.UniversalClient
+}
+
+type PostgresDatabaseModel struct {
+ ID types.String `tfsdk:"id"`
+ PostgresID types.String `tfsdk:"postgres_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"`
+ DbName types.String `tfsdk:"db_name"`
+ DbOwner types.String `tfsdk:"db_owner"`
+}
+
+func NewPostgresDatabaseResource() resource.Resource {
+ return &PostgresDatabaseResource{}
+}
+
+func (r *PostgresDatabaseResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_postgres_database"
+}
+
+func (r *PostgresDatabaseResource) 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()}},
+ "postgres_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."},
+ "db_name": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "db_owner": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *PostgresDatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan PostgresDatabaseModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if plan.DbName.IsNull() || plan.DbName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "db_name is required.")
+ return
+ }
+ if plan.DbOwner.IsNull() || plan.DbOwner.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "db_owner is required.")
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.PostgresID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "dbName": resources_core.FormatString(plan.DbName),
+ "dbOwner": resources_core.FormatString(plan.DbOwner),
+ }
+ identityCodes := []string{
+ "dbName",
+ "dbOwner",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", 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, "database", 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{
+ "dbName": resources_core.FormatString(plan.DbName),
+ "dbOwner": resources_core.FormatString(plan.DbOwner),
+ })
+
+ 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, "create_database", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_database", 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, "database", 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, "database", 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, "database", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *PostgresDatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state PostgresDatabaseModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *PostgresDatabaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan PostgresDatabaseModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state PostgresDatabaseModel
+ 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.PostgresID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "dbName": resources_core.FormatString(plan.DbName),
+ "dbOwner": resources_core.FormatString(plan.DbOwner),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *PostgresDatabaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state PostgresDatabaseModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_database" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.PostgresID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "dbName": resources_core.FormatString(state.DbName),
+ "dbOwner": resources_core.FormatString(state.DbOwner),
+ }
+ identityCodes := []string{
+ "dbName",
+ "dbOwner",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", 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{
+ "dbName": resources_core.FormatString(state.DbName),
+ })
+
+ 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, "delete_database", 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 *PostgresDatabaseResource) 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
+}
diff --git a/generated/dev/go/90_postgres_resource.go b/generated/dev/go/90_postgres_resource.go
new file mode 100644
index 0000000..518f22c
--- /dev/null
+++ b/generated/dev/go/90_postgres_resource.go
@@ -0,0 +1,499 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: postgres
+// Service ID: 90
+
+var _ resource.Resource = &PostgresResource{}
+var _ resource.ResourceWithModifyPlan = &PostgresResource{}
+var _ resource.ResourceWithImportState = &PostgresResource{}
+
+type PostgresResource struct {
+ client *core.UniversalClient
+}
+
+// PostgresAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type PostgresAccessConfigurationModel struct {
+ MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"`
+ MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"`
+ SlaveIpSpace types.String `tfsdk:"slave_ip_space" json:"slaveIpSpace"`
+ SlaveAccessList types.String `tfsdk:"slave_access_list" json:"slaveAccessList"`
+}
+
+// PostgresAutoscaleConfigurationModel — вложенная модель для map-fixed параметра autoscaleConfiguration.
+type PostgresAutoscaleConfigurationModel struct {
+ Percent types.Int64 `tfsdk:"percent" json:"percent"`
+ Enabled types.Bool `tfsdk:"enabled" json:"enabled"`
+ Schedule types.Int64 `tfsdk:"schedule" json:"schedule"`
+ Quota types.Int64 `tfsdk:"quota" json:"quota"`
+}
+
+// PostgresBackupConfigurationModel — вложенная модель для map-fixed параметра backupConfiguration.
+type PostgresBackupConfigurationModel struct {
+ S3Uid types.String `tfsdk:"s3_uid" json:"s3Uid"`
+ Retain types.Int64 `tfsdk:"retain" json:"retain"`
+ Schedule types.String `tfsdk:"schedule" json:"schedule"`
+}
+
+// PostgresClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type PostgresClusterConfigurationModel struct {
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+ Disk types.Int64 `tfsdk:"disk" json:"disk"`
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+}
+
+// PostgresMtlsConfigurationModel — вложенная модель для map-fixed параметра mtlsConfiguration.
+type PostgresMtlsConfigurationModel struct {
+ Type types.String `tfsdk:"type" json:"type"`
+ DurationCA types.Int64 `tfsdk:"duration_c_a" json:"durationCA"`
+ DurationServer types.Int64 `tfsdk:"duration_server" json:"durationServer"`
+ CertCA types.String `tfsdk:"cert_c_a" json:"certCA"`
+ CertServer types.String `tfsdk:"cert_server" json:"certServer"`
+}
+
+// PostgresPostgresConfigurationModel — вложенная модель для map-fixed параметра postgresConfiguration.
+type PostgresPostgresConfigurationModel struct {
+ Version types.String `tfsdk:"version" json:"version"`
+ SslRequired types.Bool `tfsdk:"ssl_required" json:"sslRequired"`
+ PoolerMaster types.Bool `tfsdk:"pooler_master" json:"poolerMaster"`
+ PoolerSlave types.Bool `tfsdk:"pooler_slave" json:"poolerSlave"`
+}
+
+// PostgresStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type PostgresStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+}
+
+type PostgresModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AccessConfiguration *PostgresAccessConfigurationModel `tfsdk:"access_configuration"`
+ AutoscaleConfiguration *PostgresAutoscaleConfigurationModel `tfsdk:"autoscale_configuration"`
+ BackupConfiguration *PostgresBackupConfigurationModel `tfsdk:"backup_configuration"`
+ ClusterConfiguration *PostgresClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ MtlsConfiguration *PostgresMtlsConfigurationModel `tfsdk:"mtls_configuration"`
+ PostgresConf types.String `tfsdk:"postgres_conf"`
+ PostgresConfiguration *PostgresPostgresConfigurationModel `tfsdk:"postgres_configuration"`
+ StartupConfiguration *PostgresStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewPostgresResource() resource.Resource {
+ return &PostgresResource{}
+}
+
+func (r *PostgresResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_postgres"
+}
+
+func (r *PostgresResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "master_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"},
+ "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"},
+ "slave_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"},
+ "slave_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"},
+ }},
+ "autoscale_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "percent": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Требует включения autoScale. Позволяет указать в процентах расширение от текущего макс объема
Минимальный размер на который расширяется - **1Gb**"},
+ "enabled": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает автоскейлинг PV. При включении параметра необходимо настроить autoScalePercentage и autoScaleTechWindow. Алерт при срабатывании вызывает modify у текущей услуги"},
+ "schedule": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "Требует включения autoScale. Окно обновления (пока не реализовано). Указывается час в виде Integer (0 / 5 / 12 / 23)"},
+ "quota": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100), MarkdownDescription: "До какого размера возможно увеличивать диск, когда включен Autoscale"},
+ }},
+ "backup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "s3_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Экземпляр S3 обязателен"},
+ "retain": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(7)},
+ "schedule": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("0 0 * * *")},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Указывается в Megabytes"},
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Указывается в GIGAbytes"},
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ }},
+ "mtls_configuration": schema.SingleNestedAttribute{Optional: true, Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "type": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("off"), MarkdownDescription: "`off` - Настройка MTLS производиться не будет
`manual` - После создания кластера в полях данных будет запрос на создание сертификата. **Вам необходимо будет выписать сертификат через вашу CA** и уже после включить через операцию **modify** режим MTLS
`auto` - Сертификаты будут созданы через Cert-Manager
Режимы **auto** и **manual** требует включить настройку `Включить sslMode` - **true**
MTLS в режиме **auto** будет работать сразу, в режиме **manual** только после того как будет предоставлен **CA** и **Сертификат сервера**"},
+ "duration_c_a": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(175200), MarkdownDescription: "Указывается в **часах**
Необходимо настраивать, когда параметр `Режим настройки` - **auto**"},
+ "duration_server": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(87600), MarkdownDescription: "Указывается в **часах**
Не может быть больше чем время жизни СА
Обновляется **раз в год**, если выставлено значение **более 2 лет**
Обновляется **раз в пол года**, если выставлено значение **менее 2 лет**
Необходимо настраивать, когда параметр `Режим настройки` - **auto**"},
+ "cert_c_a": schema.StringAttribute{Optional: true},
+ "cert_server": schema.StringAttribute{Optional: true},
+ }},
+ "postgres_conf": schema.StringAttribute{Optional: true},
+ "postgres_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("17")},
+ "ssl_required": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "По умолчанию подключение обязательное (sslmode=require)"},
+ "pooler_master": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает pgPooler (на запись). Будет создан новый под
Размер пода **200m/400Mi**"},
+ "pooler_slave": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает pgPooler (на чтение). Будет создан новый под
Размер пода **200m/400Mi**"},
+ }},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"},
+ }},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *PostgresResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *PostgresModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *PostgresModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan PostgresModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 788: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64())}),
+ 789: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}),
+ 790: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveAccessList.ValueString())}),
+ 791: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.PostgresConfiguration.Version.ValueString()), "sslRequired": fmt.Sprintf("%v", config.PostgresConfiguration.SslRequired.ValueBool()), "poolerMaster": fmt.Sprintf("%v", config.PostgresConfiguration.PoolerMaster.ValueBool()), "poolerSlave": fmt.Sprintf("%v", config.PostgresConfiguration.PoolerSlave.ValueBool())}),
+ 792: resources_core.FormatString(config.PostgresConf),
+ 793: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", config.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", config.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", config.BackupConfiguration.Schedule.ValueString())}),
+ 794: resources_core.BuildJSON(map[string]string{"percent": fmt.Sprintf("%d", config.AutoscaleConfiguration.Percent.ValueInt64()), "enabled": fmt.Sprintf("%v", config.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", config.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", config.AutoscaleConfiguration.Quota.ValueInt64())}),
+ 1094: resources_core.BuildJSON(map[string]string{"type": fmt.Sprintf("\"%s\"", config.MtlsConfiguration.Type.ValueString()), "durationCA": fmt.Sprintf("%d", config.MtlsConfiguration.DurationCA.ValueInt64()), "durationServer": fmt.Sprintf("%d", config.MtlsConfiguration.DurationServer.ValueInt64()), "certCA": fmt.Sprintf("\"%s\"", config.MtlsConfiguration.CertCA.ValueString()), "certServer": fmt.Sprintf("\"%s\"", config.MtlsConfiguration.CertServer.ValueString())}),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 90, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *PostgresResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data PostgresModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 90, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 788: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64())}),
+ 789: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}),
+ 790: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveAccessList.ValueString())}),
+ 791: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.PostgresConfiguration.Version.ValueString()), "sslRequired": fmt.Sprintf("%v", data.PostgresConfiguration.SslRequired.ValueBool()), "poolerMaster": fmt.Sprintf("%v", data.PostgresConfiguration.PoolerMaster.ValueBool()), "poolerSlave": fmt.Sprintf("%v", data.PostgresConfiguration.PoolerSlave.ValueBool())}),
+ 792: resources_core.FormatString(data.PostgresConf),
+ 793: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", data.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", data.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", data.BackupConfiguration.Schedule.ValueString())}),
+ 794: resources_core.BuildJSON(map[string]string{"percent": fmt.Sprintf("%d", data.AutoscaleConfiguration.Percent.ValueInt64()), "enabled": fmt.Sprintf("%v", data.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", data.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", data.AutoscaleConfiguration.Quota.ValueInt64())}),
+ 1094: resources_core.BuildJSON(map[string]string{"type": fmt.Sprintf("\"%s\"", data.MtlsConfiguration.Type.ValueString()), "durationCA": fmt.Sprintf("%d", data.MtlsConfiguration.DurationCA.ValueInt64()), "durationServer": fmt.Sprintf("%d", data.MtlsConfiguration.DurationServer.ValueInt64()), "certCA": fmt.Sprintf("\"%s\"", data.MtlsConfiguration.CertCA.ValueString()), "certServer": fmt.Sprintf("\"%s\"", data.MtlsConfiguration.CertServer.ValueString())}),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 90, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 90, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"},
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "mtlsConfiguration", Field: "MtlsConfiguration", Type: "map-fixed"},
+ {Code: "postgresConf", Field: "PostgresConf", Type: "array-map-fixed"},
+ {Code: "postgresConfiguration", Field: "PostgresConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *PostgresResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state PostgresModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 90, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"},
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "mtlsConfiguration", Field: "MtlsConfiguration", Type: "map-fixed"},
+ {Code: "postgresConf", Field: "PostgresConf", Type: "array-map-fixed"},
+ {Code: "postgresConfiguration", Field: "PostgresConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *PostgresResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan PostgresModel
+ var state PostgresModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.PostgresConf.IsNull() != state.PostgresConf.IsNull() || plan.PostgresConf.IsUnknown() != state.PostgresConf.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.PostgresConf.IsNull() && !plan.PostgresConf.IsUnknown() && !state.PostgresConf.IsNull() && !state.PostgresConf.IsUnknown() {
+ if !strings.EqualFold(plan.PostgresConf.ValueString(), state.PostgresConf.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 795: resources_core.BuildJSON(map[string]string{"replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64())}),
+ 796: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveAccessList.ValueString())}),
+ 797: resources_core.BuildJSON(map[string]string{"poolerSlave": fmt.Sprintf("%v", plan.PostgresConfiguration.PoolerSlave.ValueBool()), "version": fmt.Sprintf("\"%s\"", plan.PostgresConfiguration.Version.ValueString()), "sslRequired": fmt.Sprintf("%v", plan.PostgresConfiguration.SslRequired.ValueBool()), "poolerMaster": fmt.Sprintf("%v", plan.PostgresConfiguration.PoolerMaster.ValueBool())}),
+ 798: resources_core.FormatString(plan.PostgresConf),
+ 799: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", plan.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.Schedule.ValueString())}),
+ 800: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", plan.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Percent.ValueInt64())}),
+ 1095: resources_core.BuildJSON(map[string]string{"certServer": fmt.Sprintf("\"%s\"", plan.MtlsConfiguration.CertServer.ValueString()), "type": fmt.Sprintf("\"%s\"", plan.MtlsConfiguration.Type.ValueString()), "durationCA": fmt.Sprintf("%d", plan.MtlsConfiguration.DurationCA.ValueInt64()), "durationServer": fmt.Sprintf("%d", plan.MtlsConfiguration.DurationServer.ValueInt64()), "certCA": fmt.Sprintf("\"%s\"", plan.MtlsConfiguration.CertCA.ValueString())}),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 90, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"},
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "mtlsConfiguration", Field: "MtlsConfiguration", Type: "map-fixed"},
+ {Code: "postgresConf", Field: "PostgresConf", Type: "array-map-fixed"},
+ {Code: "postgresConfiguration", Field: "PostgresConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *PostgresResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state PostgresModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *PostgresResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *PostgresResource) 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
+}
diff --git a/generated/dev/go/90_postgres_user_resource.go b/generated/dev/go/90_postgres_user_resource.go
new file mode 100644
index 0000000..8ac874c
--- /dev/null
+++ b/generated/dev/go/90_postgres_user_resource.go
@@ -0,0 +1,367 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: postgres
+// Subresource: user
+
+var _ resource.Resource = &PostgresUserResource{}
+
+// PostgresUserResource manages a subresource via operations.
+type PostgresUserResource struct {
+ client *core.UniversalClient
+}
+
+type PostgresUserModel struct {
+ ID types.String `tfsdk:"id"`
+ PostgresID types.String `tfsdk:"postgres_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"`
+ MtlsAccess types.Bool `tfsdk:"mtls_access"`
+ Role types.String `tfsdk:"role"`
+ Username types.String `tfsdk:"username"`
+}
+
+func NewPostgresUserResource() resource.Resource {
+ return &PostgresUserResource{}
+}
+
+func (r *PostgresUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_postgres_user"
+}
+
+func (r *PostgresUserResource) 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()}},
+ "postgres_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."},
+ "mtls_access": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Подключение пользователя без ввода паролей
Политика применяется **ко всем базам** внутри Postgresql
RBAC на базы настраивать вручную Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.Bool{boolplanmodifier.RequiresReplace()}},
+ "role": schema.StringAttribute{Required: true, MarkdownDescription: "Выбрать роль из списка Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "username": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *PostgresUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan PostgresUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if plan.Role.IsNull() || plan.Role.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "role is required.")
+ return
+ }
+ if plan.Username.IsNull() || plan.Username.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "username is required.")
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.PostgresID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "mtlsAccess": resources_core.FormatBool(plan.MtlsAccess),
+ "role": resources_core.FormatString(plan.Role),
+ "username": resources_core.FormatString(plan.Username),
+ }
+ identityCodes := []string{
+ "mtlsAccess",
+ "role",
+ "username",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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, "user", 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{
+ "username": resources_core.FormatString(plan.Username),
+ "role": resources_core.FormatString(plan.Role),
+ "mtlsAccess": resources_core.FormatBool(plan.MtlsAccess),
+ })
+
+ 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, "create_user", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", 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, "user", 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, "user", 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, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *PostgresUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state PostgresUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *PostgresUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan PostgresUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state PostgresUserModel
+ 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.PostgresID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "mtlsAccess": resources_core.FormatBool(plan.MtlsAccess),
+ "role": resources_core.FormatString(plan.Role),
+ "username": resources_core.FormatString(plan.Username),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *PostgresUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state PostgresUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_user" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.PostgresID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "mtlsAccess": resources_core.FormatBool(state.MtlsAccess),
+ "role": resources_core.FormatString(state.Role),
+ "username": resources_core.FormatString(state.Username),
+ }
+ identityCodes := []string{
+ "mtlsAccess",
+ "role",
+ "username",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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{
+ "username": resources_core.FormatString(state.Username),
+ })
+
+ 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, "delete_user", 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 *PostgresUserResource) 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
+}
diff --git a/generated/dev/go/91_redis_resource.go b/generated/dev/go/91_redis_resource.go
new file mode 100644
index 0000000..2462093
--- /dev/null
+++ b/generated/dev/go/91_redis_resource.go
@@ -0,0 +1,393 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: redis
+// Service ID: 91
+
+var _ resource.Resource = &RedisResource{}
+var _ resource.ResourceWithModifyPlan = &RedisResource{}
+var _ resource.ResourceWithImportState = &RedisResource{}
+
+type RedisResource struct {
+ client *core.UniversalClient
+}
+
+// RedisAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type RedisAccessConfigurationModel struct {
+ MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"`
+ MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"`
+ SlaveIpSpace types.String `tfsdk:"slave_ip_space" json:"slaveIpSpace"`
+ SlaveAccessList types.String `tfsdk:"slave_access_list" json:"slaveAccessList"`
+}
+
+// RedisClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type RedisClusterConfigurationModel struct {
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+ Disk types.Int64 `tfsdk:"disk" json:"disk"`
+}
+
+// RedisStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type RedisStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+}
+
+type RedisModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AccessConfiguration *RedisAccessConfigurationModel `tfsdk:"access_configuration"`
+ ClusterConfiguration *RedisClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ StartupConfiguration *RedisStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewRedisResource() resource.Resource {
+ return &RedisResource{}
+}
+
+func (r *RedisResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_redis"
+}
+
+func (r *RedisResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "master_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"},
+ "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"},
+ "slave_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"},
+ "slave_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Указывается в Megabytes"},
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Указывается в GIGAbytes"},
+ }},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"},
+ }},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *RedisResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *RedisModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *RedisModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan RedisModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 1061: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1062: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64())}),
+ 1063: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveAccessList.ValueString())}),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 91, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *RedisResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data RedisModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 91, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 1061: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1062: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64())}),
+ 1063: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveAccessList.ValueString())}),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 91, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 91, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *RedisResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state RedisModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 91, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *RedisResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan RedisModel
+ var state RedisModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 1064: resources_core.BuildJSON(map[string]string{"replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64())}),
+ 1065: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveAccessList.ValueString())}),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 91, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *RedisResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state RedisModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *RedisResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *RedisResource) 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
+}
diff --git a/generated/dev/go/92_mongodb_resource.go b/generated/dev/go/92_mongodb_resource.go
new file mode 100644
index 0000000..2a8e46d
--- /dev/null
+++ b/generated/dev/go/92_mongodb_resource.go
@@ -0,0 +1,389 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: mongodb
+// Service ID: 92
+
+var _ resource.Resource = &MongodbResource{}
+var _ resource.ResourceWithModifyPlan = &MongodbResource{}
+var _ resource.ResourceWithImportState = &MongodbResource{}
+
+type MongodbResource struct {
+ client *core.UniversalClient
+}
+
+// MongodbAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type MongodbAccessConfigurationModel struct {
+ MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"`
+ MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"`
+}
+
+// MongodbClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type MongodbClusterConfigurationModel struct {
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+ Disk types.Int64 `tfsdk:"disk" json:"disk"`
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+}
+
+// MongodbStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type MongodbStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+}
+
+type MongodbModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AccessConfiguration *MongodbAccessConfigurationModel `tfsdk:"access_configuration"`
+ ClusterConfiguration *MongodbClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ StartupConfiguration *MongodbStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewMongodbResource() resource.Resource {
+ return &MongodbResource{}
+}
+
+func (r *MongodbResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_mongodb"
+}
+
+func (r *MongodbResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"},
+ "master_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1024)},
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(5), MarkdownDescription: "Указывается в Gb"},
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1000)},
+ }},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"},
+ }},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *MongodbResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *MongodbModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *MongodbModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan MongodbModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 1089: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1090: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64())}),
+ 1091: resources_core.BuildJSON(map[string]string{"masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString())}),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 92, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *MongodbResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data MongodbModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 92, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 1089: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1090: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64())}),
+ 1091: resources_core.BuildJSON(map[string]string{"masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString())}),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 92, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 92, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *MongodbResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state MongodbModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 92, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *MongodbResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan MongodbModel
+ var state MongodbModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 1092: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64())}),
+ 1093: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString())}),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 92, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *MongodbResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state MongodbModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *MongodbResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *MongodbResource) 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
+}
diff --git a/generated/dev/go/92_mongodb_user_resource.go b/generated/dev/go/92_mongodb_user_resource.go
new file mode 100644
index 0000000..6f0e40d
--- /dev/null
+++ b/generated/dev/go/92_mongodb_user_resource.go
@@ -0,0 +1,364 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: mongodb
+// Subresource: user
+
+var _ resource.Resource = &MongodbUserResource{}
+
+// MongodbUserResource manages a subresource via operations.
+type MongodbUserResource struct {
+ client *core.UniversalClient
+}
+
+type MongodbUserModel struct {
+ ID types.String `tfsdk:"id"`
+ MongodbID types.String `tfsdk:"mongodb_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"`
+ DbName types.String `tfsdk:"db_name"`
+ Role types.String `tfsdk:"role"`
+ Username types.String `tfsdk:"username"`
+}
+
+func NewMongodbUserResource() resource.Resource {
+ return &MongodbUserResource{}
+}
+
+func (r *MongodbUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_mongodb_user"
+}
+
+func (r *MongodbUserResource) 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()}},
+ "mongodb_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."},
+ "db_name": schema.StringAttribute{Required: true},
+ "role": schema.StringAttribute{Required: true},
+ "username": schema.StringAttribute{Required: true, MarkdownDescription: "Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *MongodbUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan MongodbUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if plan.DbName.IsNull() || plan.DbName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "db_name is required.")
+ return
+ }
+ if plan.Role.IsNull() || plan.Role.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "role is required.")
+ return
+ }
+ if plan.Username.IsNull() || plan.Username.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "username is required.")
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.MongodbID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "username": resources_core.FormatString(plan.Username),
+ }
+ identityCodes := []string{
+ "username",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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, "user", 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{
+ "username": resources_core.FormatString(plan.Username),
+ "dbName": resources_core.FormatString(plan.DbName),
+ "role": resources_core.FormatString(plan.Role),
+ })
+
+ 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, "create_user", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", 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, "user", 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, "user", 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, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *MongodbUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state MongodbUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *MongodbUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan MongodbUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "modify_user" == "" {
+ var state MongodbUserModel
+ 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.MongodbID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{
+ "username": resources_core.FormatString(plan.Username),
+ "dbName": resources_core.FormatString(plan.DbName),
+ "role": resources_core.FormatString(plan.Role),
+ })
+
+ idParams := map[string]string{
+ "username": resources_core.FormatString(plan.Username),
+ }
+
+ 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, "modify_user", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *MongodbUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state MongodbUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_user" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.MongodbID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "username": resources_core.FormatString(state.Username),
+ }
+ identityCodes := []string{
+ "username",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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{
+ "username": resources_core.FormatString(state.Username),
+ })
+
+ 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, "delete_user", 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 *MongodbUserResource) 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
+}
diff --git a/generated/dev/go/93_rabbitmq_resource.go b/generated/dev/go/93_rabbitmq_resource.go
new file mode 100644
index 0000000..a43cd4d
--- /dev/null
+++ b/generated/dev/go/93_rabbitmq_resource.go
@@ -0,0 +1,474 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: rabbitmq
+// Service ID: 93
+
+var _ resource.Resource = &RabbitmqResource{}
+var _ resource.ResourceWithModifyPlan = &RabbitmqResource{}
+var _ resource.ResourceWithImportState = &RabbitmqResource{}
+
+type RabbitmqResource struct {
+ client *core.UniversalClient
+}
+
+type RabbitmqModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ IpSpaceNameMaster types.String `tfsdk:"ip_space_name_master"`
+ MasterAccessList types.String `tfsdk:"master_access_list"`
+ ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"`
+ ResourceDisk types.Int64 `tfsdk:"resource_disk"`
+ ResourceInstances types.Int64 `tfsdk:"resource_instances"`
+ ResourceMemory types.Int64 `tfsdk:"resource_memory"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewRabbitmqResource() resource.Resource {
+ return &RabbitmqResource{}
+}
+
+func (r *RabbitmqResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_rabbitmq"
+}
+
+func (r *RabbitmqResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "ip_space_name_master": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `internet-no-antiddos-v1`"},
+ "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Список IP-адресов, с которых разрешён доступ. Если оставить пустым — доступ открыт всем.", PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()}},
+ "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1000), MarkdownDescription: "Пример: `2000`"},
+ "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(30), MarkdownDescription: "Пример: `20`"},
+ "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Пример: `1`"},
+ "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1124), MarkdownDescription: "Пример: `2048`"},
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Выбрать платформу из списка Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *RabbitmqResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *RabbitmqModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *RabbitmqModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan RabbitmqModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.IpSpaceNameMaster.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "ip_space_name_master is required.")
+ return
+ }
+ if config.IpSpaceNameMaster.IsUnknown() {
+ return
+ }
+ if config.ResourceRealm.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+ if config.ResourceRealm.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 116: resources_core.FormatInt64(config.ResourceCPU),
+ 117: resources_core.FormatInt64(config.ResourceMemory),
+ 118: resources_core.FormatInt64(config.ResourceDisk),
+ 119: resources_core.FormatString(config.ResourceRealm),
+ 224: resources_core.FormatInt64(config.ResourceInstances),
+ 225: resources_core.FormatString(config.MasterAccessList),
+ 543: resources_core.FormatString(config.IpSpaceNameMaster),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 93, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *RabbitmqResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data RabbitmqModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.IpSpaceNameMaster.IsNull() || data.IpSpaceNameMaster.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "ip_space_name_master is required.")
+ return
+ }
+ if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 93, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 116: resources_core.FormatInt64(data.ResourceCPU),
+ 117: resources_core.FormatInt64(data.ResourceMemory),
+ 118: resources_core.FormatInt64(data.ResourceDisk),
+ 119: resources_core.FormatString(data.ResourceRealm),
+ 224: resources_core.FormatInt64(data.ResourceInstances),
+ 225: resources_core.FormatString(data.MasterAccessList),
+ 543: resources_core.FormatString(data.IpSpaceNameMaster),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 93, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 93, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"},
+ {Code: "masterAccessList", Field: "MasterAccessList", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *RabbitmqResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state RabbitmqModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 93, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"},
+ {Code: "masterAccessList", Field: "MasterAccessList", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *RabbitmqResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan RabbitmqModel
+ var state RabbitmqModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() {
+ if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() {
+ if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceDisk.IsNull() != state.ResourceDisk.IsNull() || plan.ResourceDisk.IsUnknown() != state.ResourceDisk.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() {
+ if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceInstances.IsNull() != state.ResourceInstances.IsNull() || plan.ResourceInstances.IsUnknown() != state.ResourceInstances.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() {
+ if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.MasterAccessList.IsNull() != state.MasterAccessList.IsNull() || plan.MasterAccessList.IsUnknown() != state.MasterAccessList.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.MasterAccessList.IsNull() && !plan.MasterAccessList.IsUnknown() && !state.MasterAccessList.IsNull() && !state.MasterAccessList.IsUnknown() {
+ if !strings.EqualFold(plan.MasterAccessList.ValueString(), state.MasterAccessList.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.IpSpaceNameMaster.IsNull() != state.IpSpaceNameMaster.IsNull() || plan.IpSpaceNameMaster.IsUnknown() != state.IpSpaceNameMaster.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.IpSpaceNameMaster.IsNull() && !plan.IpSpaceNameMaster.IsUnknown() && !state.IpSpaceNameMaster.IsNull() && !state.IpSpaceNameMaster.IsUnknown() {
+ if !strings.EqualFold(plan.IpSpaceNameMaster.ValueString(), state.IpSpaceNameMaster.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 304: resources_core.FormatInt64(plan.ResourceCPU),
+ 305: resources_core.FormatInt64(plan.ResourceMemory),
+ 306: resources_core.FormatInt64(plan.ResourceDisk),
+ 307: resources_core.FormatInt64(plan.ResourceInstances),
+ 308: resources_core.FormatString(plan.MasterAccessList),
+ 545: resources_core.FormatString(plan.IpSpaceNameMaster),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 93, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"},
+ {Code: "masterAccessList", Field: "MasterAccessList", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *RabbitmqResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state RabbitmqModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *RabbitmqResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *RabbitmqResource) 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
+}
diff --git a/generated/dev/go/94_lucee_resource.go b/generated/dev/go/94_lucee_resource.go
new file mode 100644
index 0000000..d78129f
--- /dev/null
+++ b/generated/dev/go/94_lucee_resource.go
@@ -0,0 +1,449 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: lucee
+// Service ID: 94
+
+var _ resource.Resource = &LuceeResource{}
+var _ resource.ResourceWithModifyPlan = &LuceeResource{}
+var _ resource.ResourceWithImportState = &LuceeResource{}
+
+type LuceeResource struct {
+ client *core.UniversalClient
+}
+
+// LuceeAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type LuceeAccessConfigurationModel struct {
+ Domain types.String `tfsdk:"domain" json:"domain"`
+}
+
+// LuceeAppConfigurationModel — вложенная модель для map-fixed параметра appConfiguration.
+type LuceeAppConfigurationModel struct {
+ Version types.String `tfsdk:"version" json:"version"`
+ GitPath types.String `tfsdk:"git_path" json:"gitPath"`
+ HealthPath types.String `tfsdk:"health_path" json:"healthPath"`
+}
+
+// LuceeClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type LuceeClusterConfigurationModel struct {
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+}
+
+// LuceeStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type LuceeStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+}
+
+type LuceeModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // git_revision: при изменении вызывает redeploy вместо modify.
+ // Опциональное поле — если не задано, modify работает как обычно.
+ GitRevision types.String `tfsdk:"git_revision"`
+ AccessConfiguration *LuceeAccessConfigurationModel `tfsdk:"access_configuration"`
+ AppConfiguration *LuceeAppConfigurationModel `tfsdk:"app_configuration"`
+ ClusterConfiguration *LuceeClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ JsonEnv types.String `tfsdk:"json_env"`
+ StartupConfiguration *LuceeStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewLuceeResource() resource.Resource {
+ return &LuceeResource{}
+}
+
+func (r *LuceeResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_lucee"
+}
+
+func (r *LuceeResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("lucee-kctea8wd"), MarkdownDescription: "Если указан FQDN в формате: `myapp.example.com`, предварительно будет проверяться DNS-запись (А-запись **должна смотреть на IP Ingress кластера ресурсной платформы**)
Если указан в формате `myapp`, DNS-запись будет автоматически создана в служебной DNS-зоне"},
+ }},
+ "app_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("6.0")},
+ "git_path": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("https://github.com/xahys/testlucee"), MarkdownDescription: "Путь до репозитория приложения
Обязательно должен быть передан как URL: `https://....`
Должен быть сетевой доступ до URL для скачивания исходников"},
+ "health_path": schema.StringAttribute{Optional: true, MarkdownDescription: "Путь до healtcheck для запуска пода в Kubernetes, который отдаст 200 код
Пустое значение означает, что будет проверяться исключительно доступность приложения по порту"},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(200), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(256), MarkdownDescription: "Указывается в Megabytes"},
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ }},
+ "json_env": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("{}"), MarkdownDescription: "Передаётся в формате КЛЮЧ:ЗНАЧЕНИЕ"},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"},
+ }},
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // При изменении git_revision вызывается redeploy вместо modify.
+ // Если не задано — modify работает без изменений.
+ "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *LuceeResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *LuceeModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *LuceeModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan LuceeModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 1019: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1020: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64())}),
+ 1021: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Domain.ValueString())}),
+ 1022: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.HealthPath.ValueString())}),
+ 1023: resources_core.FormatString(config.JsonEnv),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 94, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *LuceeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data LuceeModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 94, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 1019: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1020: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64())}),
+ 1021: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Domain.ValueString())}),
+ 1022: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.HealthPath.ValueString())}),
+ 1023: resources_core.FormatString(data.JsonEnv),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 94, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 94, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *LuceeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state LuceeModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 94, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *LuceeResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan LuceeModel
+ var state LuceeModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.JsonEnv.IsNull() != state.JsonEnv.IsNull() || plan.JsonEnv.IsUnknown() != state.JsonEnv.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.JsonEnv.IsNull() && !plan.JsonEnv.IsUnknown() && !state.JsonEnv.IsNull() && !state.JsonEnv.IsUnknown() {
+ if !strings.EqualFold(plan.JsonEnv.ValueString(), state.JsonEnv.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // Проверяем изменился ли git_revision.
+ // Если да — вызываем redeploy вместо modify.
+ // git_revision = "" (не задано) → обычный modify.
+ redeployRequested := false
+ if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() {
+ if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() {
+ redeployRequested = true
+ }
+ }
+
+ if !hasServiceParamChanges && !redeployRequested {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ if redeployRequested {
+ // Вызываем redeploy — пустой params (операция не требует CFS-параметров)
+ if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ } else {
+ params := map[int]string{
+ 1024: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64())}),
+ 1025: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.HealthPath.ValueString())}),
+ 1026: resources_core.FormatString(plan.JsonEnv),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 94, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *LuceeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state LuceeModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *LuceeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *LuceeResource) 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
+}
diff --git a/generated/dev/go/95_nodejs_resource.go b/generated/dev/go/95_nodejs_resource.go
new file mode 100644
index 0000000..10900b8
--- /dev/null
+++ b/generated/dev/go/95_nodejs_resource.go
@@ -0,0 +1,449 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: nodejs
+// Service ID: 95
+
+var _ resource.Resource = &NodejsResource{}
+var _ resource.ResourceWithModifyPlan = &NodejsResource{}
+var _ resource.ResourceWithImportState = &NodejsResource{}
+
+type NodejsResource struct {
+ client *core.UniversalClient
+}
+
+// NodejsAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type NodejsAccessConfigurationModel struct {
+ Domain types.String `tfsdk:"domain" json:"domain"`
+}
+
+// NodejsAppConfigurationModel — вложенная модель для map-fixed параметра appConfiguration.
+type NodejsAppConfigurationModel struct {
+ Version types.String `tfsdk:"version" json:"version"`
+ GitPath types.String `tfsdk:"git_path" json:"gitPath"`
+ HealthPath types.String `tfsdk:"health_path" json:"healthPath"`
+}
+
+// NodejsClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type NodejsClusterConfigurationModel struct {
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+}
+
+// NodejsStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type NodejsStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+}
+
+type NodejsModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // git_revision: при изменении вызывает redeploy вместо modify.
+ // Опциональное поле — если не задано, modify работает как обычно.
+ GitRevision types.String `tfsdk:"git_revision"`
+ AccessConfiguration *NodejsAccessConfigurationModel `tfsdk:"access_configuration"`
+ AppConfiguration *NodejsAppConfigurationModel `tfsdk:"app_configuration"`
+ ClusterConfiguration *NodejsClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ JsonEnv types.String `tfsdk:"json_env"`
+ StartupConfiguration *NodejsStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewNodejsResource() resource.Resource {
+ return &NodejsResource{}
+}
+
+func (r *NodejsResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_nodejs"
+}
+
+func (r *NodejsResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("node-bd1l2h8o"), MarkdownDescription: "Если указан FQDN в формате: `myapp.example.com`, предварительно будет проверяться DNS-запись (А-запись **должна смотреть на IP Ingress кластера ресурсной платформы**)
Если указан в формате `myapp`, DNS-запись будет автоматически создана в служебной DNS-зоне"},
+ }},
+ "app_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("23")},
+ "git_path": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("https://github.com/xahys/testnode.git"), MarkdownDescription: "Путь до репозитория приложения
Обязательно должен быть передан как URL: `https://....`
Должен быть сетевой доступ до URL для скачивания исходников"},
+ "health_path": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("1"), MarkdownDescription: "Путь до healtcheck для запуска пода в Kubernetes, который отдаст 200 код
Пустое значение означает, что будет проверяться исключительно доступность приложения по порту"},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(200), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(256), MarkdownDescription: "Указывается в Megabytes"},
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ }},
+ "json_env": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("{}"), MarkdownDescription: "Передаётся в формате КЛЮЧ:ЗНАЧЕНИЕ"},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"},
+ }},
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // При изменении git_revision вызывается redeploy вместо modify.
+ // Если не задано — modify работает без изменений.
+ "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *NodejsResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *NodejsModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *NodejsModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan NodejsModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 1028: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1029: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64())}),
+ 1030: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Domain.ValueString())}),
+ 1031: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.HealthPath.ValueString())}),
+ 1032: resources_core.FormatString(config.JsonEnv),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 95, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *NodejsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data NodejsModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 95, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 1028: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1029: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64())}),
+ 1030: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Domain.ValueString())}),
+ 1031: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.HealthPath.ValueString())}),
+ 1032: resources_core.FormatString(data.JsonEnv),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 95, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 95, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *NodejsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state NodejsModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 95, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *NodejsResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan NodejsModel
+ var state NodejsModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.JsonEnv.IsNull() != state.JsonEnv.IsNull() || plan.JsonEnv.IsUnknown() != state.JsonEnv.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.JsonEnv.IsNull() && !plan.JsonEnv.IsUnknown() && !state.JsonEnv.IsNull() && !state.JsonEnv.IsUnknown() {
+ if !strings.EqualFold(plan.JsonEnv.ValueString(), state.JsonEnv.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // Проверяем изменился ли git_revision.
+ // Если да — вызываем redeploy вместо modify.
+ // git_revision = "" (не задано) → обычный modify.
+ redeployRequested := false
+ if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() {
+ if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() {
+ redeployRequested = true
+ }
+ }
+
+ if !hasServiceParamChanges && !redeployRequested {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ if redeployRequested {
+ // Вызываем redeploy — пустой params (операция не требует CFS-параметров)
+ if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ } else {
+ params := map[int]string{
+ 1033: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64())}),
+ 1034: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.HealthPath.ValueString())}),
+ 1035: resources_core.FormatString(plan.JsonEnv),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 95, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *NodejsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state NodejsModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *NodejsResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *NodejsResource) 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
+}
diff --git a/generated/dev/go/96_pgadmin_resource.go b/generated/dev/go/96_pgadmin_resource.go
new file mode 100644
index 0000000..aac4518
--- /dev/null
+++ b/generated/dev/go/96_pgadmin_resource.go
@@ -0,0 +1,515 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: pgadmin
+// Service ID: 96
+
+var _ resource.Resource = &PgadminResource{}
+var _ resource.ResourceWithModifyPlan = &PgadminResource{}
+var _ resource.ResourceWithImportState = &PgadminResource{}
+
+type PgadminResource struct {
+ client *core.UniversalClient
+}
+
+type PgadminModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // git_revision: при изменении вызывает redeploy вместо modify.
+ // Опциональное поле — если не задано, modify работает как обычно.
+ GitRevision types.String `tfsdk:"git_revision"`
+ Domain types.String `tfsdk:"domain"`
+ Login types.String `tfsdk:"login"`
+ Password types.String `tfsdk:"password"`
+ ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"`
+ ResourceDisk types.Int64 `tfsdk:"resource_disk"`
+ ResourceMemory types.Int64 `tfsdk:"resource_memory"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewPgadminResource() resource.Resource {
+ return &PgadminResource{}
+}
+
+func (r *PgadminResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_pgadmin"
+}
+
+func (r *PgadminResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("pgadmin-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."},
+ "login": schema.StringAttribute{Required: true, MarkdownDescription: "Логин для подключения к pgAdmin, в формате email (login@email.net) Не изменяется после создания."},
+ "password": schema.StringAttribute{Required: true, MarkdownDescription: "Пароль для подключения к pgAdmin Не изменяется после создания.", Sensitive: true},
+ "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Квота ядра пода (milicores). 1000Mili = 1 Ядро"},
+ "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Квота диска (Указывается в GIGAbytes)"},
+ "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Квота памяти пода (MBytes)"},
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // При изменении git_revision вызывается redeploy вместо modify.
+ // Если не задано — modify работает без изменений.
+ "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *PgadminResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *PgadminModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *PgadminModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan PgadminModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.Login.IsNull() && !plan.Login.IsUnknown() && !state.Login.IsNull() && !state.Login.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Login.ValueString(), state.Login.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить login", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.Password.IsNull() && !plan.Password.IsUnknown() && !state.Password.IsNull() && !state.Password.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Password.ValueString(), state.Password.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить password", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.Login.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "login is required.")
+ return
+ }
+ if config.Login.IsUnknown() {
+ return
+ }
+ if config.Password.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "password is required.")
+ return
+ }
+ if config.Password.IsUnknown() {
+ return
+ }
+ if config.ResourceRealm.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+ if config.ResourceRealm.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 164: resources_core.FormatString(config.Domain),
+ 165: resources_core.FormatInt64(config.ResourceCPU),
+ 166: resources_core.FormatInt64(config.ResourceMemory),
+ 167: resources_core.FormatInt64(config.ResourceDisk),
+ 169: resources_core.FormatString(config.ResourceRealm),
+ 170: resources_core.FormatString(config.Login),
+ 171: resources_core.FormatString(config.Password),
+ }
+ desiredDomain := ""
+ if !config.Domain.IsNull() && !config.Domain.IsUnknown() {
+ desiredDomain = config.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 96, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *PgadminResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data PgadminModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.Login.IsNull() || data.Login.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "login is required.")
+ return
+ }
+ if data.Password.IsNull() || data.Password.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "password is required.")
+ return
+ }
+ if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ if !data.Domain.IsNull() && !data.Domain.IsUnknown() {
+ desiredDomain = data.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 96, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 164: resources_core.FormatString(data.Domain),
+ 165: resources_core.FormatInt64(data.ResourceCPU),
+ 166: resources_core.FormatInt64(data.ResourceMemory),
+ 167: resources_core.FormatInt64(data.ResourceDisk),
+ 169: resources_core.FormatString(data.ResourceRealm),
+ 170: resources_core.FormatString(data.Login),
+ 171: resources_core.FormatString(data.Password),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 96, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 96, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "login", Field: "Login", Type: "string"},
+ {Code: "password", Field: "Password", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *PgadminResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state PgadminModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 96, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "login", Field: "Login", Type: "string"},
+ {Code: "password", Field: "Password", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *PgadminResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan PgadminModel
+ var state PgadminModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() {
+ if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() {
+ if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceDisk.IsNull() != state.ResourceDisk.IsNull() || plan.ResourceDisk.IsUnknown() != state.ResourceDisk.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() {
+ if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // Проверяем изменился ли git_revision.
+ // Если да — вызываем redeploy вместо modify.
+ // git_revision = "" (не задано) → обычный modify.
+ redeployRequested := false
+ if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() {
+ if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() {
+ redeployRequested = true
+ }
+ }
+
+ if !hasServiceParamChanges && !redeployRequested {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ if redeployRequested {
+ // Вызываем redeploy — пустой params (операция не требует CFS-параметров)
+ if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ } else {
+ params := map[int]string{
+ 172: resources_core.FormatInt64(plan.ResourceCPU),
+ 173: resources_core.FormatInt64(plan.ResourceMemory),
+ 174: resources_core.FormatInt64(plan.ResourceDisk),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 96, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "login", Field: "Login", Type: "string"},
+ {Code: "password", Field: "Password", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *PgadminResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state PgadminModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *PgadminResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *PgadminResource) 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
+}
diff --git a/generated/dev/go/97_nodered_resource.go b/generated/dev/go/97_nodered_resource.go
new file mode 100644
index 0000000..7b00a75
--- /dev/null
+++ b/generated/dev/go/97_nodered_resource.go
@@ -0,0 +1,431 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: nodered
+// Service ID: 97
+
+var _ resource.Resource = &NoderedResource{}
+var _ resource.ResourceWithModifyPlan = &NoderedResource{}
+var _ resource.ResourceWithImportState = &NoderedResource{}
+
+type NoderedResource struct {
+ client *core.UniversalClient
+}
+
+type NoderedModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ Domain types.String `tfsdk:"domain"`
+ ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"`
+ ResourceDisk types.Int64 `tfsdk:"resource_disk"`
+ ResourceInstances types.Int64 `tfsdk:"resource_instances"`
+ ResourceMemory types.Int64 `tfsdk:"resource_memory"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewNoderedResource() resource.Resource {
+ return &NoderedResource{}
+}
+
+func (r *NoderedResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_nodered"
+}
+
+func (r *NoderedResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("nodered-1"), MarkdownDescription: "Наименование поддомена, будет добавлено в k8stemplate.dev.nubes.ru Не изменяется после создания."},
+ "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Квота ядра пода (Указывается в mili). 1000Mili = 1 Ядро Не изменяется после создания."},
+ "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Квота диска (Указывается в GIGAbytes) Не изменяется после создания."},
+ "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов Не изменяется после создания."},
+ "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Квота памяти пода (Указывается в Megabytes) Не изменяется после создания."},
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *NoderedResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *NoderedModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *NoderedModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan NoderedModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() {
+ if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_c_p_u", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() {
+ if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_disk", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() {
+ if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_instances", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() {
+ if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_memory", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.ResourceRealm.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+ if config.ResourceRealm.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 193: resources_core.FormatInt64(config.ResourceCPU),
+ 194: resources_core.FormatInt64(config.ResourceMemory),
+ 195: resources_core.FormatInt64(config.ResourceDisk),
+ 196: resources_core.FormatString(config.ResourceRealm),
+ 197: resources_core.FormatString(config.Domain),
+ 323: resources_core.FormatInt64(config.ResourceInstances),
+ }
+ desiredDomain := ""
+ if !config.Domain.IsNull() && !config.Domain.IsUnknown() {
+ desiredDomain = config.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 97, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *NoderedResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data NoderedModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ if !data.Domain.IsNull() && !data.Domain.IsUnknown() {
+ desiredDomain = data.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 97, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 193: resources_core.FormatInt64(data.ResourceCPU),
+ 194: resources_core.FormatInt64(data.ResourceMemory),
+ 195: resources_core.FormatInt64(data.ResourceDisk),
+ 196: resources_core.FormatString(data.ResourceRealm),
+ 197: resources_core.FormatString(data.Domain),
+ 323: resources_core.FormatInt64(data.ResourceInstances),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 97, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 97, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *NoderedResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state NoderedModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 97, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *NoderedResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan NoderedModel
+ var state NoderedModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 97, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *NoderedResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state NoderedModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *NoderedResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *NoderedResource) 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
+}
diff --git a/generated/dev/go/98_http_resource.go b/generated/dev/go/98_http_resource.go
new file mode 100644
index 0000000..5d287fa
--- /dev/null
+++ b/generated/dev/go/98_http_resource.go
@@ -0,0 +1,509 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: http
+// Service ID: 98
+
+var _ resource.Resource = &HttpResource{}
+var _ resource.ResourceWithModifyPlan = &HttpResource{}
+var _ resource.ResourceWithImportState = &HttpResource{}
+
+type HttpResource struct {
+ client *core.UniversalClient
+}
+
+type HttpModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // git_revision: при изменении вызывает redeploy вместо modify.
+ // Опциональное поле — если не задано, modify работает как обычно.
+ GitRevision types.String `tfsdk:"git_revision"`
+ Domain types.String `tfsdk:"domain"`
+ IngressAnnotations types.String `tfsdk:"ingress_annotations"`
+ JsonEnv types.String `tfsdk:"json_env"`
+ RegistryPath types.String `tfsdk:"registry_path"`
+ ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"`
+ ResourceInstances types.Int64 `tfsdk:"resource_instances"`
+ ResourceMemory types.Int64 `tfsdk:"resource_memory"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewHttpResource() resource.Resource {
+ return &HttpResource{}
+}
+
+func (r *HttpResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_http"
+}
+
+func (r *HttpResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("http-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."},
+ "ingress_annotations": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("{}"), MarkdownDescription: "Формат ключ/значение
Поддерживает пока простые аннотации Не изменяется после создания."},
+ "json_env": schema.StringAttribute{Optional: true, MarkdownDescription: "Массив переменных, будут переданы в env приложения Не изменяется после создания."},
+ "registry_path": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("nexus-sa.tst.nubes.ru/docker-nubes/jolt"), MarkdownDescription: "Путь до image, который будет установлен как deployment Не изменяется после создания."},
+ "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100), MarkdownDescription: "Квота ядра пода (Указывается в mili)"},
+ "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов"},
+ "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(200), MarkdownDescription: "Квота памяти пода (Указывается в Megabytes)"},
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // При изменении git_revision вызывается redeploy вместо modify.
+ // Если не задано — modify работает без изменений.
+ "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *HttpResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *HttpModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *HttpModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan HttpModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.IngressAnnotations.IsNull() && !plan.IngressAnnotations.IsUnknown() && !state.IngressAnnotations.IsNull() && !state.IngressAnnotations.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.IngressAnnotations.ValueString(), state.IngressAnnotations.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить ingress_annotations", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.JsonEnv.IsNull() && !plan.JsonEnv.IsUnknown() && !state.JsonEnv.IsNull() && !state.JsonEnv.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.JsonEnv.ValueString(), state.JsonEnv.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить json_env", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.RegistryPath.IsNull() && !plan.RegistryPath.IsUnknown() && !state.RegistryPath.IsNull() && !state.RegistryPath.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.RegistryPath.ValueString(), state.RegistryPath.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить registry_path", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.ResourceRealm.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+ if config.ResourceRealm.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 212: resources_core.FormatString(config.Domain),
+ 213: resources_core.FormatInt64(config.ResourceCPU),
+ 214: resources_core.FormatInt64(config.ResourceMemory),
+ 215: resources_core.FormatInt64(config.ResourceInstances),
+ 216: resources_core.FormatString(config.ResourceRealm),
+ 217: resources_core.FormatString(config.RegistryPath),
+ 241: resources_core.FormatString(config.JsonEnv),
+ 782: resources_core.FormatString(config.IngressAnnotations),
+ }
+ desiredDomain := ""
+ if !config.Domain.IsNull() && !config.Domain.IsUnknown() {
+ desiredDomain = config.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 98, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *HttpResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data HttpModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ if !data.Domain.IsNull() && !data.Domain.IsUnknown() {
+ desiredDomain = data.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 98, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 212: resources_core.FormatString(data.Domain),
+ 213: resources_core.FormatInt64(data.ResourceCPU),
+ 214: resources_core.FormatInt64(data.ResourceMemory),
+ 215: resources_core.FormatInt64(data.ResourceInstances),
+ 216: resources_core.FormatString(data.ResourceRealm),
+ 217: resources_core.FormatString(data.RegistryPath),
+ 241: resources_core.FormatString(data.JsonEnv),
+ 782: resources_core.FormatString(data.IngressAnnotations),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 98, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 98, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "ingressAnnotations", Field: "IngressAnnotations", Type: "string"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "registryPath", Field: "RegistryPath", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *HttpResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state HttpModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 98, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "ingressAnnotations", Field: "IngressAnnotations", Type: "string"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "registryPath", Field: "RegistryPath", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *HttpResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan HttpModel
+ var state HttpModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() {
+ if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() {
+ if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceInstances.IsNull() != state.ResourceInstances.IsNull() || plan.ResourceInstances.IsUnknown() != state.ResourceInstances.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() {
+ if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // Проверяем изменился ли git_revision.
+ // Если да — вызываем redeploy вместо modify.
+ // git_revision = "" (не задано) → обычный modify.
+ redeployRequested := false
+ if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() {
+ if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() {
+ redeployRequested = true
+ }
+ }
+
+ if !hasServiceParamChanges && !redeployRequested {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ if redeployRequested {
+ // Вызываем redeploy — пустой params (операция не требует CFS-параметров)
+ if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ } else {
+ params := map[int]string{
+ 218: resources_core.FormatInt64(plan.ResourceCPU),
+ 219: resources_core.FormatInt64(plan.ResourceMemory),
+ 220: resources_core.FormatInt64(plan.ResourceInstances),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 98, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "ingressAnnotations", Field: "IngressAnnotations", Type: "string"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "registryPath", Field: "RegistryPath", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *HttpResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state HttpModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *HttpResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *HttpResource) 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
+}
diff --git a/generated/dev/go/99_gitea_resource.go b/generated/dev/go/99_gitea_resource.go
new file mode 100644
index 0000000..97139b1
--- /dev/null
+++ b/generated/dev/go/99_gitea_resource.go
@@ -0,0 +1,579 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: gitea
+// Service ID: 99
+
+var _ resource.Resource = &GiteaResource{}
+var _ resource.ResourceWithModifyPlan = &GiteaResource{}
+var _ resource.ResourceWithImportState = &GiteaResource{}
+
+type GiteaResource struct {
+ client *core.UniversalClient
+}
+
+type GiteaModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ Domain types.String `tfsdk:"domain"`
+ IngressExtraAnnotations types.String `tfsdk:"ingress_extra_annotations"`
+ IniServerParams types.String `tfsdk:"ini_server_params"`
+ PsqlUid types.String `tfsdk:"psql_uid"`
+ ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"`
+ ResourceDisk types.Int64 `tfsdk:"resource_disk"`
+ ResourceInstances types.Int64 `tfsdk:"resource_instances"`
+ ResourceMemory types.Int64 `tfsdk:"resource_memory"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewGiteaResource() resource.Resource {
+ return &GiteaResource{}
+}
+
+func (r *GiteaResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_gitea"
+}
+
+func (r *GiteaResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("gitea-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."},
+ "ingress_extra_annotations": schema.StringAttribute{Optional: true},
+ "ini_server_params": schema.StringAttribute{Optional: true},
+ "psql_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID Услуги k8sPgsql. Пользователя techGitea, базу данных gitea создаёт автоматически Не изменяется после создания."},
+ "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Квота ядра пода (Указывается в mili). 1000Mili = 1 Ядро"},
+ "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Квота диска (Указывается в GIGAbytes)"},
+ "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов"},
+ "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Квота памяти пода (Указывается в Megabytes)"},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *GiteaResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *GiteaModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *GiteaModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan GiteaModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.PsqlUid.IsNull() && !plan.PsqlUid.IsUnknown() && !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.PsqlUid.ValueString(), state.PsqlUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить psql_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.PsqlUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "psql_uid is required.")
+ return
+ }
+ if config.PsqlUid.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.PsqlUid.IsNull() && !config.PsqlUid.IsUnknown() {
+ resolvedPsqlUid, err := r.client.ResolveRefSvcParamValue(ctx, 90, config.PsqlUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve psql_uid", err.Error())
+ } else if resolvedPsqlUid != "" && resolvedPsqlUid != config.PsqlUid.ValueString() {
+ config.PsqlUid = types.StringValue(resolvedPsqlUid)
+ }
+ }
+ params := map[int]string{
+ 232: resources_core.FormatInt64(config.ResourceCPU),
+ 233: resources_core.FormatInt64(config.ResourceMemory),
+ 234: resources_core.FormatInt64(config.ResourceDisk),
+ 235: resources_core.FormatInt64(config.ResourceInstances),
+ 237: resources_core.FormatString(config.Domain),
+ 238: resources_core.FormatString(config.PsqlUid),
+ 818: resources_core.FormatString(config.IngressExtraAnnotations),
+ 821: resources_core.FormatString(config.IniServerParams),
+ }
+ desiredDomain := ""
+ if !config.Domain.IsNull() && !config.Domain.IsUnknown() {
+ desiredDomain = config.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 99, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *GiteaResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data GiteaModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.PsqlUid.IsNull() || data.PsqlUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "psql_uid is required.")
+ return
+ }
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalPsqlUid := data.PsqlUid
+ if !data.PsqlUid.IsNull() && !data.PsqlUid.IsUnknown() {
+ resolvedPsqlUid, err := r.client.ResolveRefSvcParamValue(ctx, 90, data.PsqlUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedPsqlUid != "" && resolvedPsqlUid != data.PsqlUid.ValueString() {
+ data.PsqlUid = types.StringValue(resolvedPsqlUid)
+ }
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ if !data.Domain.IsNull() && !data.Domain.IsUnknown() {
+ desiredDomain = data.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 99, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 232: resources_core.FormatInt64(data.ResourceCPU),
+ 233: resources_core.FormatInt64(data.ResourceMemory),
+ 234: resources_core.FormatInt64(data.ResourceDisk),
+ 235: resources_core.FormatInt64(data.ResourceInstances),
+ 237: resources_core.FormatString(data.Domain),
+ 238: resources_core.FormatString(data.PsqlUid),
+ 818: resources_core.FormatString(data.IngressExtraAnnotations),
+ 821: resources_core.FormatString(data.IniServerParams),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 99, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 99, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "ingressExtraAnnotations", Field: "IngressExtraAnnotations", Type: "string"},
+ {Code: "iniServerParams", Field: "IniServerParams", Type: "string"},
+ {Code: "psqlUid", Field: "PsqlUid", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() {
+ resolvedPsqlUid, err := r.client.ResolveRefSvcParamValue(ctx, 90, state.PsqlUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve psql_uid", err.Error())
+ } else if resolvedPsqlUid != "" && resolvedPsqlUid != state.PsqlUid.ValueString() {
+ state.PsqlUid = types.StringValue(resolvedPsqlUid)
+ }
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+ if !originalPsqlUid.IsNull() && !originalPsqlUid.IsUnknown() && !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() {
+ if strings.EqualFold(state.PsqlUid.ValueString(), originalPsqlUid.ValueString()) {
+ state.PsqlUid = originalPsqlUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *GiteaResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state GiteaModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 99, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "ingressExtraAnnotations", Field: "IngressExtraAnnotations", Type: "string"},
+ {Code: "iniServerParams", Field: "IniServerParams", Type: "string"},
+ {Code: "psqlUid", Field: "PsqlUid", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !newState.PsqlUid.IsNull() && !newState.PsqlUid.IsUnknown() {
+ resolvedPsqlUid, err := r.client.ResolveRefSvcParamValue(ctx, 90, newState.PsqlUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve psql_uid", err.Error())
+ } else if resolvedPsqlUid != "" && resolvedPsqlUid != newState.PsqlUid.ValueString() {
+ newState.PsqlUid = types.StringValue(resolvedPsqlUid)
+ }
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+ if !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() && !newState.PsqlUid.IsNull() && !newState.PsqlUid.IsUnknown() {
+ if strings.EqualFold(newState.PsqlUid.ValueString(), state.PsqlUid.ValueString()) {
+ newState.PsqlUid = state.PsqlUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *GiteaResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan GiteaModel
+ var state GiteaModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() {
+ if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() {
+ if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceDisk.IsNull() != state.ResourceDisk.IsNull() || plan.ResourceDisk.IsUnknown() != state.ResourceDisk.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() {
+ if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceInstances.IsNull() != state.ResourceInstances.IsNull() || plan.ResourceInstances.IsUnknown() != state.ResourceInstances.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() {
+ if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.IngressExtraAnnotations.IsNull() != state.IngressExtraAnnotations.IsNull() || plan.IngressExtraAnnotations.IsUnknown() != state.IngressExtraAnnotations.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.IngressExtraAnnotations.IsNull() && !plan.IngressExtraAnnotations.IsUnknown() && !state.IngressExtraAnnotations.IsNull() && !state.IngressExtraAnnotations.IsUnknown() {
+ if !strings.EqualFold(plan.IngressExtraAnnotations.ValueString(), state.IngressExtraAnnotations.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.IniServerParams.IsNull() != state.IniServerParams.IsNull() || plan.IniServerParams.IsUnknown() != state.IniServerParams.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.IniServerParams.IsNull() && !plan.IniServerParams.IsUnknown() && !state.IniServerParams.IsNull() && !state.IniServerParams.IsUnknown() {
+ if !strings.EqualFold(plan.IniServerParams.ValueString(), state.IniServerParams.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 259: resources_core.FormatInt64(plan.ResourceCPU),
+ 260: resources_core.FormatInt64(plan.ResourceMemory),
+ 261: resources_core.FormatInt64(plan.ResourceDisk),
+ 262: resources_core.FormatInt64(plan.ResourceInstances),
+ 819: resources_core.FormatString(plan.IngressExtraAnnotations),
+ 820: resources_core.FormatString(plan.IniServerParams),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 99, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "ingressExtraAnnotations", Field: "IngressExtraAnnotations", Type: "string"},
+ {Code: "iniServerParams", Field: "IniServerParams", Type: "string"},
+ {Code: "psqlUid", Field: "PsqlUid", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() {
+ resolvedPsqlUid, err := r.client.ResolveRefSvcParamValue(ctx, 90, state.PsqlUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve psql_uid", err.Error())
+ } else if resolvedPsqlUid != "" && resolvedPsqlUid != state.PsqlUid.ValueString() {
+ state.PsqlUid = types.StringValue(resolvedPsqlUid)
+ }
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+ if !plan.PsqlUid.IsNull() && !plan.PsqlUid.IsUnknown() && !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() {
+ if strings.EqualFold(state.PsqlUid.ValueString(), plan.PsqlUid.ValueString()) {
+ state.PsqlUid = plan.PsqlUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *GiteaResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state GiteaModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *GiteaResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *GiteaResource) 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
+}
diff --git a/generated/dev/go/registry.go b/generated/dev/go/registry.go
new file mode 100644
index 0000000..893cf0d
--- /dev/null
+++ b/generated/dev/go/registry.go
@@ -0,0 +1,81 @@
+package resources_gen
+
+import "github.com/hashicorp/terraform-plugin-framework/resource"
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+func AllResources() []func() resource.Resource {
+ return []func() resource.Resource{
+ NewAkhqResource,
+ NewClickhouseResource,
+ NewDnsrecordResource,
+ NewDnszoneResource,
+ NewDummyResource,
+ NewFlaskResource,
+ NewGiteaResource,
+ NewHarborResource,
+ NewHttpResource,
+ NewK8sGoResource,
+ NewK8sOpenbaoResource,
+ NewK8sSthutrvalClusterResource,
+ NewK8sVeleroResource,
+ NewK8sZitiControllerResource,
+ NewK8svalkeyResource,
+ NewKafkaResource,
+ NewLlmAiResource,
+ NewLuceeResource,
+ NewMariadbResource,
+ NewMongodbResource,
+ NewNextcloudResource,
+ NewNifiResource,
+ NewNodejsResource,
+ NewNoderedResource,
+ NewOpenwhiskResource,
+ NewPgadminResource,
+ NewPostgresResource,
+ NewRabbitmqResource,
+ NewRedisResource,
+ NewS3Resource,
+ NewS3bucketResource,
+ NewSupersetResource,
+ NewTemplateResource,
+ NewTenantResource,
+ NewValoTenantResource,
+ NewVappResource,
+ NewVcComplexResource,
+ NewVcMgmtSthutrvalClusterResource,
+ NewVcNatResource,
+ NewVcNsxtResource,
+ NewVcOrgResource,
+ NewVcOrgSaasResource,
+ NewVcVdcResource,
+ NewVcVdcGroupResource,
+ NewVcVmResource,
+ NewVcVmV2Resource,
+ NewVcVmV3Resource,
+ NewVcexternalipResource,
+ NewVmpostgreResource,
+ NewZonesV2Resource,
+ NewClickhouseDatabaseResource,
+ NewClickhouseUserResource,
+ NewK8sOpenbaoKvResource,
+ NewK8sOpenbaoPolicyResource,
+ NewK8sOpenbaoSecretsResource,
+ NewK8sSthutrvalClusterUserResource,
+ NewK8svalkeyUserResource,
+ NewKafkaTopicResource,
+ NewKafkaUserResource,
+ NewLlmAiSecretsResource,
+ NewLlmAiTeamResource,
+ NewMariadbDatabaseResource,
+ NewMariadbUserResource,
+ NewMongodbUserResource,
+ NewNextcloudBackupResource,
+ NewPostgresBackupResource,
+ NewPostgresDatabaseResource,
+ NewPostgresUserResource,
+ NewS3SubUserResource,
+ NewVcOrgUserResource,
+ NewVcVdcGroupVdcResource,
+ NewZonesV2RecordResource,
+ }
+}
diff --git a/generated/prod/go/100_openwhisk_resource.go b/generated/prod/go/100_openwhisk_resource.go
new file mode 100644
index 0000000..fa8337a
--- /dev/null
+++ b/generated/prod/go/100_openwhisk_resource.go
@@ -0,0 +1,355 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: openwhisk
+// Service ID: 100
+
+var _ resource.Resource = &OpenwhiskResource{}
+var _ resource.ResourceWithModifyPlan = &OpenwhiskResource{}
+var _ resource.ResourceWithImportState = &OpenwhiskResource{}
+
+type OpenwhiskResource struct {
+ client *core.UniversalClient
+}
+
+type OpenwhiskModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ Username types.String `tfsdk:"username"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewOpenwhiskResource() resource.Resource {
+ return &OpenwhiskResource{}
+}
+
+func (r *OpenwhiskResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_openwhisk"
+}
+
+func (r *OpenwhiskResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "username": schema.StringAttribute{Required: true, MarkdownDescription: "Имя пользователя Openwhisk Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *OpenwhiskResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *OpenwhiskModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *OpenwhiskModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan OpenwhiskModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.Username.IsNull() && !plan.Username.IsUnknown() && !state.Username.IsNull() && !state.Username.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Username.ValueString(), state.Username.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить username", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.Username.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "username is required.")
+ return
+ }
+ if config.Username.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 64: resources_core.FormatString(config.Username),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 100, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *OpenwhiskResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data OpenwhiskModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.Username.IsNull() || data.Username.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "username is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 100, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 64: resources_core.FormatString(data.Username),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 100, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 100, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "username", Field: "Username", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *OpenwhiskResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state OpenwhiskModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 100, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "username", Field: "Username", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *OpenwhiskResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan OpenwhiskModel
+ var state OpenwhiskModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 100, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "username", Field: "Username", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *OpenwhiskResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state OpenwhiskModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *OpenwhiskResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *OpenwhiskResource) 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
+}
diff --git a/generated/prod/go/109_zones_v2_record_resource.go b/generated/prod/go/109_zones_v2_record_resource.go
new file mode 100644
index 0000000..b875e09
--- /dev/null
+++ b/generated/prod/go/109_zones_v2_record_resource.go
@@ -0,0 +1,366 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: zones_v2
+// Subresource: record
+
+var _ resource.Resource = &ZonesV2RecordResource{}
+
+// ZonesV2RecordResource manages a subresource via operations.
+type ZonesV2RecordResource struct {
+ client *core.UniversalClient
+}
+
+type ZonesV2RecordModel struct {
+ ID types.String `tfsdk:"id"`
+ ZonesV2ID types.String `tfsdk:"zones_v2_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"`
+ RecordInput types.String `tfsdk:"record_input"`
+ RecordName types.String `tfsdk:"record_name"`
+ RecordTTL types.Int64 `tfsdk:"record_t_t_l"`
+ RecordType types.String `tfsdk:"record_type"`
+}
+
+func NewZonesV2RecordResource() resource.Resource {
+ return &ZonesV2RecordResource{}
+}
+
+func (r *ZonesV2RecordResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_zones_v2_record"
+}
+
+func (r *ZonesV2RecordResource) 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()}},
+ "zones_v2_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."},
+ "record_input": schema.StringAttribute{Required: true, MarkdownDescription: "Валидация производится только после запуска операции. Если при указанном типе будет указано некорректно значение, это будет описано в логе ошибки операции"},
+ "record_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя зоны в конце проставляется автоматически Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "record_t_t_l": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(3600), MarkdownDescription: "Time-to-Leave записи. Раз в какое время запись будет обновлена"},
+ "record_type": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("A")},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *ZonesV2RecordResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan ZonesV2RecordModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if plan.RecordInput.IsNull() || plan.RecordInput.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "record_input is required.")
+ return
+ }
+ if plan.RecordName.IsNull() || plan.RecordName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "record_name is required.")
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.ZonesV2ID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "recordName": resources_core.FormatString(plan.RecordName),
+ }
+ identityCodes := []string{
+ "recordName",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("record", 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, "record", 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{
+ "recordName": resources_core.FormatString(plan.RecordName),
+ "recordTTL": resources_core.FormatInt64(plan.RecordTTL),
+ "recordType": resources_core.FormatString(plan.RecordType),
+ "recordInput": resources_core.FormatString(plan.RecordInput),
+ })
+
+ 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, "create_record", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_record", 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, "record", 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, "record", 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, "record", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *ZonesV2RecordResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state ZonesV2RecordModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *ZonesV2RecordResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan ZonesV2RecordModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "modify_record" == "" {
+ var state ZonesV2RecordModel
+ 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.ZonesV2ID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{
+ "recordName": resources_core.FormatString(plan.RecordName),
+ "recordTTL": resources_core.FormatInt64(plan.RecordTTL),
+ "recordType": resources_core.FormatString(plan.RecordType),
+ "recordInput": resources_core.FormatString(plan.RecordInput),
+ })
+
+ idParams := map[string]string{
+ "recordName": resources_core.FormatString(plan.RecordName),
+ }
+
+ 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, "modify_record", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "record", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *ZonesV2RecordResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state ZonesV2RecordModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_record" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.ZonesV2ID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "recordName": resources_core.FormatString(state.RecordName),
+ }
+ identityCodes := []string{
+ "recordName",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("record", 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{
+ "recordName": resources_core.FormatString(state.RecordName),
+ })
+
+ 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, "delete_record", 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 *ZonesV2RecordResource) 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
+}
diff --git a/generated/prod/go/109_zones_v2_resource.go b/generated/prod/go/109_zones_v2_resource.go
new file mode 100644
index 0000000..e15f4b2
--- /dev/null
+++ b/generated/prod/go/109_zones_v2_resource.go
@@ -0,0 +1,484 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: zones_v2
+// Service ID: 109
+
+var _ resource.Resource = &ZonesV2Resource{}
+var _ resource.ResourceWithModifyPlan = &ZonesV2Resource{}
+var _ resource.ResourceWithImportState = &ZonesV2Resource{}
+
+type ZonesV2Resource struct {
+ client *core.UniversalClient
+}
+
+type ZonesV2Model struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ Records types.String `tfsdk:"records"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ ZoneExpire types.Int64 `tfsdk:"zone_expire"`
+ ZoneKind types.String `tfsdk:"zone_kind"`
+ ZoneMastersForSlave types.String `tfsdk:"zone_masters_for_slave"`
+ ZoneName types.String `tfsdk:"zone_name"`
+ ZoneRefresh types.Int64 `tfsdk:"zone_refresh"`
+ ZoneRetry types.Int64 `tfsdk:"zone_retry"`
+ ZoneTTL types.Int64 `tfsdk:"zone_t_t_l"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewZonesV2Resource() resource.Resource {
+ return &ZonesV2Resource{}
+}
+
+func (r *ZonesV2Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_zones_v2"
+}
+
+func (r *ZonesV2Resource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "records": schema.StringAttribute{Required: true},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("powerdns"), MarkdownDescription: "Не изменяется после создания."},
+ "zone_expire": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(3600000), MarkdownDescription: "Время в течение которого вторичный DNS будет пытаться завершить синхронизацию зоны с первичным Не изменяется после создания."},
+ "zone_kind": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("NATIVE"), MarkdownDescription: "Тип зоны Не изменяется после создания."},
+ "zone_masters_for_slave": schema.StringAttribute{Optional: true, MarkdownDescription: "Использовать если выбран тип SLAVE. IP адреса в формате через пробел для настройки Master-серверов Не изменяется после создания."},
+ "zone_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя зоны Не изменяется после создания."},
+ "zone_refresh": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(86400), MarkdownDescription: "Время ожидания ответа вторичного DNS перед запросом SOA-записи с первичных серверов Не изменяется после создания."},
+ "zone_retry": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(7200), MarkdownDescription: "Время в течение которого сервер повторно попробует синхронизировать информацию с первичных DNS-серверов Не изменяется после создания."},
+ "zone_t_t_l": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(86400), MarkdownDescription: "Время, в течение которого информация будет кешироваться другими DNS-серверами Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *ZonesV2Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *ZonesV2Model
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *ZonesV2Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan ZonesV2Model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneExpire.IsNull() && !plan.ZoneExpire.IsUnknown() && !state.ZoneExpire.IsNull() && !state.ZoneExpire.IsUnknown() {
+ if plan.ZoneExpire.ValueInt64() != state.ZoneExpire.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить zone_expire", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneKind.IsNull() && !plan.ZoneKind.IsUnknown() && !state.ZoneKind.IsNull() && !state.ZoneKind.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneKind.ValueString(), state.ZoneKind.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_kind", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneMastersForSlave.IsNull() && !plan.ZoneMastersForSlave.IsUnknown() && !state.ZoneMastersForSlave.IsNull() && !state.ZoneMastersForSlave.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneMastersForSlave.ValueString(), state.ZoneMastersForSlave.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_masters_for_slave", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneName.IsNull() && !plan.ZoneName.IsUnknown() && !state.ZoneName.IsNull() && !state.ZoneName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneName.ValueString(), state.ZoneName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneRefresh.IsNull() && !plan.ZoneRefresh.IsUnknown() && !state.ZoneRefresh.IsNull() && !state.ZoneRefresh.IsUnknown() {
+ if plan.ZoneRefresh.ValueInt64() != state.ZoneRefresh.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить zone_refresh", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneRetry.IsNull() && !plan.ZoneRetry.IsUnknown() && !state.ZoneRetry.IsNull() && !state.ZoneRetry.IsUnknown() {
+ if plan.ZoneRetry.ValueInt64() != state.ZoneRetry.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить zone_retry", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneTTL.IsNull() && !plan.ZoneTTL.IsUnknown() && !state.ZoneTTL.IsNull() && !state.ZoneTTL.IsUnknown() {
+ if plan.ZoneTTL.ValueInt64() != state.ZoneTTL.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить zone_t_t_l", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.Records.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "records is required.")
+ return
+ }
+ if config.Records.IsUnknown() {
+ return
+ }
+ if config.ZoneName.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "zone_name is required.")
+ return
+ }
+ if config.ZoneName.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 761: resources_core.FormatString(config.ZoneName),
+ 762: resources_core.FormatString(config.ZoneKind),
+ 763: resources_core.FormatString(config.ZoneMastersForSlave),
+ 764: resources_core.FormatInt64(config.ZoneRefresh),
+ 765: resources_core.FormatInt64(config.ZoneRetry),
+ 766: resources_core.FormatInt64(config.ZoneExpire),
+ 767: resources_core.FormatInt64(config.ZoneTTL),
+ 768: resources_core.FormatString(config.ResourceRealm),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 109, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *ZonesV2Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data ZonesV2Model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.Records.IsNull() || data.Records.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "records is required.")
+ return
+ }
+ if data.ZoneName.IsNull() || data.ZoneName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "zone_name is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 109, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 761: resources_core.FormatString(data.ZoneName),
+ 762: resources_core.FormatString(data.ZoneKind),
+ 763: resources_core.FormatString(data.ZoneMastersForSlave),
+ 764: resources_core.FormatInt64(data.ZoneRefresh),
+ 765: resources_core.FormatInt64(data.ZoneRetry),
+ 766: resources_core.FormatInt64(data.ZoneExpire),
+ 767: resources_core.FormatInt64(data.ZoneTTL),
+ 768: resources_core.FormatString(data.ResourceRealm),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 109, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 109, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "records", Field: "Records", Type: "array-map-fixed"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "zoneExpire", Field: "ZoneExpire", Type: "int64"},
+ {Code: "zoneKind", Field: "ZoneKind", Type: "string"},
+ {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"},
+ {Code: "zoneName", Field: "ZoneName", Type: "string"},
+ {Code: "zoneRefresh", Field: "ZoneRefresh", Type: "int64"},
+ {Code: "zoneRetry", Field: "ZoneRetry", Type: "int64"},
+ {Code: "zoneTTL", Field: "ZoneTTL", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *ZonesV2Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state ZonesV2Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 109, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "records", Field: "Records", Type: "array-map-fixed"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "zoneExpire", Field: "ZoneExpire", Type: "int64"},
+ {Code: "zoneKind", Field: "ZoneKind", Type: "string"},
+ {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"},
+ {Code: "zoneName", Field: "ZoneName", Type: "string"},
+ {Code: "zoneRefresh", Field: "ZoneRefresh", Type: "int64"},
+ {Code: "zoneRetry", Field: "ZoneRetry", Type: "int64"},
+ {Code: "zoneTTL", Field: "ZoneTTL", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *ZonesV2Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan ZonesV2Model
+ var state ZonesV2Model
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.Records.IsNull() != state.Records.IsNull() || plan.Records.IsUnknown() != state.Records.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.Records.IsNull() && !plan.Records.IsUnknown() && !state.Records.IsNull() && !state.Records.IsUnknown() {
+ if !strings.EqualFold(plan.Records.ValueString(), state.Records.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 769: resources_core.FormatString(plan.Records),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 109, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "records", Field: "Records", Type: "array-map-fixed"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "zoneExpire", Field: "ZoneExpire", Type: "int64"},
+ {Code: "zoneKind", Field: "ZoneKind", Type: "string"},
+ {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"},
+ {Code: "zoneName", Field: "ZoneName", Type: "string"},
+ {Code: "zoneRefresh", Field: "ZoneRefresh", Type: "int64"},
+ {Code: "zoneRetry", Field: "ZoneRetry", Type: "int64"},
+ {Code: "zoneTTL", Field: "ZoneTTL", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *ZonesV2Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state ZonesV2Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *ZonesV2Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *ZonesV2Resource) 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
+}
diff --git a/generated/prod/go/110_dnszone_resource.go b/generated/prod/go/110_dnszone_resource.go
new file mode 100644
index 0000000..fc3170b
--- /dev/null
+++ b/generated/prod/go/110_dnszone_resource.go
@@ -0,0 +1,495 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: dnszone
+// Service ID: 110
+
+var _ resource.Resource = &DnszoneResource{}
+var _ resource.ResourceWithModifyPlan = &DnszoneResource{}
+var _ resource.ResourceWithImportState = &DnszoneResource{}
+
+type DnszoneResource struct {
+ client *core.UniversalClient
+}
+
+type DnszoneModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ ZoneEmail types.String `tfsdk:"zone_email"`
+ ZoneKind types.String `tfsdk:"zone_kind"`
+ ZoneMastersForSlave types.String `tfsdk:"zone_masters_for_slave"`
+ ZoneName types.String `tfsdk:"zone_name"`
+ ZoneTimeToExpire types.String `tfsdk:"zone_time_to_expire"`
+ ZoneTimeToLive types.String `tfsdk:"zone_time_to_live"`
+ ZoneTimeToRefresh types.String `tfsdk:"zone_time_to_refresh"`
+ ZoneTimeToRetry types.String `tfsdk:"zone_time_to_retry"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewDnszoneResource() resource.Resource {
+ return &DnszoneResource{}
+}
+
+func (r *DnszoneResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_dnszone"
+}
+
+func (r *DnszoneResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("powerdns"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ "zone_email": schema.StringAttribute{Required: true, MarkdownDescription: "Контактный e-mail Не изменяется после создания."},
+ "zone_kind": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("NATIVE"), MarkdownDescription: "Тип зоны Не изменяется после создания."},
+ "zone_masters_for_slave": schema.StringAttribute{Optional: true, MarkdownDescription: "Использовать если выбран тип SLAVE. IP адреса в формате через пробел для настройки Master-серверов Не изменяется после создания."},
+ "zone_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя зоны Не изменяется после создания."},
+ "zone_time_to_expire": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("3600000"), MarkdownDescription: "Время в течение которого вторичный DNS будет пытаться завершить синхронизацию зоны с первичным Не изменяется после создания."},
+ "zone_time_to_live": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("86400"), MarkdownDescription: "Время, в течение которого информация будет кешироваться другими DNS-серверами Не изменяется после создания."},
+ "zone_time_to_refresh": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("86400"), MarkdownDescription: "Время ожидания ответа вторичного DNS перед запросом SOA-записи с первичных серверов Не изменяется после создания."},
+ "zone_time_to_retry": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("7200"), MarkdownDescription: "Время в течение которого сервер повторно попробует синхронизировать информацию с первичных DNS-серверов Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *DnszoneResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *DnszoneModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *DnszoneModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan DnszoneModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneEmail.IsNull() && !plan.ZoneEmail.IsUnknown() && !state.ZoneEmail.IsNull() && !state.ZoneEmail.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneEmail.ValueString(), state.ZoneEmail.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_email", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneKind.IsNull() && !plan.ZoneKind.IsUnknown() && !state.ZoneKind.IsNull() && !state.ZoneKind.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneKind.ValueString(), state.ZoneKind.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_kind", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneMastersForSlave.IsNull() && !plan.ZoneMastersForSlave.IsUnknown() && !state.ZoneMastersForSlave.IsNull() && !state.ZoneMastersForSlave.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneMastersForSlave.ValueString(), state.ZoneMastersForSlave.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_masters_for_slave", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneName.IsNull() && !plan.ZoneName.IsUnknown() && !state.ZoneName.IsNull() && !state.ZoneName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneName.ValueString(), state.ZoneName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneTimeToExpire.IsNull() && !plan.ZoneTimeToExpire.IsUnknown() && !state.ZoneTimeToExpire.IsNull() && !state.ZoneTimeToExpire.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneTimeToExpire.ValueString(), state.ZoneTimeToExpire.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_time_to_expire", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneTimeToLive.IsNull() && !plan.ZoneTimeToLive.IsUnknown() && !state.ZoneTimeToLive.IsNull() && !state.ZoneTimeToLive.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneTimeToLive.ValueString(), state.ZoneTimeToLive.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_time_to_live", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneTimeToRefresh.IsNull() && !plan.ZoneTimeToRefresh.IsUnknown() && !state.ZoneTimeToRefresh.IsNull() && !state.ZoneTimeToRefresh.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneTimeToRefresh.ValueString(), state.ZoneTimeToRefresh.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_time_to_refresh", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneTimeToRetry.IsNull() && !plan.ZoneTimeToRetry.IsUnknown() && !state.ZoneTimeToRetry.IsNull() && !state.ZoneTimeToRetry.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneTimeToRetry.ValueString(), state.ZoneTimeToRetry.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_time_to_retry", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.ZoneEmail.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "zone_email is required.")
+ return
+ }
+ if config.ZoneEmail.IsUnknown() {
+ return
+ }
+ if config.ZoneName.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "zone_name is required.")
+ return
+ }
+ if config.ZoneName.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 66: resources_core.FormatString(config.ZoneEmail),
+ 67: resources_core.FormatString(config.ZoneName),
+ 68: resources_core.FormatString(config.ZoneKind),
+ 69: resources_core.FormatString(config.ZoneMastersForSlave),
+ 70: resources_core.FormatString(config.ZoneTimeToRefresh),
+ 71: resources_core.FormatString(config.ZoneTimeToRetry),
+ 72: resources_core.FormatString(config.ZoneTimeToExpire),
+ 73: resources_core.FormatString(config.ZoneTimeToLive),
+ 284: resources_core.FormatString(config.ResourceRealm),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 110, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *DnszoneResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data DnszoneModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.ZoneEmail.IsNull() || data.ZoneEmail.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "zone_email is required.")
+ return
+ }
+ if data.ZoneName.IsNull() || data.ZoneName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "zone_name is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 110, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 66: resources_core.FormatString(data.ZoneEmail),
+ 67: resources_core.FormatString(data.ZoneName),
+ 68: resources_core.FormatString(data.ZoneKind),
+ 69: resources_core.FormatString(data.ZoneMastersForSlave),
+ 70: resources_core.FormatString(data.ZoneTimeToRefresh),
+ 71: resources_core.FormatString(data.ZoneTimeToRetry),
+ 72: resources_core.FormatString(data.ZoneTimeToExpire),
+ 73: resources_core.FormatString(data.ZoneTimeToLive),
+ 284: resources_core.FormatString(data.ResourceRealm),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 110, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 110, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "zoneEmail", Field: "ZoneEmail", Type: "string"},
+ {Code: "zoneKind", Field: "ZoneKind", Type: "string"},
+ {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"},
+ {Code: "zoneName", Field: "ZoneName", Type: "string"},
+ {Code: "zoneTimeToExpire", Field: "ZoneTimeToExpire", Type: "string"},
+ {Code: "zoneTimeToLive", Field: "ZoneTimeToLive", Type: "string"},
+ {Code: "zoneTimeToRefresh", Field: "ZoneTimeToRefresh", Type: "string"},
+ {Code: "zoneTimeToRetry", Field: "ZoneTimeToRetry", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *DnszoneResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state DnszoneModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 110, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "zoneEmail", Field: "ZoneEmail", Type: "string"},
+ {Code: "zoneKind", Field: "ZoneKind", Type: "string"},
+ {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"},
+ {Code: "zoneName", Field: "ZoneName", Type: "string"},
+ {Code: "zoneTimeToExpire", Field: "ZoneTimeToExpire", Type: "string"},
+ {Code: "zoneTimeToLive", Field: "ZoneTimeToLive", Type: "string"},
+ {Code: "zoneTimeToRefresh", Field: "ZoneTimeToRefresh", Type: "string"},
+ {Code: "zoneTimeToRetry", Field: "ZoneTimeToRetry", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *DnszoneResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan DnszoneModel
+ var state DnszoneModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 110, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "zoneEmail", Field: "ZoneEmail", Type: "string"},
+ {Code: "zoneKind", Field: "ZoneKind", Type: "string"},
+ {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"},
+ {Code: "zoneName", Field: "ZoneName", Type: "string"},
+ {Code: "zoneTimeToExpire", Field: "ZoneTimeToExpire", Type: "string"},
+ {Code: "zoneTimeToLive", Field: "ZoneTimeToLive", Type: "string"},
+ {Code: "zoneTimeToRefresh", Field: "ZoneTimeToRefresh", Type: "string"},
+ {Code: "zoneTimeToRetry", Field: "ZoneTimeToRetry", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *DnszoneResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state DnszoneModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *DnszoneResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *DnszoneResource) 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
+}
diff --git a/generated/prod/go/111_dnsrecord_resource.go b/generated/prod/go/111_dnsrecord_resource.go
new file mode 100644
index 0000000..e5bea0b
--- /dev/null
+++ b/generated/prod/go/111_dnsrecord_resource.go
@@ -0,0 +1,546 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: dnsrecord
+// Service ID: 111
+
+var _ resource.Resource = &DnsrecordResource{}
+var _ resource.ResourceWithModifyPlan = &DnsrecordResource{}
+var _ resource.ResourceWithImportState = &DnsrecordResource{}
+
+type DnsrecordResource struct {
+ client *core.UniversalClient
+}
+
+type DnsrecordModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ RecordInput types.String `tfsdk:"record_input"`
+ RecordName types.String `tfsdk:"record_name"`
+ RecordTTL types.Int64 `tfsdk:"record_t_t_l"`
+ RecordType types.String `tfsdk:"record_type"`
+ ZoneUid types.String `tfsdk:"zone_uid"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewDnsrecordResource() resource.Resource {
+ return &DnsrecordResource{}
+}
+
+func (r *DnsrecordResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_dnsrecord"
+}
+
+func (r *DnsrecordResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "record_input": schema.StringAttribute{Required: true, MarkdownDescription: "Значение. Необходима валидация, в зависимости от типа записи. Должно быть заменено на UUID-внешнийIP"},
+ "record_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя записи. Имя зоны в конце проставляется автоматически Не изменяется после создания."},
+ "record_t_t_l": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(3600), MarkdownDescription: "Time-to-Leave записи. Раз в какое время запись будет обновлена Не изменяется после создания."},
+ "record_type": schema.StringAttribute{Required: true, MarkdownDescription: "Тип DNS-записи Не изменяется после создания."},
+ "zone_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID экземпляра услуги DNS-зоны Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *DnsrecordResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *DnsrecordModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *DnsrecordModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan DnsrecordModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.RecordName.IsNull() && !plan.RecordName.IsUnknown() && !state.RecordName.IsNull() && !state.RecordName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.RecordName.ValueString(), state.RecordName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить record_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.RecordTTL.IsNull() && !plan.RecordTTL.IsUnknown() && !state.RecordTTL.IsNull() && !state.RecordTTL.IsUnknown() {
+ if plan.RecordTTL.ValueInt64() != state.RecordTTL.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить record_t_t_l", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.RecordType.IsNull() && !plan.RecordType.IsUnknown() && !state.RecordType.IsNull() && !state.RecordType.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.RecordType.ValueString(), state.RecordType.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить record_type", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneUid.IsNull() && !plan.ZoneUid.IsUnknown() && !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneUid.ValueString(), state.ZoneUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.RecordInput.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "record_input is required.")
+ return
+ }
+ if config.RecordInput.IsUnknown() {
+ return
+ }
+ if config.RecordName.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "record_name is required.")
+ return
+ }
+ if config.RecordName.IsUnknown() {
+ return
+ }
+ if config.RecordType.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "record_type is required.")
+ return
+ }
+ if config.RecordType.IsUnknown() {
+ return
+ }
+ if config.ZoneUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "zone_uid is required.")
+ return
+ }
+ if config.ZoneUid.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.ZoneUid.IsNull() && !config.ZoneUid.IsUnknown() {
+ resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 109, config.ZoneUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve zone_uid", err.Error())
+ } else if resolvedZoneUid != "" && resolvedZoneUid != config.ZoneUid.ValueString() {
+ config.ZoneUid = types.StringValue(resolvedZoneUid)
+ }
+ }
+ params := map[int]string{
+ 75: resources_core.FormatString(config.ZoneUid),
+ 76: resources_core.FormatString(config.RecordType),
+ 77: resources_core.FormatString(config.RecordName),
+ 78: resources_core.FormatString(config.RecordInput),
+ 450: resources_core.FormatInt64(config.RecordTTL),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 111, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *DnsrecordResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data DnsrecordModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.RecordInput.IsNull() || data.RecordInput.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "record_input is required.")
+ return
+ }
+ if data.RecordName.IsNull() || data.RecordName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "record_name is required.")
+ return
+ }
+ if data.RecordType.IsNull() || data.RecordType.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "record_type is required.")
+ return
+ }
+ if data.ZoneUid.IsNull() || data.ZoneUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "zone_uid is required.")
+ return
+ }
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalZoneUid := data.ZoneUid
+ if !data.ZoneUid.IsNull() && !data.ZoneUid.IsUnknown() {
+ resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 109, data.ZoneUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedZoneUid != "" && resolvedZoneUid != data.ZoneUid.ValueString() {
+ data.ZoneUid = types.StringValue(resolvedZoneUid)
+ }
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 111, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 75: resources_core.FormatString(data.ZoneUid),
+ 76: resources_core.FormatString(data.RecordType),
+ 77: resources_core.FormatString(data.RecordName),
+ 78: resources_core.FormatString(data.RecordInput),
+ 450: resources_core.FormatInt64(data.RecordTTL),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 111, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 111, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "recordInput", Field: "RecordInput", Type: "string"},
+ {Code: "recordName", Field: "RecordName", Type: "string"},
+ {Code: "recordTTL", Field: "RecordTTL", Type: "int64"},
+ {Code: "recordType", Field: "RecordType", Type: "string"},
+ {Code: "zoneUid", Field: "ZoneUid", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() {
+ resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 109, state.ZoneUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve zone_uid", err.Error())
+ } else if resolvedZoneUid != "" && resolvedZoneUid != state.ZoneUid.ValueString() {
+ state.ZoneUid = types.StringValue(resolvedZoneUid)
+ }
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+ if !originalZoneUid.IsNull() && !originalZoneUid.IsUnknown() && !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() {
+ if strings.EqualFold(state.ZoneUid.ValueString(), originalZoneUid.ValueString()) {
+ state.ZoneUid = originalZoneUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *DnsrecordResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state DnsrecordModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 111, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "recordInput", Field: "RecordInput", Type: "string"},
+ {Code: "recordName", Field: "RecordName", Type: "string"},
+ {Code: "recordTTL", Field: "RecordTTL", Type: "int64"},
+ {Code: "recordType", Field: "RecordType", Type: "string"},
+ {Code: "zoneUid", Field: "ZoneUid", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !newState.ZoneUid.IsNull() && !newState.ZoneUid.IsUnknown() {
+ resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 109, newState.ZoneUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve zone_uid", err.Error())
+ } else if resolvedZoneUid != "" && resolvedZoneUid != newState.ZoneUid.ValueString() {
+ newState.ZoneUid = types.StringValue(resolvedZoneUid)
+ }
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+ if !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() && !newState.ZoneUid.IsNull() && !newState.ZoneUid.IsUnknown() {
+ if strings.EqualFold(newState.ZoneUid.ValueString(), state.ZoneUid.ValueString()) {
+ newState.ZoneUid = state.ZoneUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *DnsrecordResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan DnsrecordModel
+ var state DnsrecordModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.RecordInput.IsNull() != state.RecordInput.IsNull() || plan.RecordInput.IsUnknown() != state.RecordInput.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.RecordInput.IsNull() && !plan.RecordInput.IsUnknown() && !state.RecordInput.IsNull() && !state.RecordInput.IsUnknown() {
+ if !strings.EqualFold(plan.RecordInput.ValueString(), state.RecordInput.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 256: resources_core.FormatString(plan.RecordInput),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 111, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "recordInput", Field: "RecordInput", Type: "string"},
+ {Code: "recordName", Field: "RecordName", Type: "string"},
+ {Code: "recordTTL", Field: "RecordTTL", Type: "int64"},
+ {Code: "recordType", Field: "RecordType", Type: "string"},
+ {Code: "zoneUid", Field: "ZoneUid", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() {
+ resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 109, state.ZoneUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve zone_uid", err.Error())
+ } else if resolvedZoneUid != "" && resolvedZoneUid != state.ZoneUid.ValueString() {
+ state.ZoneUid = types.StringValue(resolvedZoneUid)
+ }
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+ if !plan.ZoneUid.IsNull() && !plan.ZoneUid.IsUnknown() && !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() {
+ if strings.EqualFold(state.ZoneUid.ValueString(), plan.ZoneUid.ValueString()) {
+ state.ZoneUid = plan.ZoneUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *DnsrecordResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state DnsrecordModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *DnsrecordResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *DnsrecordResource) 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
+}
diff --git a/generated/prod/go/112_tenant_resource.go b/generated/prod/go/112_tenant_resource.go
new file mode 100644
index 0000000..ec395dd
--- /dev/null
+++ b/generated/prod/go/112_tenant_resource.go
@@ -0,0 +1,372 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: tenant
+// Service ID: 112
+
+var _ resource.Resource = &TenantResource{}
+var _ resource.ResourceWithModifyPlan = &TenantResource{}
+var _ resource.ResourceWithImportState = &TenantResource{}
+
+type TenantResource struct {
+ client *core.UniversalClient
+}
+
+type TenantModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ GrafanaOrgName types.String `tfsdk:"grafana_org_name"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewTenantResource() resource.Resource {
+ return &TenantResource{}
+}
+
+func (r *TenantResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_tenant"
+}
+
+func (r *TenantResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "grafana_org_name": schema.StringAttribute{Required: true, MarkdownDescription: "WZ-Код клиента Не изменяется после создания."},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("grafana.ngcloud.ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *TenantResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *TenantModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *TenantModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan TenantModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.GrafanaOrgName.IsNull() && !plan.GrafanaOrgName.IsUnknown() && !state.GrafanaOrgName.IsNull() && !state.GrafanaOrgName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.GrafanaOrgName.ValueString(), state.GrafanaOrgName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить grafana_org_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.GrafanaOrgName.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "grafana_org_name is required.")
+ return
+ }
+ if config.GrafanaOrgName.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 239: resources_core.FormatString(config.GrafanaOrgName),
+ 240: resources_core.FormatString(config.ResourceRealm),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 112, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *TenantResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data TenantModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.GrafanaOrgName.IsNull() || data.GrafanaOrgName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "grafana_org_name is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 112, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 239: resources_core.FormatString(data.GrafanaOrgName),
+ 240: resources_core.FormatString(data.ResourceRealm),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 112, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 112, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "grafanaOrgName", Field: "GrafanaOrgName", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *TenantResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state TenantModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 112, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "grafanaOrgName", Field: "GrafanaOrgName", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *TenantResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan TenantModel
+ var state TenantModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 112, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "grafanaOrgName", Field: "GrafanaOrgName", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *TenantResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state TenantModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *TenantResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *TenantResource) 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
+}
diff --git a/generated/prod/go/113_vc_complex_resource.go b/generated/prod/go/113_vc_complex_resource.go
new file mode 100644
index 0000000..69d834d
--- /dev/null
+++ b/generated/prod/go/113_vc_complex_resource.go
@@ -0,0 +1,563 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_complex
+// Service ID: 113
+
+var _ resource.Resource = &VcComplexResource{}
+var _ resource.ResourceWithModifyPlan = &VcComplexResource{}
+var _ resource.ResourceWithImportState = &VcComplexResource{}
+
+type VcComplexResource struct {
+ client *core.UniversalClient
+}
+
+type VcComplexModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ CpuAllocated types.String `tfsdk:"cpu_allocated"`
+ CpuGuaranteed types.Int64 `tfsdk:"cpu_guaranteed"`
+ IpSpaceName types.String `tfsdk:"ip_space_name"`
+ MemAllocated types.Int64 `tfsdk:"mem_allocated"`
+ MemGuaranteed types.Int64 `tfsdk:"mem_guaranteed"`
+ NeedEnableAVI types.Bool `tfsdk:"need_enable_a_v_i"`
+ NeedExternalAddressSNAT types.Bool `tfsdk:"need_external_address_s_n_a_t"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SegroupName types.String `tfsdk:"segroup_name"`
+ StorageConfig types.String `tfsdk:"storage_config"`
+ VIPCount types.Int64 `tfsdk:"v_i_p_count"`
+ VappName types.String `tfsdk:"vapp_name"`
+ VdcNetworkPool types.String `tfsdk:"vdc_network_pool"`
+ VdcProviderGateway types.String `tfsdk:"vdc_provider_gateway"`
+ VirtualServicesCount types.String `tfsdk:"virtual_services_count"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVcComplexResource() resource.Resource {
+ return &VcComplexResource{}
+}
+
+func (r *VcComplexResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_complex"
+}
+
+func (r *VcComplexResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "cpu_allocated": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("1"), MarkdownDescription: "шт Не изменяется после создания."},
+ "cpu_guaranteed": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(20), MarkdownDescription: "Не изменяется после создания."},
+ "ip_space_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("internet-ipv4-v1"), MarkdownDescription: "Имя ip Space для внешнего IP. Целесообразнее брать самый дешёвый, так как это vip для исходящего трафика. Необходимо задавать если включен needExternalAddressSNAT Не изменяется после создания."},
+ "mem_allocated": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "gb Не изменяется после создания."},
+ "mem_guaranteed": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(20), MarkdownDescription: "Не изменяется после создания."},
+ "need_enable_a_v_i": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает AVI в Cloud Director Не изменяется после создания."},
+ "need_external_address_s_n_a_t": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Выделяет внешний IP на Edge. Используется для SNAT всего трафика Не изменяется после создания."},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("ngcloud.ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ "segroup_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("SEGROUP-SANDBOX-CL1-SHARED-01"), MarkdownDescription: "Наименование segroup, в котором будут выделяться пулы на AVI Не изменяется после создания."},
+ "storage_config": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{\"name\": \"\", \"size\": 1024}]"), MarkdownDescription: "Описывается как массив объектов, где описывается имя политики и её размер Не изменяется после создания.", PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()}},
+ "v_i_p_count": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "ш Не изменяется после создания."},
+ "vapp_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя vApp Не изменяется после создания."},
+ "vdc_network_pool": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("nsxt-sandbox-geneve-np"), MarkdownDescription: "Не изменяется после создания."},
+ "vdc_provider_gateway": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("sandbox-v1cl1-pvdc"), MarkdownDescription: "Взять из свойств ресурсной платформы.
Путь .vcd.providerVdc - взять KEY
providerGateway Cloud Director. Влияет ресурсы будут предоставлены. Является полем (List<String>) в свойствах рес. платформы ЛК, но сам параметр передаётся только как один String Не изменяется после создания."},
+ "virtual_services_count": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("1"), MarkdownDescription: "Кол-во виртуальных сервисов, которые выделяются на AVI. Во избежании коллапса пока выделяется до 4 Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcComplexResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VcComplexModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VcComplexModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VcComplexModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.CpuAllocated.IsNull() && !plan.CpuAllocated.IsUnknown() && !state.CpuAllocated.IsNull() && !state.CpuAllocated.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.CpuAllocated.ValueString(), state.CpuAllocated.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить cpu_allocated", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.CpuGuaranteed.IsNull() && !plan.CpuGuaranteed.IsUnknown() && !state.CpuGuaranteed.IsNull() && !state.CpuGuaranteed.IsUnknown() {
+ if plan.CpuGuaranteed.ValueInt64() != state.CpuGuaranteed.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить cpu_guaranteed", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.IpSpaceName.IsNull() && !plan.IpSpaceName.IsUnknown() && !state.IpSpaceName.IsNull() && !state.IpSpaceName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.IpSpaceName.ValueString(), state.IpSpaceName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить ip_space_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.MemAllocated.IsNull() && !plan.MemAllocated.IsUnknown() && !state.MemAllocated.IsNull() && !state.MemAllocated.IsUnknown() {
+ if plan.MemAllocated.ValueInt64() != state.MemAllocated.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить mem_allocated", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.MemGuaranteed.IsNull() && !plan.MemGuaranteed.IsUnknown() && !state.MemGuaranteed.IsNull() && !state.MemGuaranteed.IsUnknown() {
+ if plan.MemGuaranteed.ValueInt64() != state.MemGuaranteed.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить mem_guaranteed", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.NeedEnableAVI.IsNull() && !plan.NeedEnableAVI.IsUnknown() && !state.NeedEnableAVI.IsNull() && !state.NeedEnableAVI.IsUnknown() {
+ if plan.NeedEnableAVI.ValueBool() != state.NeedEnableAVI.ValueBool() {
+ resp.Diagnostics.AddError("Нельзя изменить need_enable_a_v_i", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.NeedExternalAddressSNAT.IsNull() && !plan.NeedExternalAddressSNAT.IsUnknown() && !state.NeedExternalAddressSNAT.IsNull() && !state.NeedExternalAddressSNAT.IsUnknown() {
+ if plan.NeedExternalAddressSNAT.ValueBool() != state.NeedExternalAddressSNAT.ValueBool() {
+ resp.Diagnostics.AddError("Нельзя изменить need_external_address_s_n_a_t", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.SegroupName.IsNull() && !plan.SegroupName.IsUnknown() && !state.SegroupName.IsNull() && !state.SegroupName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.SegroupName.ValueString(), state.SegroupName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить segroup_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.StorageConfig.IsNull() && !plan.StorageConfig.IsUnknown() && !state.StorageConfig.IsNull() && !state.StorageConfig.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.StorageConfig.ValueString(), state.StorageConfig.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить storage_config", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VIPCount.IsNull() && !plan.VIPCount.IsUnknown() && !state.VIPCount.IsNull() && !state.VIPCount.IsUnknown() {
+ if plan.VIPCount.ValueInt64() != state.VIPCount.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить v_i_p_count", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VappName.IsNull() && !plan.VappName.IsUnknown() && !state.VappName.IsNull() && !state.VappName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VappName.ValueString(), state.VappName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vapp_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VdcNetworkPool.IsNull() && !plan.VdcNetworkPool.IsUnknown() && !state.VdcNetworkPool.IsNull() && !state.VdcNetworkPool.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VdcNetworkPool.ValueString(), state.VdcNetworkPool.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vdc_network_pool", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VdcProviderGateway.IsNull() && !plan.VdcProviderGateway.IsUnknown() && !state.VdcProviderGateway.IsNull() && !state.VdcProviderGateway.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VdcProviderGateway.ValueString(), state.VdcProviderGateway.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vdc_provider_gateway", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VirtualServicesCount.IsNull() && !plan.VirtualServicesCount.IsUnknown() && !state.VirtualServicesCount.IsNull() && !state.VirtualServicesCount.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VirtualServicesCount.ValueString(), state.VirtualServicesCount.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить virtual_services_count", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.VappName.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "vapp_name is required.")
+ return
+ }
+ if config.VappName.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 332: resources_core.FormatString(config.ResourceRealm),
+ 334: resources_core.FormatString(config.VappName),
+ 358: resources_core.FormatString(config.VdcProviderGateway),
+ 359: resources_core.FormatString(config.VirtualServicesCount),
+ 360: resources_core.FormatString(config.IpSpaceName),
+ 399: resources_core.FormatString(config.StorageConfig),
+ 400: resources_core.FormatString(config.VdcNetworkPool),
+ 401: resources_core.FormatInt64(config.CpuGuaranteed),
+ 402: resources_core.FormatInt64(config.MemGuaranteed),
+ 403: resources_core.FormatString(config.SegroupName),
+ 404: resources_core.FormatBool(config.NeedExternalAddressSNAT),
+ 405: resources_core.FormatBool(config.NeedEnableAVI),
+ 566: resources_core.FormatString(config.CpuAllocated),
+ 567: resources_core.FormatInt64(config.MemAllocated),
+ 568: resources_core.FormatInt64(config.VIPCount),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 113, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VcComplexResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VcComplexModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.VappName.IsNull() || data.VappName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "vapp_name is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 113, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 332: resources_core.FormatString(data.ResourceRealm),
+ 334: resources_core.FormatString(data.VappName),
+ 358: resources_core.FormatString(data.VdcProviderGateway),
+ 359: resources_core.FormatString(data.VirtualServicesCount),
+ 360: resources_core.FormatString(data.IpSpaceName),
+ 399: resources_core.FormatString(data.StorageConfig),
+ 400: resources_core.FormatString(data.VdcNetworkPool),
+ 401: resources_core.FormatInt64(data.CpuGuaranteed),
+ 402: resources_core.FormatInt64(data.MemGuaranteed),
+ 403: resources_core.FormatString(data.SegroupName),
+ 404: resources_core.FormatBool(data.NeedExternalAddressSNAT),
+ 405: resources_core.FormatBool(data.NeedEnableAVI),
+ 566: resources_core.FormatString(data.CpuAllocated),
+ 567: resources_core.FormatInt64(data.MemAllocated),
+ 568: resources_core.FormatInt64(data.VIPCount),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 113, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 113, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "cpuAllocated", Field: "CpuAllocated", Type: "string"},
+ {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "memAllocated", Field: "MemAllocated", Type: "int64"},
+ {Code: "memGuaranteed", Field: "MemGuaranteed", Type: "int64"},
+ {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"},
+ {Code: "needExternalAddressSNAT", Field: "NeedExternalAddressSNAT", Type: "bool"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "segroupName", Field: "SegroupName", Type: "string"},
+ {Code: "storageConfig", Field: "StorageConfig", Type: "string"},
+ {Code: "vIPCount", Field: "VIPCount", Type: "int64"},
+ {Code: "vappName", Field: "VappName", Type: "string"},
+ {Code: "vdcNetworkPool", Field: "VdcNetworkPool", Type: "string"},
+ {Code: "vdcProviderGateway", Field: "VdcProviderGateway", Type: "string"},
+ {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcComplexResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcComplexModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 113, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "cpuAllocated", Field: "CpuAllocated", Type: "string"},
+ {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "memAllocated", Field: "MemAllocated", Type: "int64"},
+ {Code: "memGuaranteed", Field: "MemGuaranteed", Type: "int64"},
+ {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"},
+ {Code: "needExternalAddressSNAT", Field: "NeedExternalAddressSNAT", Type: "bool"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "segroupName", Field: "SegroupName", Type: "string"},
+ {Code: "storageConfig", Field: "StorageConfig", Type: "string"},
+ {Code: "vIPCount", Field: "VIPCount", Type: "int64"},
+ {Code: "vappName", Field: "VappName", Type: "string"},
+ {Code: "vdcNetworkPool", Field: "VdcNetworkPool", Type: "string"},
+ {Code: "vdcProviderGateway", Field: "VdcProviderGateway", Type: "string"},
+ {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VcComplexResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcComplexModel
+ var state VcComplexModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 113, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "cpuAllocated", Field: "CpuAllocated", Type: "string"},
+ {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "memAllocated", Field: "MemAllocated", Type: "int64"},
+ {Code: "memGuaranteed", Field: "MemGuaranteed", Type: "int64"},
+ {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"},
+ {Code: "needExternalAddressSNAT", Field: "NeedExternalAddressSNAT", Type: "bool"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "segroupName", Field: "SegroupName", Type: "string"},
+ {Code: "storageConfig", Field: "StorageConfig", Type: "string"},
+ {Code: "vIPCount", Field: "VIPCount", Type: "int64"},
+ {Code: "vappName", Field: "VappName", Type: "string"},
+ {Code: "vdcNetworkPool", Field: "VdcNetworkPool", Type: "string"},
+ {Code: "vdcProviderGateway", Field: "VdcProviderGateway", Type: "string"},
+ {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcComplexResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcComplexModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VcComplexResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VcComplexResource) 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
+}
diff --git a/generated/prod/go/114_gitea_complex_resource.go b/generated/prod/go/114_gitea_complex_resource.go
new file mode 100644
index 0000000..f6c0b7c
--- /dev/null
+++ b/generated/prod/go/114_gitea_complex_resource.go
@@ -0,0 +1,322 @@
+package resources_gen
+
+import (
+ "context"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: gitea_complex
+// Service ID: 114
+
+var _ resource.Resource = &GiteaComplexResource{}
+var _ resource.ResourceWithModifyPlan = &GiteaComplexResource{}
+var _ resource.ResourceWithImportState = &GiteaComplexResource{}
+
+type GiteaComplexResource struct {
+ client *core.UniversalClient
+}
+
+type GiteaComplexModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewGiteaComplexResource() resource.Resource {
+ return &GiteaComplexResource{}
+}
+
+func (r *GiteaComplexResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_gitea_complex"
+}
+
+func (r *GiteaComplexResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *GiteaComplexResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *GiteaComplexModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *GiteaComplexModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan GiteaComplexModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{}
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 114, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *GiteaComplexResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data GiteaComplexModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 114, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{}
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 114, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 114, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{})
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *GiteaComplexResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state GiteaComplexModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 114, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{})
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *GiteaComplexResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan GiteaComplexModel
+ var state GiteaComplexModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 114, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{})
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *GiteaComplexResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state GiteaComplexModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *GiteaComplexResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *GiteaComplexResource) 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
+}
diff --git a/generated/prod/go/115_mariadb_database_resource.go b/generated/prod/go/115_mariadb_database_resource.go
new file mode 100644
index 0000000..8ca1ed8
--- /dev/null
+++ b/generated/prod/go/115_mariadb_database_resource.go
@@ -0,0 +1,343 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: mariadb
+// Subresource: database
+
+var _ resource.Resource = &MariadbDatabaseResource{}
+
+// MariadbDatabaseResource manages a subresource via operations.
+type MariadbDatabaseResource struct {
+ client *core.UniversalClient
+}
+
+type MariadbDatabaseModel struct {
+ ID types.String `tfsdk:"id"`
+ MariadbID types.String `tfsdk:"mariadb_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"`
+ DbName types.String `tfsdk:"db_name"`
+}
+
+func NewMariadbDatabaseResource() resource.Resource {
+ return &MariadbDatabaseResource{}
+}
+
+func (r *MariadbDatabaseResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_mariadb_database"
+}
+
+func (r *MariadbDatabaseResource) 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()}},
+ "mariadb_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."},
+ "db_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("db-zs0q9wuv"), MarkdownDescription: "Пример: `db01` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *MariadbDatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan MariadbDatabaseModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.MariadbID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "dbName": resources_core.FormatString(plan.DbName),
+ }
+ identityCodes := []string{
+ "dbName",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", 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, "database", 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{
+ "dbName": resources_core.FormatString(plan.DbName),
+ })
+
+ 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, "create_database", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_database", 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, "database", 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, "database", 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, "database", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *MariadbDatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state MariadbDatabaseModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *MariadbDatabaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan MariadbDatabaseModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state MariadbDatabaseModel
+ 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.MariadbID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "dbName": resources_core.FormatString(plan.DbName),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *MariadbDatabaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state MariadbDatabaseModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_database" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.MariadbID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "dbName": resources_core.FormatString(state.DbName),
+ }
+ identityCodes := []string{
+ "dbName",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", 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{
+ "dbName": resources_core.FormatString(state.DbName),
+ })
+
+ 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, "delete_database", 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 *MariadbDatabaseResource) 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
+}
diff --git a/generated/prod/go/115_mariadb_resource.go b/generated/prod/go/115_mariadb_resource.go
new file mode 100644
index 0000000..4e4044f
--- /dev/null
+++ b/generated/prod/go/115_mariadb_resource.go
@@ -0,0 +1,447 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: mariadb
+// Service ID: 115
+
+var _ resource.Resource = &MariadbResource{}
+var _ resource.ResourceWithModifyPlan = &MariadbResource{}
+var _ resource.ResourceWithImportState = &MariadbResource{}
+
+type MariadbResource struct {
+ client *core.UniversalClient
+}
+
+// MariadbAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type MariadbAccessConfigurationModel struct {
+ MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"`
+ MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"`
+}
+
+// MariadbAutoscaleConfigurationModel — вложенная модель для map-fixed параметра autoscaleConfiguration.
+type MariadbAutoscaleConfigurationModel struct {
+ Enabled types.Bool `tfsdk:"enabled" json:"enabled"`
+ Schedule types.Int64 `tfsdk:"schedule" json:"schedule"`
+ Quota types.Int64 `tfsdk:"quota" json:"quota"`
+ Percent types.Int64 `tfsdk:"percent" json:"percent"`
+}
+
+// MariadbBackupConfigurationModel — вложенная модель для map-fixed параметра backupConfiguration.
+type MariadbBackupConfigurationModel struct {
+ S3Uid types.String `tfsdk:"s3_uid" json:"s3Uid"`
+ Retain types.Int64 `tfsdk:"retain" json:"retain"`
+ Schedule types.String `tfsdk:"schedule" json:"schedule"`
+}
+
+// MariadbClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type MariadbClusterConfigurationModel struct {
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+ Disk types.Int64 `tfsdk:"disk" json:"disk"`
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+}
+
+// MariadbMariadbConfigurationModel — вложенная модель для map-fixed параметра mariadbConfiguration.
+type MariadbMariadbConfigurationModel struct {
+ Version types.String `tfsdk:"version" json:"version"`
+}
+
+// MariadbStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type MariadbStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+}
+
+type MariadbModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AccessConfiguration *MariadbAccessConfigurationModel `tfsdk:"access_configuration"`
+ AutoscaleConfiguration *MariadbAutoscaleConfigurationModel `tfsdk:"autoscale_configuration"`
+ BackupConfiguration *MariadbBackupConfigurationModel `tfsdk:"backup_configuration"`
+ ClusterConfiguration *MariadbClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ MariadbConfiguration *MariadbMariadbConfigurationModel `tfsdk:"mariadb_configuration"`
+ StartupConfiguration *MariadbStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewMariadbResource() resource.Resource {
+ return &MariadbResource{}
+}
+
+func (r *MariadbResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_mariadb"
+}
+
+func (r *MariadbResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "master_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"},
+ "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"},
+ }},
+ "autoscale_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "enabled": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает автоскейлинг PV. При включении параметра необходимо настроить autoScalePercentage и autoScaleTechWindow. Алерт при срабатывании вызывает modify у текущей услуги"},
+ "schedule": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "Требует включения autoScale. Окно обновления (пока не реализовано). Указывается час в виде Integer (0 / 5 / 12 / 23)"},
+ "quota": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100), MarkdownDescription: "До какого размера возможно увеличивать диск, когда включен Autoscale"},
+ "percent": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Требует включения autoScale. Позволяет указать в процентах расширение от текущего макс объема
Минимальный размер на который расширяется - **1Gb**"},
+ }},
+ "backup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "s3_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Экземпляр S3 обязателен"},
+ "retain": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(720)},
+ "schedule": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("0 0 * * *")},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Пока `Galera` выключена
Как будет запрос на включение и тестирование, реплики можно будет увеличивать"},
+ "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Указывается в GIGAbytes"},
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Указывается в Megabytes"},
+ }},
+ "mariadb_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("9.4.0")},
+ }},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"},
+ }},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *MariadbResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *MariadbModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *MariadbModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan MariadbModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 877: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}),
+ 878: resources_core.BuildJSON(map[string]string{"replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64())}),
+ 879: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString())}),
+ 880: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.MariadbConfiguration.Version.ValueString())}),
+ 881: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", config.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", config.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", config.BackupConfiguration.Schedule.ValueString())}),
+ 882: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", config.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", config.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", config.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", config.AutoscaleConfiguration.Percent.ValueInt64())}),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 115, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *MariadbResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data MariadbModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 115, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 877: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}),
+ 878: resources_core.BuildJSON(map[string]string{"replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64())}),
+ 879: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString())}),
+ 880: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.MariadbConfiguration.Version.ValueString())}),
+ 881: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", data.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", data.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", data.BackupConfiguration.Schedule.ValueString())}),
+ 882: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", data.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", data.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", data.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", data.AutoscaleConfiguration.Percent.ValueInt64())}),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 115, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 115, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"},
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "mariadbConfiguration", Field: "MariadbConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *MariadbResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state MariadbModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 115, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"},
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "mariadbConfiguration", Field: "MariadbConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *MariadbResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan MariadbModel
+ var state MariadbModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 883: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64())}),
+ 884: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString())}),
+ 885: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.MariadbConfiguration.Version.ValueString())}),
+ 886: resources_core.BuildJSON(map[string]string{"retain": fmt.Sprintf("%d", plan.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.Schedule.ValueString()), "s3Uid": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.S3Uid.ValueString())}),
+ 887: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", plan.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Percent.ValueInt64())}),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 115, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"},
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "mariadbConfiguration", Field: "MariadbConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *MariadbResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state MariadbModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *MariadbResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *MariadbResource) 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
+}
diff --git a/generated/prod/go/115_mariadb_user_resource.go b/generated/prod/go/115_mariadb_user_resource.go
new file mode 100644
index 0000000..d1e6619
--- /dev/null
+++ b/generated/prod/go/115_mariadb_user_resource.go
@@ -0,0 +1,367 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: mariadb
+// Subresource: user
+
+var _ resource.Resource = &MariadbUserResource{}
+
+// MariadbUserResource manages a subresource via operations.
+type MariadbUserResource struct {
+ client *core.UniversalClient
+}
+
+type MariadbUserModel struct {
+ ID types.String `tfsdk:"id"`
+ MariadbID types.String `tfsdk:"mariadb_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"`
+ AccessHosts types.String `tfsdk:"access_hosts"`
+ DbName types.String `tfsdk:"db_name"`
+ Password types.String `tfsdk:"password"`
+ Username types.String `tfsdk:"username"`
+}
+
+func NewMariadbUserResource() resource.Resource {
+ return &MariadbUserResource{}
+}
+
+func (r *MariadbUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_mariadb_user"
+}
+
+func (r *MariadbUserResource) 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()}},
+ "mariadb_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."},
+ "access_hosts": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("%"), MarkdownDescription: "Пример: `%` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "db_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Необходимо указать массив существующих баз Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "password": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("abpwcb75"), MarkdownDescription: "Секрет будет записан в хранилище секретов и не будет отображаться публично Не изменяется после создания. Изменение требует пересоздания.", Sensitive: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "username": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("usertigstlyt"), MarkdownDescription: "Пример: `myusername` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *MariadbUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan MariadbUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.MariadbID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "accessHosts": resources_core.FormatString(plan.AccessHosts),
+ "dbName": resources_core.FormatString(plan.DbName),
+ "password": resources_core.FormatString(plan.Password),
+ "username": resources_core.FormatString(plan.Username),
+ }
+ identityCodes := []string{
+ "accessHosts",
+ "dbName",
+ "password",
+ "username",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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, "user", 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{
+ "username": resources_core.FormatString(plan.Username),
+ "password": resources_core.FormatString(plan.Password),
+ "dbName": resources_core.FormatString(plan.DbName),
+ "accessHosts": resources_core.FormatString(plan.AccessHosts),
+ })
+
+ 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, "create_user", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", 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, "user", 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, "user", 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, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *MariadbUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state MariadbUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *MariadbUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan MariadbUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state MariadbUserModel
+ 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.MariadbID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "accessHosts": resources_core.FormatString(plan.AccessHosts),
+ "dbName": resources_core.FormatString(plan.DbName),
+ "password": resources_core.FormatString(plan.Password),
+ "username": resources_core.FormatString(plan.Username),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *MariadbUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state MariadbUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_user" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.MariadbID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "accessHosts": resources_core.FormatString(state.AccessHosts),
+ "dbName": resources_core.FormatString(state.DbName),
+ "password": resources_core.FormatString(state.Password),
+ "username": resources_core.FormatString(state.Username),
+ }
+ identityCodes := []string{
+ "accessHosts",
+ "dbName",
+ "password",
+ "username",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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{
+ "username": resources_core.FormatString(state.Username),
+ })
+
+ 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, "delete_user", 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 *MariadbUserResource) 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
+}
diff --git a/generated/prod/go/116_kafka_resource.go b/generated/prod/go/116_kafka_resource.go
new file mode 100644
index 0000000..7ed0fa5
--- /dev/null
+++ b/generated/prod/go/116_kafka_resource.go
@@ -0,0 +1,389 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: kafka
+// Service ID: 116
+
+var _ resource.Resource = &KafkaResource{}
+var _ resource.ResourceWithModifyPlan = &KafkaResource{}
+var _ resource.ResourceWithImportState = &KafkaResource{}
+
+type KafkaResource struct {
+ client *core.UniversalClient
+}
+
+// KafkaAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type KafkaAccessConfigurationModel struct {
+ AllocationExternal types.Bool `tfsdk:"allocation_external" json:"allocationExternal"`
+ MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"`
+}
+
+// KafkaClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type KafkaClusterConfigurationModel struct {
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+ Disk types.Int64 `tfsdk:"disk" json:"disk"`
+}
+
+// KafkaStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type KafkaStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+}
+
+type KafkaModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AccessConfiguration *KafkaAccessConfigurationModel `tfsdk:"access_configuration"`
+ ClusterConfiguration *KafkaClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ StartupConfiguration *KafkaStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewKafkaResource() resource.Resource {
+ return &KafkaResource{}
+}
+
+func (r *KafkaResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_kafka"
+}
+
+func (r *KafkaResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "allocation_external": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2000), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2048)},
+ "replicas": schema.Int64Attribute{Required: true},
+ "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ }},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"},
+ }},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *KafkaResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *KafkaModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *KafkaModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan KafkaModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 1084: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1085: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64())}),
+ 1086: resources_core.BuildJSON(map[string]string{"allocationExternal": fmt.Sprintf("%v", config.AccessConfiguration.AllocationExternal.ValueBool()), "masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString())}),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 116, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *KafkaResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data KafkaModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 116, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 1084: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1085: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64())}),
+ 1086: resources_core.BuildJSON(map[string]string{"allocationExternal": fmt.Sprintf("%v", data.AccessConfiguration.AllocationExternal.ValueBool()), "masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString())}),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 116, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 116, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *KafkaResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state KafkaModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 116, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *KafkaResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan KafkaModel
+ var state KafkaModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 1087: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64())}),
+ 1088: resources_core.BuildJSON(map[string]string{"allocationExternal": fmt.Sprintf("%v", plan.AccessConfiguration.AllocationExternal.ValueBool()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString())}),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 116, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *KafkaResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state KafkaModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *KafkaResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *KafkaResource) 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
+}
diff --git a/generated/prod/go/116_kafka_topic_resource.go b/generated/prod/go/116_kafka_topic_resource.go
new file mode 100644
index 0000000..3f289b3
--- /dev/null
+++ b/generated/prod/go/116_kafka_topic_resource.go
@@ -0,0 +1,364 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: kafka
+// Subresource: topic
+
+var _ resource.Resource = &KafkaTopicResource{}
+
+// KafkaTopicResource manages a subresource via operations.
+type KafkaTopicResource struct {
+ client *core.UniversalClient
+}
+
+type KafkaTopicModel struct {
+ ID types.String `tfsdk:"id"`
+ KafkaID types.String `tfsdk:"kafka_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"`
+ NameTopic types.String `tfsdk:"name_topic"`
+ Partitions types.Int64 `tfsdk:"partitions"`
+ Replicas types.Int64 `tfsdk:"replicas"`
+}
+
+func NewKafkaTopicResource() resource.Resource {
+ return &KafkaTopicResource{}
+}
+
+func (r *KafkaTopicResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_kafka_topic"
+}
+
+func (r *KafkaTopicResource) 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()}},
+ "kafka_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."},
+ "name_topic": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `orders-events` Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "partitions": schema.Int64Attribute{Required: true, MarkdownDescription: "Число партиций можно увеличить, но уменьшить их нельзя."},
+ "replicas": schema.Int64Attribute{Required: true, MarkdownDescription: "Количество реплик не может привышать количество реплик брокера"},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *KafkaTopicResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan KafkaTopicModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if plan.NameTopic.IsNull() || plan.NameTopic.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "name_topic is required.")
+ return
+ }
+ if plan.Partitions.IsNull() || plan.Partitions.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "partitions is required.")
+ return
+ }
+ if plan.Replicas.IsNull() || plan.Replicas.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "replicas is required.")
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.KafkaID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "nameTopic": resources_core.FormatString(plan.NameTopic),
+ }
+ identityCodes := []string{
+ "nameTopic",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("topic", 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, "topic", 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{
+ "nameTopic": resources_core.FormatString(plan.NameTopic),
+ "partitions": resources_core.FormatInt64(plan.Partitions),
+ "replicas": resources_core.FormatInt64(plan.Replicas),
+ })
+
+ 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, "create_topic", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_topic", 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, "topic", 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, "topic", 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, "topic", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *KafkaTopicResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state KafkaTopicModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *KafkaTopicResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan KafkaTopicModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "modify_topic" == "" {
+ var state KafkaTopicModel
+ 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.KafkaID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{
+ "nameTopic": resources_core.FormatString(plan.NameTopic),
+ "partitions": resources_core.FormatInt64(plan.Partitions),
+ "replicas": resources_core.FormatInt64(plan.Replicas),
+ })
+
+ idParams := map[string]string{
+ "nameTopic": resources_core.FormatString(plan.NameTopic),
+ }
+
+ 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, "modify_topic", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "topic", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *KafkaTopicResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state KafkaTopicModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_topic" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.KafkaID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "nameTopic": resources_core.FormatString(state.NameTopic),
+ }
+ identityCodes := []string{
+ "nameTopic",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("topic", 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{
+ "nameTopic": resources_core.FormatString(state.NameTopic),
+ })
+
+ 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, "delete_topic", 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 *KafkaTopicResource) 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
+}
diff --git a/generated/prod/go/116_kafka_user_resource.go b/generated/prod/go/116_kafka_user_resource.go
new file mode 100644
index 0000000..52ea73c
--- /dev/null
+++ b/generated/prod/go/116_kafka_user_resource.go
@@ -0,0 +1,373 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: kafka
+// Subresource: user
+
+var _ resource.Resource = &KafkaUserResource{}
+
+// KafkaUserResource manages a subresource via operations.
+type KafkaUserResource struct {
+ client *core.UniversalClient
+}
+
+type KafkaUserModel struct {
+ ID types.String `tfsdk:"id"`
+ KafkaID types.String `tfsdk:"kafka_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"`
+ AccessHosts types.String `tfsdk:"access_hosts"`
+ Group types.String `tfsdk:"group"`
+ NameTopic types.String `tfsdk:"name_topic"`
+ Operations types.String `tfsdk:"operations"`
+ Username types.String `tfsdk:"username"`
+}
+
+func NewKafkaUserResource() resource.Resource {
+ return &KafkaUserResource{}
+}
+
+func (r *KafkaUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_kafka_user"
+}
+
+func (r *KafkaUserResource) 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()}},
+ "kafka_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."},
+ "access_hosts": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("*"), MarkdownDescription: "Пример: `192.168.1.0/24` или `*`"},
+ "group": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("*"), MarkdownDescription: "Пример: `order-consumers` или `*`"},
+ "name_topic": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `orders-events`. Использовать маску нельзя(`orders*`), только полное название или `*`"},
+ "operations": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `Create, Describe, Read, Write`"},
+ "username": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `username-admin` Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *KafkaUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan KafkaUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if plan.NameTopic.IsNull() || plan.NameTopic.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "name_topic is required.")
+ return
+ }
+ if plan.Operations.IsNull() || plan.Operations.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "operations is required.")
+ return
+ }
+ if plan.Username.IsNull() || plan.Username.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "username is required.")
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.KafkaID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "username": resources_core.FormatString(plan.Username),
+ }
+ identityCodes := []string{
+ "username",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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, "user", 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{
+ "username": resources_core.FormatString(plan.Username),
+ "nameTopic": resources_core.FormatString(plan.NameTopic),
+ "operations": resources_core.FormatString(plan.Operations),
+ "group": resources_core.FormatString(plan.Group),
+ "accessHosts": resources_core.FormatString(plan.AccessHosts),
+ })
+
+ 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, "create_user", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", 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, "user", 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, "user", 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, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *KafkaUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state KafkaUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *KafkaUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan KafkaUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "modify_user" == "" {
+ var state KafkaUserModel
+ 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.KafkaID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{
+ "nameTopic": resources_core.FormatString(plan.NameTopic),
+ "operations": resources_core.FormatString(plan.Operations),
+ "group": resources_core.FormatString(plan.Group),
+ "accessHosts": resources_core.FormatString(plan.AccessHosts),
+ "username": resources_core.FormatString(plan.Username),
+ })
+
+ idParams := map[string]string{
+ "username": resources_core.FormatString(plan.Username),
+ }
+
+ 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, "modify_user", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *KafkaUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state KafkaUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_user" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.KafkaID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "username": resources_core.FormatString(state.Username),
+ }
+ identityCodes := []string{
+ "username",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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{
+ "username": resources_core.FormatString(state.Username),
+ })
+
+ 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, "delete_user", 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 *KafkaUserResource) 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
+}
diff --git a/generated/prod/go/117_nifi_resource.go b/generated/prod/go/117_nifi_resource.go
new file mode 100644
index 0000000..77f570f
--- /dev/null
+++ b/generated/prod/go/117_nifi_resource.go
@@ -0,0 +1,533 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: nifi
+// Service ID: 117
+
+var _ resource.Resource = &NifiResource{}
+var _ resource.ResourceWithModifyPlan = &NifiResource{}
+var _ resource.ResourceWithImportState = &NifiResource{}
+
+type NifiResource struct {
+ client *core.UniversalClient
+}
+
+type NifiModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ KafkaUid types.String `tfsdk:"kafka_uid"`
+ NameTopic types.String `tfsdk:"name_topic"`
+ Partitions types.Int64 `tfsdk:"partitions"`
+ Replicas types.Int64 `tfsdk:"replicas"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewNifiResource() resource.Resource {
+ return &NifiResource{}
+}
+
+func (r *NifiResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_nifi"
+}
+
+func (r *NifiResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "kafka_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Выбрать кластер из списка Не изменяется после создания."},
+ "name_topic": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `orders-events` Не изменяется после создания."},
+ "partitions": schema.Int64Attribute{Required: true, MarkdownDescription: "Число партиций на кластер ≤ 100 × (число брокеров) × (фактор репликации). Пример `50`"},
+ "replicas": schema.Int64Attribute{Required: true, MarkdownDescription: "Количество партиций ≤ (число брокеров). Пример `2`"},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *NifiResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *NifiModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *NifiModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan NifiModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.KafkaUid.IsNull() && !plan.KafkaUid.IsUnknown() && !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.KafkaUid.ValueString(), state.KafkaUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить kafka_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.NameTopic.IsNull() && !plan.NameTopic.IsUnknown() && !state.NameTopic.IsNull() && !state.NameTopic.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.NameTopic.ValueString(), state.NameTopic.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить name_topic", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.KafkaUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "kafka_uid is required.")
+ return
+ }
+ if config.KafkaUid.IsUnknown() {
+ return
+ }
+ if config.NameTopic.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "name_topic is required.")
+ return
+ }
+ if config.NameTopic.IsUnknown() {
+ return
+ }
+ if config.Partitions.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "partitions is required.")
+ return
+ }
+ if config.Partitions.IsUnknown() {
+ return
+ }
+ if config.Replicas.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "replicas is required.")
+ return
+ }
+ if config.Replicas.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.KafkaUid.IsNull() && !config.KafkaUid.IsUnknown() {
+ resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, config.KafkaUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error())
+ } else if resolvedKafkaUid != "" && resolvedKafkaUid != config.KafkaUid.ValueString() {
+ config.KafkaUid = types.StringValue(resolvedKafkaUid)
+ }
+ }
+ params := map[int]string{
+ 471: resources_core.FormatString(config.KafkaUid),
+ 472: resources_core.FormatInt64(config.Partitions),
+ 473: resources_core.FormatInt64(config.Replicas),
+ 474: resources_core.FormatString(config.NameTopic),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 117, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *NifiResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data NifiModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.KafkaUid.IsNull() || data.KafkaUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "kafka_uid is required.")
+ return
+ }
+ if data.NameTopic.IsNull() || data.NameTopic.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "name_topic is required.")
+ return
+ }
+ if data.Partitions.IsNull() || data.Partitions.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "partitions is required.")
+ return
+ }
+ if data.Replicas.IsNull() || data.Replicas.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "replicas is required.")
+ return
+ }
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalKafkaUid := data.KafkaUid
+ if !data.KafkaUid.IsNull() && !data.KafkaUid.IsUnknown() {
+ resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, data.KafkaUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedKafkaUid != "" && resolvedKafkaUid != data.KafkaUid.ValueString() {
+ data.KafkaUid = types.StringValue(resolvedKafkaUid)
+ }
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 117, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 471: resources_core.FormatString(data.KafkaUid),
+ 472: resources_core.FormatInt64(data.Partitions),
+ 473: resources_core.FormatInt64(data.Replicas),
+ 474: resources_core.FormatString(data.NameTopic),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 117, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 117, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "kafkaUid", Field: "KafkaUid", Type: "string"},
+ {Code: "nameTopic", Field: "NameTopic", Type: "string"},
+ {Code: "partitions", Field: "Partitions", Type: "int64"},
+ {Code: "replicas", Field: "Replicas", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() {
+ resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, state.KafkaUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error())
+ } else if resolvedKafkaUid != "" && resolvedKafkaUid != state.KafkaUid.ValueString() {
+ state.KafkaUid = types.StringValue(resolvedKafkaUid)
+ }
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+ if !originalKafkaUid.IsNull() && !originalKafkaUid.IsUnknown() && !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() {
+ if strings.EqualFold(state.KafkaUid.ValueString(), originalKafkaUid.ValueString()) {
+ state.KafkaUid = originalKafkaUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *NifiResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state NifiModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 117, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "kafkaUid", Field: "KafkaUid", Type: "string"},
+ {Code: "nameTopic", Field: "NameTopic", Type: "string"},
+ {Code: "partitions", Field: "Partitions", Type: "int64"},
+ {Code: "replicas", Field: "Replicas", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !newState.KafkaUid.IsNull() && !newState.KafkaUid.IsUnknown() {
+ resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, newState.KafkaUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error())
+ } else if resolvedKafkaUid != "" && resolvedKafkaUid != newState.KafkaUid.ValueString() {
+ newState.KafkaUid = types.StringValue(resolvedKafkaUid)
+ }
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+ if !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() && !newState.KafkaUid.IsNull() && !newState.KafkaUid.IsUnknown() {
+ if strings.EqualFold(newState.KafkaUid.ValueString(), state.KafkaUid.ValueString()) {
+ newState.KafkaUid = state.KafkaUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *NifiResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan NifiModel
+ var state NifiModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.Partitions.IsNull() != state.Partitions.IsNull() || plan.Partitions.IsUnknown() != state.Partitions.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.Partitions.IsNull() && !plan.Partitions.IsUnknown() && !state.Partitions.IsNull() && !state.Partitions.IsUnknown() {
+ if plan.Partitions.ValueInt64() != state.Partitions.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.Replicas.IsNull() != state.Replicas.IsNull() || plan.Replicas.IsUnknown() != state.Replicas.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.Replicas.IsNull() && !plan.Replicas.IsUnknown() && !state.Replicas.IsNull() && !state.Replicas.IsUnknown() {
+ if plan.Replicas.ValueInt64() != state.Replicas.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 477: resources_core.FormatInt64(plan.Partitions),
+ 478: resources_core.FormatInt64(plan.Replicas),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 117, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "kafkaUid", Field: "KafkaUid", Type: "string"},
+ {Code: "nameTopic", Field: "NameTopic", Type: "string"},
+ {Code: "partitions", Field: "Partitions", Type: "int64"},
+ {Code: "replicas", Field: "Replicas", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() {
+ resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, state.KafkaUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error())
+ } else if resolvedKafkaUid != "" && resolvedKafkaUid != state.KafkaUid.ValueString() {
+ state.KafkaUid = types.StringValue(resolvedKafkaUid)
+ }
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+ if !plan.KafkaUid.IsNull() && !plan.KafkaUid.IsUnknown() && !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() {
+ if strings.EqualFold(state.KafkaUid.ValueString(), plan.KafkaUid.ValueString()) {
+ state.KafkaUid = plan.KafkaUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *NifiResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state NifiModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *NifiResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *NifiResource) 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
+}
diff --git a/generated/prod/go/119_akhq_resource.go b/generated/prod/go/119_akhq_resource.go
new file mode 100644
index 0000000..d7e3c02
--- /dev/null
+++ b/generated/prod/go/119_akhq_resource.go
@@ -0,0 +1,544 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: akhq
+// Service ID: 119
+
+var _ resource.Resource = &AkhqResource{}
+var _ resource.ResourceWithModifyPlan = &AkhqResource{}
+var _ resource.ResourceWithImportState = &AkhqResource{}
+
+type AkhqResource struct {
+ client *core.UniversalClient
+}
+
+type AkhqModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ Domain types.String `tfsdk:"domain"`
+ KafkaUid types.String `tfsdk:"kafka_uid"`
+ ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"`
+ ResourceInstances types.Int64 `tfsdk:"resource_instances"`
+ ResourceMemory types.Int64 `tfsdk:"resource_memory"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewAkhqResource() resource.Resource {
+ return &AkhqResource{}
+}
+
+func (r *AkhqResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_akhq"
+}
+
+func (r *AkhqResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "domain": schema.StringAttribute{Required: true, MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."},
+ "kafka_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Выбрать кластер из списка"},
+ "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Пример: `1000` Не изменяется после создания."},
+ "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Не изменяется после создания."},
+ "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Пример: `1024` Не изменяется после создания."},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Выбрать платформу из списка Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *AkhqResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *AkhqModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *AkhqModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan AkhqModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() {
+ if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_c_p_u", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() {
+ if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_instances", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() {
+ if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_memory", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.Domain.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "domain is required.")
+ return
+ }
+ if config.Domain.IsUnknown() {
+ return
+ }
+ if config.KafkaUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "kafka_uid is required.")
+ return
+ }
+ if config.KafkaUid.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.KafkaUid.IsNull() && !config.KafkaUid.IsUnknown() {
+ resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, config.KafkaUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error())
+ } else if resolvedKafkaUid != "" && resolvedKafkaUid != config.KafkaUid.ValueString() {
+ config.KafkaUid = types.StringValue(resolvedKafkaUid)
+ }
+ }
+ params := map[int]string{
+ 550: resources_core.FormatString(config.ResourceRealm),
+ 551: resources_core.FormatInt64(config.ResourceMemory),
+ 552: resources_core.FormatInt64(config.ResourceCPU),
+ 553: resources_core.FormatString(config.Domain),
+ 554: resources_core.FormatInt64(config.ResourceInstances),
+ 555: resources_core.FormatString(config.KafkaUid),
+ }
+ desiredDomain := ""
+ if !config.Domain.IsNull() && !config.Domain.IsUnknown() {
+ desiredDomain = config.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 119, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *AkhqResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data AkhqModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.Domain.IsNull() || data.Domain.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "domain is required.")
+ return
+ }
+ if data.KafkaUid.IsNull() || data.KafkaUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "kafka_uid is required.")
+ return
+ }
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalKafkaUid := data.KafkaUid
+ if !data.KafkaUid.IsNull() && !data.KafkaUid.IsUnknown() {
+ resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, data.KafkaUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedKafkaUid != "" && resolvedKafkaUid != data.KafkaUid.ValueString() {
+ data.KafkaUid = types.StringValue(resolvedKafkaUid)
+ }
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ if !data.Domain.IsNull() && !data.Domain.IsUnknown() {
+ desiredDomain = data.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 119, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 550: resources_core.FormatString(data.ResourceRealm),
+ 551: resources_core.FormatInt64(data.ResourceMemory),
+ 552: resources_core.FormatInt64(data.ResourceCPU),
+ 553: resources_core.FormatString(data.Domain),
+ 554: resources_core.FormatInt64(data.ResourceInstances),
+ 555: resources_core.FormatString(data.KafkaUid),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 119, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 119, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "kafkaUid", Field: "KafkaUid", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() {
+ resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, state.KafkaUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error())
+ } else if resolvedKafkaUid != "" && resolvedKafkaUid != state.KafkaUid.ValueString() {
+ state.KafkaUid = types.StringValue(resolvedKafkaUid)
+ }
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+ if !originalKafkaUid.IsNull() && !originalKafkaUid.IsUnknown() && !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() {
+ if strings.EqualFold(state.KafkaUid.ValueString(), originalKafkaUid.ValueString()) {
+ state.KafkaUid = originalKafkaUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *AkhqResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state AkhqModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 119, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "kafkaUid", Field: "KafkaUid", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !newState.KafkaUid.IsNull() && !newState.KafkaUid.IsUnknown() {
+ resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, newState.KafkaUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error())
+ } else if resolvedKafkaUid != "" && resolvedKafkaUid != newState.KafkaUid.ValueString() {
+ newState.KafkaUid = types.StringValue(resolvedKafkaUid)
+ }
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+ if !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() && !newState.KafkaUid.IsNull() && !newState.KafkaUid.IsUnknown() {
+ if strings.EqualFold(newState.KafkaUid.ValueString(), state.KafkaUid.ValueString()) {
+ newState.KafkaUid = state.KafkaUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *AkhqResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan AkhqModel
+ var state AkhqModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.KafkaUid.IsNull() != state.KafkaUid.IsNull() || plan.KafkaUid.IsUnknown() != state.KafkaUid.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.KafkaUid.IsNull() && !plan.KafkaUid.IsUnknown() && !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() {
+ if !strings.EqualFold(plan.KafkaUid.ValueString(), state.KafkaUid.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 565: resources_core.FormatString(plan.KafkaUid),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 119, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "kafkaUid", Field: "KafkaUid", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() {
+ resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, state.KafkaUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error())
+ } else if resolvedKafkaUid != "" && resolvedKafkaUid != state.KafkaUid.ValueString() {
+ state.KafkaUid = types.StringValue(resolvedKafkaUid)
+ }
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+ if !plan.KafkaUid.IsNull() && !plan.KafkaUid.IsUnknown() && !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() {
+ if strings.EqualFold(state.KafkaUid.ValueString(), plan.KafkaUid.ValueString()) {
+ state.KafkaUid = plan.KafkaUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *AkhqResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state AkhqModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *AkhqResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *AkhqResource) 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
+}
diff --git a/generated/prod/go/120_clickhouse_database_resource.go b/generated/prod/go/120_clickhouse_database_resource.go
new file mode 100644
index 0000000..cc5c42e
--- /dev/null
+++ b/generated/prod/go/120_clickhouse_database_resource.go
@@ -0,0 +1,343 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: clickhouse
+// Subresource: database
+
+var _ resource.Resource = &ClickhouseDatabaseResource{}
+
+// ClickhouseDatabaseResource manages a subresource via operations.
+type ClickhouseDatabaseResource struct {
+ client *core.UniversalClient
+}
+
+type ClickhouseDatabaseModel struct {
+ ID types.String `tfsdk:"id"`
+ ClickhouseID types.String `tfsdk:"clickhouse_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"`
+ DbName types.String `tfsdk:"db_name"`
+}
+
+func NewClickhouseDatabaseResource() resource.Resource {
+ return &ClickhouseDatabaseResource{}
+}
+
+func (r *ClickhouseDatabaseResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_clickhouse_database"
+}
+
+func (r *ClickhouseDatabaseResource) 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()}},
+ "clickhouse_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."},
+ "db_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("db01"), MarkdownDescription: "Пример: `db01` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *ClickhouseDatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan ClickhouseDatabaseModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.ClickhouseID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "dbName": resources_core.FormatString(plan.DbName),
+ }
+ identityCodes := []string{
+ "dbName",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", 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, "database", 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{
+ "dbName": resources_core.FormatString(plan.DbName),
+ })
+
+ 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, "create_database", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_database", 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, "database", 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, "database", 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, "database", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *ClickhouseDatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state ClickhouseDatabaseModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *ClickhouseDatabaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan ClickhouseDatabaseModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state ClickhouseDatabaseModel
+ 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.ClickhouseID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "dbName": resources_core.FormatString(plan.DbName),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *ClickhouseDatabaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state ClickhouseDatabaseModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_database" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.ClickhouseID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "dbName": resources_core.FormatString(state.DbName),
+ }
+ identityCodes := []string{
+ "dbName",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", 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{
+ "dbName": resources_core.FormatString(state.DbName),
+ })
+
+ 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, "delete_database", 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 *ClickhouseDatabaseResource) 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
+}
diff --git a/generated/prod/go/120_clickhouse_resource.go b/generated/prod/go/120_clickhouse_resource.go
new file mode 100644
index 0000000..53dcba4
--- /dev/null
+++ b/generated/prod/go/120_clickhouse_resource.go
@@ -0,0 +1,487 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: clickhouse
+// Service ID: 120
+
+var _ resource.Resource = &ClickhouseResource{}
+var _ resource.ResourceWithModifyPlan = &ClickhouseResource{}
+var _ resource.ResourceWithImportState = &ClickhouseResource{}
+
+type ClickhouseResource struct {
+ client *core.UniversalClient
+}
+
+// ClickhouseAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type ClickhouseAccessConfigurationModel struct {
+ MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"`
+ MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"`
+}
+
+// ClickhouseAutoscaleConfigurationModel — вложенная модель для map-fixed параметра autoscaleConfiguration.
+type ClickhouseAutoscaleConfigurationModel struct {
+ Enabled types.Bool `tfsdk:"enabled" json:"enabled"`
+ Schedule types.Int64 `tfsdk:"schedule" json:"schedule"`
+ Quota types.Int64 `tfsdk:"quota" json:"quota"`
+ Percent types.Int64 `tfsdk:"percent" json:"percent"`
+}
+
+// ClickhouseBackupConfigurationModel — вложенная модель для map-fixed параметра backupConfiguration.
+type ClickhouseBackupConfigurationModel struct {
+ S3Uid types.String `tfsdk:"s3_uid" json:"s3Uid"`
+ Retain types.Int64 `tfsdk:"retain" json:"retain"`
+ Schedule types.String `tfsdk:"schedule" json:"schedule"`
+}
+
+// ClickhouseClickhouseConfigurationModel — вложенная модель для map-fixed параметра clickhouseConfiguration.
+type ClickhouseClickhouseConfigurationModel struct {
+ Version types.String `tfsdk:"version" json:"version"`
+}
+
+// ClickhouseClickhouseKeeperConfigurationModel — вложенная модель для map-fixed параметра clickhouseKeeperConfiguration.
+type ClickhouseClickhouseKeeperConfigurationModel struct {
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+}
+
+// ClickhouseClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type ClickhouseClusterConfigurationModel struct {
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+ Shards types.Int64 `tfsdk:"shards" json:"shards"`
+ Disk types.Int64 `tfsdk:"disk" json:"disk"`
+}
+
+// ClickhouseStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type ClickhouseStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+}
+
+type ClickhouseModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AccessConfiguration *ClickhouseAccessConfigurationModel `tfsdk:"access_configuration"`
+ AutoscaleConfiguration *ClickhouseAutoscaleConfigurationModel `tfsdk:"autoscale_configuration"`
+ BackupConfiguration *ClickhouseBackupConfigurationModel `tfsdk:"backup_configuration"`
+ ClickhouseConfiguration *ClickhouseClickhouseConfigurationModel `tfsdk:"clickhouse_configuration"`
+ ClickhouseFilesConfiguration types.String `tfsdk:"clickhouse_files_configuration"`
+ ClickhouseKeeperConfiguration *ClickhouseClickhouseKeeperConfigurationModel `tfsdk:"clickhouse_keeper_configuration"`
+ ClusterConfiguration *ClickhouseClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ StartupConfiguration *ClickhouseStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewClickhouseResource() resource.Resource {
+ return &ClickhouseResource{}
+}
+
+func (r *ClickhouseResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_clickhouse"
+}
+
+func (r *ClickhouseResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "master_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"},
+ "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"},
+ }},
+ "autoscale_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "enabled": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает автоскейлинг PV. При включении параметра необходимо настроить autoScalePercentage и autoScaleTechWindow. Алерт при срабатывании вызывает modify у текущей услуги"},
+ "schedule": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "Требует включения autoScale. Окно обновления (пока не реализовано). Указывается час в виде Integer (0 / 5 / 12 / 23)"},
+ "quota": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100), MarkdownDescription: "До какого размера возможно увеличивать диск, когда включен Autoscale"},
+ "percent": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Требует включения autoScale. Позволяет указать в процентах расширение от текущего макс объема
Минимальный размер на который расширяется - **1Gb**"},
+ }},
+ "backup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "s3_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Экземпляр S3 обязателен"},
+ "retain": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(7)},
+ "schedule": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("0 0 * * *")},
+ }},
+ "clickhouse_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("25.10")},
+ }},
+ "clickhouse_files_configuration": schema.StringAttribute{Optional: true, MarkdownDescription: "Формат:
- path: somethings3.xml
xmlConfig: '<clickhouse>......</clickhouse>'"},
+ "clickhouse_keeper_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Указывается в Megabytes"},
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "Если указан 0, то инстансы ClickHouse Keeper развёрнуты не будут"},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1000), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2048), MarkdownDescription: "Указывается в Megabytes"},
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ "shards": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "**Шард** - это часть данных, разделенная на разные узлы (серверы) для горизонтального масштабирования и обработки больших объемов информации.
**Реплика** - это точная копия данных внутри одного шарда"},
+ "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Указывается в GIGAbytes"},
+ }},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"},
+ }},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *ClickhouseResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *ClickhouseModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *ClickhouseModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan ClickhouseModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 837: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}),
+ 838: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "shards": fmt.Sprintf("%d", config.ClusterConfiguration.Shards.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64())}),
+ 839: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString())}),
+ 840: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClickhouseKeeperConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClickhouseKeeperConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClickhouseKeeperConfiguration.Replicas.ValueInt64())}),
+ 841: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.ClickhouseConfiguration.Version.ValueString())}),
+ 842: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", config.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", config.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", config.BackupConfiguration.Schedule.ValueString())}),
+ 843: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", config.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", config.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", config.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", config.AutoscaleConfiguration.Percent.ValueInt64())}),
+ 851: resources_core.FormatString(config.ClickhouseFilesConfiguration),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 120, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *ClickhouseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data ClickhouseModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 120, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 837: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}),
+ 838: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "shards": fmt.Sprintf("%d", data.ClusterConfiguration.Shards.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64())}),
+ 839: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString())}),
+ 840: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClickhouseKeeperConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClickhouseKeeperConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClickhouseKeeperConfiguration.Replicas.ValueInt64())}),
+ 841: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.ClickhouseConfiguration.Version.ValueString())}),
+ 842: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", data.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", data.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", data.BackupConfiguration.Schedule.ValueString())}),
+ 843: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", data.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", data.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", data.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", data.AutoscaleConfiguration.Percent.ValueInt64())}),
+ 851: resources_core.FormatString(data.ClickhouseFilesConfiguration),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 120, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 120, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"},
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clickhouseConfiguration", Field: "ClickhouseConfiguration", Type: "map-fixed"},
+ {Code: "clickhouseFilesConfiguration", Field: "ClickhouseFilesConfiguration", Type: "array-map-fixed"},
+ {Code: "clickhouseKeeperConfiguration", Field: "ClickhouseKeeperConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *ClickhouseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state ClickhouseModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 120, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"},
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clickhouseConfiguration", Field: "ClickhouseConfiguration", Type: "map-fixed"},
+ {Code: "clickhouseFilesConfiguration", Field: "ClickhouseFilesConfiguration", Type: "array-map-fixed"},
+ {Code: "clickhouseKeeperConfiguration", Field: "ClickhouseKeeperConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *ClickhouseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan ClickhouseModel
+ var state ClickhouseModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.ClickhouseFilesConfiguration.IsNull() != state.ClickhouseFilesConfiguration.IsNull() || plan.ClickhouseFilesConfiguration.IsUnknown() != state.ClickhouseFilesConfiguration.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ClickhouseFilesConfiguration.IsNull() && !plan.ClickhouseFilesConfiguration.IsUnknown() && !state.ClickhouseFilesConfiguration.IsNull() && !state.ClickhouseFilesConfiguration.IsUnknown() {
+ if !strings.EqualFold(plan.ClickhouseFilesConfiguration.ValueString(), state.ClickhouseFilesConfiguration.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 844: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "shards": fmt.Sprintf("%d", plan.ClusterConfiguration.Shards.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64())}),
+ 845: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClickhouseKeeperConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClickhouseKeeperConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClickhouseKeeperConfiguration.Replicas.ValueInt64())}),
+ 846: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString())}),
+ 847: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.ClickhouseConfiguration.Version.ValueString())}),
+ 848: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", plan.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.Schedule.ValueString())}),
+ 849: resources_core.BuildJSON(map[string]string{"quota": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Percent.ValueInt64()), "enabled": fmt.Sprintf("%v", plan.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Schedule.ValueInt64())}),
+ 850: resources_core.FormatString(plan.ClickhouseFilesConfiguration),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 120, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"},
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clickhouseConfiguration", Field: "ClickhouseConfiguration", Type: "map-fixed"},
+ {Code: "clickhouseFilesConfiguration", Field: "ClickhouseFilesConfiguration", Type: "array-map-fixed"},
+ {Code: "clickhouseKeeperConfiguration", Field: "ClickhouseKeeperConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *ClickhouseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state ClickhouseModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *ClickhouseResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *ClickhouseResource) 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
+}
diff --git a/generated/prod/go/120_clickhouse_user_resource.go b/generated/prod/go/120_clickhouse_user_resource.go
new file mode 100644
index 0000000..8dc28f7
--- /dev/null
+++ b/generated/prod/go/120_clickhouse_user_resource.go
@@ -0,0 +1,359 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: clickhouse
+// Subresource: user
+
+var _ resource.Resource = &ClickhouseUserResource{}
+
+// ClickhouseUserResource manages a subresource via operations.
+type ClickhouseUserResource struct {
+ client *core.UniversalClient
+}
+
+type ClickhouseUserModel struct {
+ ID types.String `tfsdk:"id"`
+ ClickhouseID types.String `tfsdk:"clickhouse_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"`
+ AccessHosts types.String `tfsdk:"access_hosts"`
+ DbName types.String `tfsdk:"db_name"`
+ Username types.String `tfsdk:"username"`
+}
+
+func NewClickhouseUserResource() resource.Resource {
+ return &ClickhouseUserResource{}
+}
+
+func (r *ClickhouseUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_clickhouse_user"
+}
+
+func (r *ClickhouseUserResource) 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()}},
+ "clickhouse_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."},
+ "access_hosts": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("::/0"), MarkdownDescription: "Пример: `::/0` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "db_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "username": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("myusername"), MarkdownDescription: "Пример: `myusername` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *ClickhouseUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan ClickhouseUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.ClickhouseID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "accessHosts": resources_core.FormatString(plan.AccessHosts),
+ "dbName": resources_core.FormatString(plan.DbName),
+ "username": resources_core.FormatString(plan.Username),
+ }
+ identityCodes := []string{
+ "accessHosts",
+ "dbName",
+ "username",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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, "user", 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{
+ "username": resources_core.FormatString(plan.Username),
+ "dbName": resources_core.FormatString(plan.DbName),
+ "accessHosts": resources_core.FormatString(plan.AccessHosts),
+ })
+
+ 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, "create_user", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", 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, "user", 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, "user", 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, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *ClickhouseUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state ClickhouseUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *ClickhouseUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan ClickhouseUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state ClickhouseUserModel
+ 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.ClickhouseID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "accessHosts": resources_core.FormatString(plan.AccessHosts),
+ "dbName": resources_core.FormatString(plan.DbName),
+ "username": resources_core.FormatString(plan.Username),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *ClickhouseUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state ClickhouseUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_user" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.ClickhouseID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "accessHosts": resources_core.FormatString(state.AccessHosts),
+ "dbName": resources_core.FormatString(state.DbName),
+ "username": resources_core.FormatString(state.Username),
+ }
+ identityCodes := []string{
+ "accessHosts",
+ "dbName",
+ "username",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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{
+ "username": resources_core.FormatString(state.Username),
+ })
+
+ 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, "delete_user", 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 *ClickhouseUserResource) 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
+}
diff --git a/generated/prod/go/12_s3_resource.go b/generated/prod/go/12_s3_resource.go
new file mode 100644
index 0000000..6347765
--- /dev/null
+++ b/generated/prod/go/12_s3_resource.go
@@ -0,0 +1,421 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: s3
+// Service ID: 12
+
+var _ resource.Resource = &S3Resource{}
+var _ resource.ResourceWithModifyPlan = &S3Resource{}
+var _ resource.ResourceWithImportState = &S3Resource{}
+
+type S3Resource struct {
+ client *core.UniversalClient
+}
+
+type S3Model struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ DisplayName types.String `tfsdk:"display_name"`
+ MaxBucketsPerUser types.Int64 `tfsdk:"max_buckets_per_user"`
+ MaxObjectsPerBucket types.Int64 `tfsdk:"max_objects_per_bucket"`
+ MaxSizeGbPerUser types.Int64 `tfsdk:"max_size_gb_per_user"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewS3Resource() resource.Resource {
+ return &S3Resource{}
+}
+
+func (r *S3Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_s3"
+}
+
+func (r *S3Resource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "display_name": schema.StringAttribute{Required: true, MarkdownDescription: "Человеко-читаемое имя (транслит из Elma) - передаётся имя клиента Не изменяется после создания."},
+ "max_buckets_per_user": schema.Int64Attribute{Optional: true, MarkdownDescription: "Квота на пользователя. Количество бакетов пользователя. Передаём руками"},
+ "max_objects_per_bucket": schema.Int64Attribute{Optional: true, MarkdownDescription: "Квота на бакет. Кол-во объектов. Передаём руками"},
+ "max_size_gb_per_user": schema.Int64Attribute{Optional: true, MarkdownDescription: "maxSizeGb. Передаём руками"},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("s3.msk-1.ngcloud.ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *S3Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *S3Model
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *S3Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan S3Model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.DisplayName.IsNull() && !plan.DisplayName.IsUnknown() && !state.DisplayName.IsNull() && !state.DisplayName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.DisplayName.ValueString(), state.DisplayName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить display_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.DisplayName.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "display_name is required.")
+ return
+ }
+ if config.DisplayName.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 163: resources_core.FormatString(config.ResourceRealm),
+ 320: resources_core.FormatString(config.DisplayName),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 12, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *S3Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data S3Model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.DisplayName.IsNull() || data.DisplayName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "display_name is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 12, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 163: resources_core.FormatString(data.ResourceRealm),
+ 320: resources_core.FormatString(data.DisplayName),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 12, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 12, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "displayName", Field: "DisplayName", Type: "string"},
+ {Code: "maxBucketsPerUser", Field: "MaxBucketsPerUser", Type: "int64"},
+ {Code: "maxObjectsPerBucket", Field: "MaxObjectsPerBucket", Type: "int64"},
+ {Code: "maxSizeGbPerUser", Field: "MaxSizeGbPerUser", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *S3Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state S3Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 12, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "displayName", Field: "DisplayName", Type: "string"},
+ {Code: "maxBucketsPerUser", Field: "MaxBucketsPerUser", Type: "int64"},
+ {Code: "maxObjectsPerBucket", Field: "MaxObjectsPerBucket", Type: "int64"},
+ {Code: "maxSizeGbPerUser", Field: "MaxSizeGbPerUser", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *S3Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan S3Model
+ var state S3Model
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.MaxSizeGbPerUser.IsNull() != state.MaxSizeGbPerUser.IsNull() || plan.MaxSizeGbPerUser.IsUnknown() != state.MaxSizeGbPerUser.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.MaxSizeGbPerUser.IsNull() && !plan.MaxSizeGbPerUser.IsUnknown() && !state.MaxSizeGbPerUser.IsNull() && !state.MaxSizeGbPerUser.IsUnknown() {
+ if plan.MaxSizeGbPerUser.ValueInt64() != state.MaxSizeGbPerUser.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.MaxObjectsPerBucket.IsNull() != state.MaxObjectsPerBucket.IsNull() || plan.MaxObjectsPerBucket.IsUnknown() != state.MaxObjectsPerBucket.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.MaxObjectsPerBucket.IsNull() && !plan.MaxObjectsPerBucket.IsUnknown() && !state.MaxObjectsPerBucket.IsNull() && !state.MaxObjectsPerBucket.IsUnknown() {
+ if plan.MaxObjectsPerBucket.ValueInt64() != state.MaxObjectsPerBucket.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.MaxBucketsPerUser.IsNull() != state.MaxBucketsPerUser.IsNull() || plan.MaxBucketsPerUser.IsUnknown() != state.MaxBucketsPerUser.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.MaxBucketsPerUser.IsNull() && !plan.MaxBucketsPerUser.IsUnknown() && !state.MaxBucketsPerUser.IsNull() && !state.MaxBucketsPerUser.IsUnknown() {
+ if plan.MaxBucketsPerUser.ValueInt64() != state.MaxBucketsPerUser.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 50: resources_core.FormatInt64(plan.MaxSizeGbPerUser),
+ 51: resources_core.FormatInt64(plan.MaxObjectsPerBucket),
+ 52: resources_core.FormatInt64(plan.MaxBucketsPerUser),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 12, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "displayName", Field: "DisplayName", Type: "string"},
+ {Code: "maxBucketsPerUser", Field: "MaxBucketsPerUser", Type: "int64"},
+ {Code: "maxObjectsPerBucket", Field: "MaxObjectsPerBucket", Type: "int64"},
+ {Code: "maxSizeGbPerUser", Field: "MaxSizeGbPerUser", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *S3Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state S3Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *S3Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *S3Resource) 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
+}
diff --git a/generated/prod/go/12_s3_sub_user_resource.go b/generated/prod/go/12_s3_sub_user_resource.go
new file mode 100644
index 0000000..b8e0168
--- /dev/null
+++ b/generated/prod/go/12_s3_sub_user_resource.go
@@ -0,0 +1,358 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: s3
+// Subresource: sub_user
+
+var _ resource.Resource = &S3SubUserResource{}
+
+// S3SubUserResource manages a subresource via operations.
+type S3SubUserResource struct {
+ client *core.UniversalClient
+}
+
+type S3SubUserModel struct {
+ ID types.String `tfsdk:"id"`
+ S3ID types.String `tfsdk:"s3_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"`
+ SubUserAccess types.String `tfsdk:"sub_user_access"`
+ SubUserName types.String `tfsdk:"sub_user_name"`
+}
+
+func NewS3SubUserResource() resource.Resource {
+ return &S3SubUserResource{}
+}
+
+func (r *S3SubUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_s3_sub_user"
+}
+
+func (r *S3SubUserResource) 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()}},
+ "s3_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."},
+ "sub_user_access": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "sub_user_name": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *S3SubUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan S3SubUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if plan.SubUserAccess.IsNull() || plan.SubUserAccess.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "sub_user_access is required.")
+ return
+ }
+ if plan.SubUserName.IsNull() || plan.SubUserName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "sub_user_name is required.")
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.S3ID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "subUserAccess": resources_core.FormatString(plan.SubUserAccess),
+ "subUserName": resources_core.FormatString(plan.SubUserName),
+ }
+ identityCodes := []string{
+ "subUserAccess",
+ "subUserName",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("sub_user", 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, "sub_user", 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{
+ "subUserName": resources_core.FormatString(plan.SubUserName),
+ "subUserAccess": resources_core.FormatString(plan.SubUserAccess),
+ })
+
+ 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, "create_sub_user", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_sub_user", 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, "sub_user", 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, "sub_user", 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, "sub_user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *S3SubUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state S3SubUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *S3SubUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan S3SubUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state S3SubUserModel
+ 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.S3ID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "subUserAccess": resources_core.FormatString(plan.SubUserAccess),
+ "subUserName": resources_core.FormatString(plan.SubUserName),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "sub_user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *S3SubUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state S3SubUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_sub_user" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.S3ID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "subUserAccess": resources_core.FormatString(state.SubUserAccess),
+ "subUserName": resources_core.FormatString(state.SubUserName),
+ }
+ identityCodes := []string{
+ "subUserAccess",
+ "subUserName",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("sub_user", 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{
+ "subUserName": resources_core.FormatString(state.SubUserName),
+ })
+
+ 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, "delete_sub_user", 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 *S3SubUserResource) 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
+}
diff --git a/generated/prod/go/13_s3bucket_resource.go b/generated/prod/go/13_s3bucket_resource.go
new file mode 100644
index 0000000..c45905c
--- /dev/null
+++ b/generated/prod/go/13_s3bucket_resource.go
@@ -0,0 +1,478 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: s3bucket
+// Service ID: 13
+
+var _ resource.Resource = &S3bucketResource{}
+var _ resource.ResourceWithModifyPlan = &S3bucketResource{}
+var _ resource.ResourceWithImportState = &S3bucketResource{}
+
+type S3bucketResource struct {
+ client *core.UniversalClient
+}
+
+type S3bucketModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ BucketName types.String `tfsdk:"bucket_name"`
+ CorsAll types.Bool `tfsdk:"cors_all"`
+ ListAll types.Bool `tfsdk:"list_all"`
+ MaxSize types.String `tfsdk:"max_size"`
+ Placement types.String `tfsdk:"placement"`
+ ReadAll types.Bool `tfsdk:"read_all"`
+ S3UserUid types.String `tfsdk:"s3_user_uid"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewS3bucketResource() resource.Resource {
+ return &S3bucketResource{}
+}
+
+func (r *S3bucketResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_s3bucket"
+}
+
+func (r *S3bucketResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "bucket_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя бакета s3 Не изменяется после создания."},
+ "cors_all": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Публичный доступ к чтению настроек позволяет любому пользователю интернета читать конфигурации CORS Не изменяется после создания."},
+ "list_all": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Публичный доступ к листингу позволяет любому пользователю интернета получить список объектов в бакете. Не изменяется после создания."},
+ "max_size": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("-1"), MarkdownDescription: "Максимальный размер бакета s3. Если поставить -1, будет безлимит. Размер указывается в Bytes Не изменяется после создания."},
+ "placement": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("HOT"), MarkdownDescription: "Класс хранилища, который используется по умолчанию при загрузке объектов. Чем холоднее, тем дешевле место, но дороже обращения Не изменяется после создания."},
+ "read_all": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Публичный доступ на чтение позволяет любому пользователю интернета скачивать объекты из бакета. Не изменяется после создания."},
+ "s3_user_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UID Корневой услуги S3. На основе него будет создан бакет. Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *S3bucketResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *S3bucketModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *S3bucketModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan S3bucketModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.BucketName.IsNull() && !plan.BucketName.IsUnknown() && !state.BucketName.IsNull() && !state.BucketName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.BucketName.ValueString(), state.BucketName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить bucket_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.CorsAll.IsNull() && !plan.CorsAll.IsUnknown() && !state.CorsAll.IsNull() && !state.CorsAll.IsUnknown() {
+ if plan.CorsAll.ValueBool() != state.CorsAll.ValueBool() {
+ resp.Diagnostics.AddError("Нельзя изменить cors_all", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ListAll.IsNull() && !plan.ListAll.IsUnknown() && !state.ListAll.IsNull() && !state.ListAll.IsUnknown() {
+ if plan.ListAll.ValueBool() != state.ListAll.ValueBool() {
+ resp.Diagnostics.AddError("Нельзя изменить list_all", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.MaxSize.IsNull() && !plan.MaxSize.IsUnknown() && !state.MaxSize.IsNull() && !state.MaxSize.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.MaxSize.ValueString(), state.MaxSize.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить max_size", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.Placement.IsNull() && !plan.Placement.IsUnknown() && !state.Placement.IsNull() && !state.Placement.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Placement.ValueString(), state.Placement.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить placement", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ReadAll.IsNull() && !plan.ReadAll.IsUnknown() && !state.ReadAll.IsNull() && !state.ReadAll.IsUnknown() {
+ if plan.ReadAll.ValueBool() != state.ReadAll.ValueBool() {
+ resp.Diagnostics.AddError("Нельзя изменить read_all", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.S3UserUid.IsNull() && !plan.S3UserUid.IsUnknown() && !state.S3UserUid.IsNull() && !state.S3UserUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.S3UserUid.ValueString(), state.S3UserUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить s3_user_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.BucketName.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "bucket_name is required.")
+ return
+ }
+ if config.BucketName.IsUnknown() {
+ return
+ }
+ if config.S3UserUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "s3_user_uid is required.")
+ return
+ }
+ if config.S3UserUid.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.S3UserUid.IsNull() && !config.S3UserUid.IsUnknown() {
+ resolvedS3UserUid, err := r.client.ResolveRefSvcParamValue(ctx, 12, config.S3UserUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve s3_user_uid", err.Error())
+ } else if resolvedS3UserUid != "" && resolvedS3UserUid != config.S3UserUid.ValueString() {
+ config.S3UserUid = types.StringValue(resolvedS3UserUid)
+ }
+ }
+ params := map[int]string{
+ 124: resources_core.FormatString(config.S3UserUid),
+ 125: resources_core.FormatString(config.BucketName),
+ 126: resources_core.FormatString(config.MaxSize),
+ 127: resources_core.FormatBool(config.ReadAll),
+ 128: resources_core.FormatBool(config.ListAll),
+ 129: resources_core.FormatBool(config.CorsAll),
+ 130: resources_core.FormatString(config.Placement),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 13, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *S3bucketResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data S3bucketModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.BucketName.IsNull() || data.BucketName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "bucket_name is required.")
+ return
+ }
+ if data.S3UserUid.IsNull() || data.S3UserUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "s3_user_uid is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 13, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 124: resources_core.FormatString(data.S3UserUid),
+ 125: resources_core.FormatString(data.BucketName),
+ 126: resources_core.FormatString(data.MaxSize),
+ 127: resources_core.FormatBool(data.ReadAll),
+ 128: resources_core.FormatBool(data.ListAll),
+ 129: resources_core.FormatBool(data.CorsAll),
+ 130: resources_core.FormatString(data.Placement),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 13, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 13, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "bucketName", Field: "BucketName", Type: "string"},
+ {Code: "corsAll", Field: "CorsAll", Type: "bool"},
+ {Code: "listAll", Field: "ListAll", Type: "bool"},
+ {Code: "maxSize", Field: "MaxSize", Type: "string"},
+ {Code: "placement", Field: "Placement", Type: "string"},
+ {Code: "readAll", Field: "ReadAll", Type: "bool"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *S3bucketResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state S3bucketModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 13, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "bucketName", Field: "BucketName", Type: "string"},
+ {Code: "corsAll", Field: "CorsAll", Type: "bool"},
+ {Code: "listAll", Field: "ListAll", Type: "bool"},
+ {Code: "maxSize", Field: "MaxSize", Type: "string"},
+ {Code: "placement", Field: "Placement", Type: "string"},
+ {Code: "readAll", Field: "ReadAll", Type: "bool"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *S3bucketResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan S3bucketModel
+ var state S3bucketModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 13, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "bucketName", Field: "BucketName", Type: "string"},
+ {Code: "corsAll", Field: "CorsAll", Type: "bool"},
+ {Code: "listAll", Field: "ListAll", Type: "bool"},
+ {Code: "maxSize", Field: "MaxSize", Type: "string"},
+ {Code: "placement", Field: "Placement", Type: "string"},
+ {Code: "readAll", Field: "ReadAll", Type: "bool"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *S3bucketResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state S3bucketModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *S3bucketResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *S3bucketResource) 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
+}
diff --git a/generated/prod/go/148_vc_mgmt_sthutrval_cluster_resource.go b/generated/prod/go/148_vc_mgmt_sthutrval_cluster_resource.go
new file mode 100644
index 0000000..92f4d9f
--- /dev/null
+++ b/generated/prod/go/148_vc_mgmt_sthutrval_cluster_resource.go
@@ -0,0 +1,419 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_mgmt_sthutrval_cluster
+// Service ID: 148
+
+var _ resource.Resource = &VcMgmtSthutrvalClusterResource{}
+var _ resource.ResourceWithModifyPlan = &VcMgmtSthutrvalClusterResource{}
+var _ resource.ResourceWithImportState = &VcMgmtSthutrvalClusterResource{}
+
+type VcMgmtSthutrvalClusterResource struct {
+ client *core.UniversalClient
+}
+
+// VcMgmtSthutrvalClusterAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type VcMgmtSthutrvalClusterAccessConfigurationModel struct {
+ NeedExternalAddressAPI types.Bool `tfsdk:"need_external_address_a_p_i" json:"needExternalAddressAPI"`
+ AccessIpListAPI types.String `tfsdk:"access_ip_list_a_p_i" json:"accessIpListAPI"`
+ NeedExternalAddressIngress types.Bool `tfsdk:"need_external_address_ingress" json:"needExternalAddressIngress"`
+ AccessIpListIngress types.String `tfsdk:"access_ip_list_ingress" json:"accessIpListIngress"`
+}
+
+// VcMgmtSthutrvalClusterClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type VcMgmtSthutrvalClusterClusterConfigurationModel struct {
+ AppVersion types.String `tfsdk:"app_version" json:"appVersion"`
+}
+
+// VcMgmtSthutrvalClusterControlPlaneConfigurationModel — вложенная модель для map-fixed параметра controlPlaneConfiguration.
+type VcMgmtSthutrvalClusterControlPlaneConfigurationModel struct {
+ Count types.Int64 `tfsdk:"count" json:"count"`
+ SizingPolicy types.String `tfsdk:"sizing_policy" json:"sizingPolicy"`
+ SizingDisk types.Int64 `tfsdk:"sizing_disk" json:"sizingDisk"`
+}
+
+// VcMgmtSthutrvalClusterStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type VcMgmtSthutrvalClusterStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+ ClusterName types.String `tfsdk:"cluster_name" json:"clusterName"`
+}
+
+type VcMgmtSthutrvalClusterModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AccessConfiguration *VcMgmtSthutrvalClusterAccessConfigurationModel `tfsdk:"access_configuration"`
+ ClusterConfiguration *VcMgmtSthutrvalClusterClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ ControlPlaneConfiguration *VcMgmtSthutrvalClusterControlPlaneConfigurationModel `tfsdk:"control_plane_configuration"`
+ StartupConfiguration *VcMgmtSthutrvalClusterStartupConfigurationModel `tfsdk:"startup_configuration"`
+ WorkerConfiguration types.String `tfsdk:"worker_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVcMgmtSthutrvalClusterResource() resource.Resource {
+ return &VcMgmtSthutrvalClusterResource{}
+}
+
+func (r *VcMgmtSthutrvalClusterResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_mgmt_sthutrval_cluster"
+}
+
+func (r *VcMgmtSthutrvalClusterResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "need_external_address_a_p_i": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Выделяет внешний IP для доступа к kubernetes API. Пока нельзя выставить false"},
+ "access_ip_list_a_p_i": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Access IP List для подключения к Kubernetes API. Указывается массив. Если передан пустой массив, доступ выделяется всем
Пример: **["0.0.0.0/0"]** или **[]** или **["178.4.13.2", "178.4.14.0/24"]**"},
+ "need_external_address_ingress": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Выделяет внешний IP для доступа к Ingress"},
+ "access_ip_list_ingress": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Access IP List для подключения к Ingress. Указывается массив. Если передан пустой массив, доступ выделяется всем
Пример: **["0.0.0.0/0"]** или **[]** или **["178.4.13.2", "178.4.14.0/24"]**"},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "app_version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("2.13.1")},
+ }},
+ "control_plane_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "count": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ "sizing_policy": schema.StringAttribute{Required: true, MarkdownDescription: "Доступные sizing Policy представлены исходя из ресурсной платформы, которая была выбрана"},
+ "sizing_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(50)},
+ }},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("ngcloud.ru"), MarkdownDescription: "Облако в котором будет развёрнут тенант"},
+ "cluster_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("example-ewwnrebe")},
+ }},
+ "worker_configuration": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcMgmtSthutrvalClusterResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VcMgmtSthutrvalClusterModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VcMgmtSthutrvalClusterModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VcMgmtSthutrvalClusterModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.WorkerConfiguration.IsNull() && !plan.WorkerConfiguration.IsUnknown() && !state.WorkerConfiguration.IsNull() && !state.WorkerConfiguration.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.WorkerConfiguration.ValueString(), state.WorkerConfiguration.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить worker_configuration", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 771: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString()), "clusterName": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ClusterName.ValueString())}),
+ 772: resources_core.BuildJSON(map[string]string{"appVersion": fmt.Sprintf("\"%s\"", config.ClusterConfiguration.AppVersion.ValueString())}),
+ 773: resources_core.BuildJSON(map[string]string{"count": fmt.Sprintf("%d", config.ControlPlaneConfiguration.Count.ValueInt64()), "sizingPolicy": fmt.Sprintf("\"%s\"", config.ControlPlaneConfiguration.SizingPolicy.ValueString()), "sizingDisk": fmt.Sprintf("%d", config.ControlPlaneConfiguration.SizingDisk.ValueInt64())}),
+ 774: resources_core.FormatString(config.WorkerConfiguration),
+ 775: resources_core.BuildJSON(map[string]string{"needExternalAddressAPI": fmt.Sprintf("%v", config.AccessConfiguration.NeedExternalAddressAPI.ValueBool()), "accessIpListAPI": fmt.Sprintf("\"%s\"", config.AccessConfiguration.AccessIpListAPI.ValueString()), "needExternalAddressIngress": fmt.Sprintf("%v", config.AccessConfiguration.NeedExternalAddressIngress.ValueBool()), "accessIpListIngress": fmt.Sprintf("\"%s\"", config.AccessConfiguration.AccessIpListIngress.ValueString())}),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 148, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VcMgmtSthutrvalClusterResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VcMgmtSthutrvalClusterModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 148, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 771: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString()), "clusterName": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ClusterName.ValueString())}),
+ 772: resources_core.BuildJSON(map[string]string{"appVersion": fmt.Sprintf("\"%s\"", data.ClusterConfiguration.AppVersion.ValueString())}),
+ 773: resources_core.BuildJSON(map[string]string{"count": fmt.Sprintf("%d", data.ControlPlaneConfiguration.Count.ValueInt64()), "sizingPolicy": fmt.Sprintf("\"%s\"", data.ControlPlaneConfiguration.SizingPolicy.ValueString()), "sizingDisk": fmt.Sprintf("%d", data.ControlPlaneConfiguration.SizingDisk.ValueInt64())}),
+ 774: resources_core.FormatString(data.WorkerConfiguration),
+ 775: resources_core.BuildJSON(map[string]string{"needExternalAddressAPI": fmt.Sprintf("%v", data.AccessConfiguration.NeedExternalAddressAPI.ValueBool()), "accessIpListAPI": fmt.Sprintf("\"%s\"", data.AccessConfiguration.AccessIpListAPI.ValueString()), "needExternalAddressIngress": fmt.Sprintf("%v", data.AccessConfiguration.NeedExternalAddressIngress.ValueBool()), "accessIpListIngress": fmt.Sprintf("\"%s\"", data.AccessConfiguration.AccessIpListIngress.ValueString())}),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 148, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 148, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcMgmtSthutrvalClusterResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcMgmtSthutrvalClusterModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 148, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VcMgmtSthutrvalClusterResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcMgmtSthutrvalClusterModel
+ var state VcMgmtSthutrvalClusterModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 148, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcMgmtSthutrvalClusterResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcMgmtSthutrvalClusterModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VcMgmtSthutrvalClusterResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VcMgmtSthutrvalClusterResource) 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
+}
diff --git a/generated/prod/go/149_valo_tenant_resource.go b/generated/prod/go/149_valo_tenant_resource.go
new file mode 100644
index 0000000..37670d5
--- /dev/null
+++ b/generated/prod/go/149_valo_tenant_resource.go
@@ -0,0 +1,431 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: valo_tenant
+// Service ID: 149
+
+var _ resource.Resource = &ValoTenantResource{}
+var _ resource.ResourceWithModifyPlan = &ValoTenantResource{}
+var _ resource.ResourceWithImportState = &ValoTenantResource{}
+
+type ValoTenantResource struct {
+ client *core.UniversalClient
+}
+
+// ValoTenantValoOptionsModel — вложенная модель для map-fixed параметра valoOptions.
+type ValoTenantValoOptionsModel struct {
+ FfUserImport types.Bool `tfsdk:"ff_user_import" json:"ffUserImport"`
+ FfThemes types.Bool `tfsdk:"ff_themes" json:"ffThemes"`
+ FfPublicLink types.Bool `tfsdk:"ff_public_link" json:"ffPublicLink"`
+ FfLogging types.Bool `tfsdk:"ff_logging" json:"ffLogging"`
+ UserCount types.Int64 `tfsdk:"user_count" json:"userCount"`
+ RequestApproveMaxPeriod types.Int64 `tfsdk:"request_approve_max_period" json:"requestApproveMaxPeriod"`
+ FfOnlineEditor types.Bool `tfsdk:"ff_online_editor" json:"ffOnlineEditor"`
+ FfWatermark types.Bool `tfsdk:"ff_watermark" json:"ffWatermark"`
+}
+
+type ValoTenantModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ Domain types.String `tfsdk:"domain"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ S3Storage types.Int64 `tfsdk:"s3_storage"`
+ ValoOptions *ValoTenantValoOptionsModel `tfsdk:"valo_options"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewValoTenantResource() resource.Resource {
+ return &ValoTenantResource{}
+}
+
+func (r *ValoTenantResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_valo_tenant"
+}
+
+func (r *ValoTenantResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "domain": schema.StringAttribute{Required: true, MarkdownDescription: "Будет добавлено в зону valo.ngcloud.ru Не изменяется после создания."},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("valo.ngcloud.ru"), MarkdownDescription: "Не изменяется после создания."},
+ "s3_storage": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Параметр указывается в `Gb`"},
+ "valo_options": schema.SingleNestedAttribute{Optional: true, Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "ff_user_import": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"},
+ "ff_themes": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"},
+ "ff_public_link": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"},
+ "ff_logging": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"},
+ "user_count": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10)},
+ "request_approve_max_period": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2), MarkdownDescription: "Максимальное значение периода согласования (в днях)"},
+ "ff_online_editor": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"},
+ "ff_watermark": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"},
+ }},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *ValoTenantResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *ValoTenantModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *ValoTenantModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan ValoTenantModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.Domain.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "domain is required.")
+ return
+ }
+ if config.Domain.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 655: resources_core.FormatString(config.ResourceRealm),
+ 656: resources_core.FormatString(config.Domain),
+ 657: resources_core.BuildJSON(map[string]string{"ffUserImport": fmt.Sprintf("%v", config.ValoOptions.FfUserImport.ValueBool()), "ffThemes": fmt.Sprintf("%v", config.ValoOptions.FfThemes.ValueBool()), "ffPublicLink": fmt.Sprintf("%v", config.ValoOptions.FfPublicLink.ValueBool()), "ffLogging": fmt.Sprintf("%v", config.ValoOptions.FfLogging.ValueBool()), "userCount": fmt.Sprintf("%d", config.ValoOptions.UserCount.ValueInt64()), "requestApproveMaxPeriod": fmt.Sprintf("%d", config.ValoOptions.RequestApproveMaxPeriod.ValueInt64()), "ffOnlineEditor": fmt.Sprintf("%v", config.ValoOptions.FfOnlineEditor.ValueBool()), "ffWatermark": fmt.Sprintf("%v", config.ValoOptions.FfWatermark.ValueBool())}),
+ 658: resources_core.FormatInt64(config.S3Storage),
+ }
+ desiredDomain := ""
+ if !config.Domain.IsNull() && !config.Domain.IsUnknown() {
+ desiredDomain = config.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 149, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *ValoTenantResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data ValoTenantModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.Domain.IsNull() || data.Domain.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "domain is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ if !data.Domain.IsNull() && !data.Domain.IsUnknown() {
+ desiredDomain = data.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 149, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 655: resources_core.FormatString(data.ResourceRealm),
+ 656: resources_core.FormatString(data.Domain),
+ 657: resources_core.BuildJSON(map[string]string{"ffUserImport": fmt.Sprintf("%v", data.ValoOptions.FfUserImport.ValueBool()), "ffThemes": fmt.Sprintf("%v", data.ValoOptions.FfThemes.ValueBool()), "ffPublicLink": fmt.Sprintf("%v", data.ValoOptions.FfPublicLink.ValueBool()), "ffLogging": fmt.Sprintf("%v", data.ValoOptions.FfLogging.ValueBool()), "userCount": fmt.Sprintf("%d", data.ValoOptions.UserCount.ValueInt64()), "requestApproveMaxPeriod": fmt.Sprintf("%d", data.ValoOptions.RequestApproveMaxPeriod.ValueInt64()), "ffOnlineEditor": fmt.Sprintf("%v", data.ValoOptions.FfOnlineEditor.ValueBool()), "ffWatermark": fmt.Sprintf("%v", data.ValoOptions.FfWatermark.ValueBool())}),
+ 658: resources_core.FormatInt64(data.S3Storage),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 149, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 149, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "s3Storage", Field: "S3Storage", Type: "int64"},
+ {Code: "valoOptions", Field: "ValoOptions", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *ValoTenantResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state ValoTenantModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 149, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "s3Storage", Field: "S3Storage", Type: "int64"},
+ {Code: "valoOptions", Field: "ValoOptions", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *ValoTenantResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan ValoTenantModel
+ var state ValoTenantModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.S3Storage.IsNull() != state.S3Storage.IsNull() || plan.S3Storage.IsUnknown() != state.S3Storage.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.S3Storage.IsNull() && !plan.S3Storage.IsUnknown() && !state.S3Storage.IsNull() && !state.S3Storage.IsUnknown() {
+ if plan.S3Storage.ValueInt64() != state.S3Storage.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 659: resources_core.BuildJSON(map[string]string{"ffPublicLink": fmt.Sprintf("%v", plan.ValoOptions.FfPublicLink.ValueBool()), "ffLogging": fmt.Sprintf("%v", plan.ValoOptions.FfLogging.ValueBool()), "userCount": fmt.Sprintf("%d", plan.ValoOptions.UserCount.ValueInt64()), "requestApproveMaxPeriod": fmt.Sprintf("%d", plan.ValoOptions.RequestApproveMaxPeriod.ValueInt64()), "ffOnlineEditor": fmt.Sprintf("%v", plan.ValoOptions.FfOnlineEditor.ValueBool()), "ffWatermark": fmt.Sprintf("%v", plan.ValoOptions.FfWatermark.ValueBool()), "ffUserImport": fmt.Sprintf("%v", plan.ValoOptions.FfUserImport.ValueBool()), "ffThemes": fmt.Sprintf("%v", plan.ValoOptions.FfThemes.ValueBool())}),
+ 660: resources_core.FormatInt64(plan.S3Storage),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 149, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "s3Storage", Field: "S3Storage", Type: "int64"},
+ {Code: "valoOptions", Field: "ValoOptions", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *ValoTenantResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state ValoTenantModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *ValoTenantResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *ValoTenantResource) 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
+}
diff --git a/generated/prod/go/150_k8s_sthutrval_cluster_resource.go b/generated/prod/go/150_k8s_sthutrval_cluster_resource.go
new file mode 100644
index 0000000..34f08a9
--- /dev/null
+++ b/generated/prod/go/150_k8s_sthutrval_cluster_resource.go
@@ -0,0 +1,429 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: k8s_sthutrval_cluster
+// Service ID: 150
+
+var _ resource.Resource = &K8sSthutrvalClusterResource{}
+var _ resource.ResourceWithModifyPlan = &K8sSthutrvalClusterResource{}
+var _ resource.ResourceWithImportState = &K8sSthutrvalClusterResource{}
+
+type K8sSthutrvalClusterResource struct {
+ client *core.UniversalClient
+}
+
+// K8sSthutrvalClusterAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type K8sSthutrvalClusterAccessConfigurationModel struct {
+ NeedExternalAddressAPI types.Bool `tfsdk:"need_external_address_a_p_i" json:"needExternalAddressAPI"`
+ AccessIpListAPI types.String `tfsdk:"access_ip_list_a_p_i" json:"accessIpListAPI"`
+ NeedExternalAddressIngress types.Bool `tfsdk:"need_external_address_ingress" json:"needExternalAddressIngress"`
+ AccessIpListIngress types.String `tfsdk:"access_ip_list_ingress" json:"accessIpListIngress"`
+}
+
+// K8sSthutrvalClusterClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type K8sSthutrvalClusterClusterConfigurationModel struct {
+ AppVersion types.String `tfsdk:"app_version" json:"appVersion"`
+}
+
+// K8sSthutrvalClusterControlPlaneConfigurationModel — вложенная модель для map-fixed параметра controlPlaneConfiguration.
+type K8sSthutrvalClusterControlPlaneConfigurationModel struct {
+ SizingPolicy types.String `tfsdk:"sizing_policy" json:"sizingPolicy"`
+ SizingDisk types.Int64 `tfsdk:"sizing_disk" json:"sizingDisk"`
+ Count types.Int64 `tfsdk:"count" json:"count"`
+}
+
+// K8sSthutrvalClusterStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type K8sSthutrvalClusterStartupConfigurationModel struct {
+ VdcUid types.String `tfsdk:"vdc_uid" json:"vdcUid"`
+ NsxtUid types.String `tfsdk:"nsxt_uid" json:"nsxtUid"`
+ ClusterName types.String `tfsdk:"cluster_name" json:"clusterName"`
+}
+
+type K8sSthutrvalClusterModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AccessConfiguration *K8sSthutrvalClusterAccessConfigurationModel `tfsdk:"access_configuration"`
+ ClusterConfiguration *K8sSthutrvalClusterClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ ControlPlaneConfiguration *K8sSthutrvalClusterControlPlaneConfigurationModel `tfsdk:"control_plane_configuration"`
+ StartupConfiguration *K8sSthutrvalClusterStartupConfigurationModel `tfsdk:"startup_configuration"`
+ WorkerConfiguration types.String `tfsdk:"worker_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewK8sSthutrvalClusterResource() resource.Resource {
+ return &K8sSthutrvalClusterResource{}
+}
+
+func (r *K8sSthutrvalClusterResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_k8s_sthutrval_cluster"
+}
+
+func (r *K8sSthutrvalClusterResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "need_external_address_a_p_i": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Выделяет внешний IP для доступа к kubernetes API. Пока нельзя выставить false"},
+ "access_ip_list_a_p_i": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Access IP List для подключения к Kubernetes API. Указывается массив. Если передан пустой массив, доступ выделяется всем"},
+ "need_external_address_ingress": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Выделяет внешний IP для доступа к Ingress"},
+ "access_ip_list_ingress": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Access IP List для подключения к Ingress. Указывается массив. Если передан пустой массив, доступ выделяется всем"},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "app_version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("2.13.1")},
+ }},
+ "control_plane_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "sizing_policy": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Доступные sizing Policy можно обновлять через услугу `Виртуальный датацентр`"},
+ "sizing_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(50), MarkdownDescription: "Дисковая политика берётся из услуги VDC, первый в списке"},
+ "count": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ }},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "vdc_uid": schema.StringAttribute{Required: true},
+ "nsxt_uid": schema.StringAttribute{Required: true},
+ "cluster_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("example-wln2kdvp")},
+ }},
+ "worker_configuration": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]")},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *K8sSthutrvalClusterResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *K8sSthutrvalClusterModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *K8sSthutrvalClusterModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan K8sSthutrvalClusterModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 750: resources_core.BuildJSON(map[string]string{"vdcUid": fmt.Sprintf("\"%s\"", config.StartupConfiguration.VdcUid.ValueString()), "nsxtUid": fmt.Sprintf("\"%s\"", config.StartupConfiguration.NsxtUid.ValueString()), "clusterName": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ClusterName.ValueString())}),
+ 751: resources_core.BuildJSON(map[string]string{"sizingPolicy": fmt.Sprintf("\"%s\"", config.ControlPlaneConfiguration.SizingPolicy.ValueString()), "sizingDisk": fmt.Sprintf("%d", config.ControlPlaneConfiguration.SizingDisk.ValueInt64()), "count": fmt.Sprintf("%d", config.ControlPlaneConfiguration.Count.ValueInt64())}),
+ 752: resources_core.FormatString(config.WorkerConfiguration),
+ 753: resources_core.BuildJSON(map[string]string{"needExternalAddressAPI": fmt.Sprintf("%v", config.AccessConfiguration.NeedExternalAddressAPI.ValueBool()), "accessIpListAPI": fmt.Sprintf("\"%s\"", config.AccessConfiguration.AccessIpListAPI.ValueString()), "needExternalAddressIngress": fmt.Sprintf("%v", config.AccessConfiguration.NeedExternalAddressIngress.ValueBool()), "accessIpListIngress": fmt.Sprintf("\"%s\"", config.AccessConfiguration.AccessIpListIngress.ValueString())}),
+ 755: resources_core.BuildJSON(map[string]string{"appVersion": fmt.Sprintf("\"%s\"", config.ClusterConfiguration.AppVersion.ValueString())}),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 150, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *K8sSthutrvalClusterResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data K8sSthutrvalClusterModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 150, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 750: resources_core.BuildJSON(map[string]string{"vdcUid": fmt.Sprintf("\"%s\"", data.StartupConfiguration.VdcUid.ValueString()), "nsxtUid": fmt.Sprintf("\"%s\"", data.StartupConfiguration.NsxtUid.ValueString()), "clusterName": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ClusterName.ValueString())}),
+ 751: resources_core.BuildJSON(map[string]string{"sizingPolicy": fmt.Sprintf("\"%s\"", data.ControlPlaneConfiguration.SizingPolicy.ValueString()), "sizingDisk": fmt.Sprintf("%d", data.ControlPlaneConfiguration.SizingDisk.ValueInt64()), "count": fmt.Sprintf("%d", data.ControlPlaneConfiguration.Count.ValueInt64())}),
+ 752: resources_core.FormatString(data.WorkerConfiguration),
+ 753: resources_core.BuildJSON(map[string]string{"needExternalAddressAPI": fmt.Sprintf("%v", data.AccessConfiguration.NeedExternalAddressAPI.ValueBool()), "accessIpListAPI": fmt.Sprintf("\"%s\"", data.AccessConfiguration.AccessIpListAPI.ValueString()), "needExternalAddressIngress": fmt.Sprintf("%v", data.AccessConfiguration.NeedExternalAddressIngress.ValueBool()), "accessIpListIngress": fmt.Sprintf("\"%s\"", data.AccessConfiguration.AccessIpListIngress.ValueString())}),
+ 755: resources_core.BuildJSON(map[string]string{"appVersion": fmt.Sprintf("\"%s\"", data.ClusterConfiguration.AppVersion.ValueString())}),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 150, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 150, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8sSthutrvalClusterResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state K8sSthutrvalClusterModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 150, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *K8sSthutrvalClusterResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan K8sSthutrvalClusterModel
+ var state K8sSthutrvalClusterModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.WorkerConfiguration.IsNull() != state.WorkerConfiguration.IsNull() || plan.WorkerConfiguration.IsUnknown() != state.WorkerConfiguration.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.WorkerConfiguration.IsNull() && !plan.WorkerConfiguration.IsUnknown() && !state.WorkerConfiguration.IsNull() && !state.WorkerConfiguration.IsUnknown() {
+ if !strings.EqualFold(plan.WorkerConfiguration.ValueString(), state.WorkerConfiguration.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 756: resources_core.BuildJSON(map[string]string{"appVersion": fmt.Sprintf("\"%s\"", plan.ClusterConfiguration.AppVersion.ValueString())}),
+ 757: resources_core.BuildJSON(map[string]string{"sizingDisk": fmt.Sprintf("%d", plan.ControlPlaneConfiguration.SizingDisk.ValueInt64()), "count": fmt.Sprintf("%d", plan.ControlPlaneConfiguration.Count.ValueInt64()), "sizingPolicy": fmt.Sprintf("\"%s\"", plan.ControlPlaneConfiguration.SizingPolicy.ValueString())}),
+ 758: resources_core.FormatString(plan.WorkerConfiguration),
+ 759: resources_core.BuildJSON(map[string]string{"needExternalAddressAPI": fmt.Sprintf("%v", plan.AccessConfiguration.NeedExternalAddressAPI.ValueBool()), "accessIpListAPI": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.AccessIpListAPI.ValueString()), "needExternalAddressIngress": fmt.Sprintf("%v", plan.AccessConfiguration.NeedExternalAddressIngress.ValueBool()), "accessIpListIngress": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.AccessIpListIngress.ValueString())}),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 150, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8sSthutrvalClusterResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state K8sSthutrvalClusterModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *K8sSthutrvalClusterResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *K8sSthutrvalClusterResource) 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
+}
diff --git a/generated/prod/go/150_k8s_sthutrval_cluster_user_resource.go b/generated/prod/go/150_k8s_sthutrval_cluster_user_resource.go
new file mode 100644
index 0000000..55faf0e
--- /dev/null
+++ b/generated/prod/go/150_k8s_sthutrval_cluster_user_resource.go
@@ -0,0 +1,343 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: k8s_sthutrval_cluster
+// Subresource: user
+
+var _ resource.Resource = &K8sSthutrvalClusterUserResource{}
+
+// K8sSthutrvalClusterUserResource manages a subresource via operations.
+type K8sSthutrvalClusterUserResource struct {
+ client *core.UniversalClient
+}
+
+type K8sSthutrvalClusterUserModel struct {
+ ID types.String `tfsdk:"id"`
+ K8sSthutrvalClusterID types.String `tfsdk:"k8s_sthutrval_cluster_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"`
+ Admins types.String `tfsdk:"admins"`
+}
+
+func NewK8sSthutrvalClusterUserResource() resource.Resource {
+ return &K8sSthutrvalClusterUserResource{}
+}
+
+func (r *K8sSthutrvalClusterUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_k8s_sthutrval_cluster_user"
+}
+
+func (r *K8sSthutrvalClusterUserResource) 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()}},
+ "k8s_sthutrval_cluster_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."},
+ "admins": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *K8sSthutrvalClusterUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan K8sSthutrvalClusterUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.K8sSthutrvalClusterID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "admins": resources_core.FormatString(plan.Admins),
+ }
+ identityCodes := []string{
+ "admins",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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, "user", 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{
+ "admins": resources_core.FormatString(plan.Admins),
+ })
+
+ 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, "create_user", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", 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, "user", 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, "user", 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, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *K8sSthutrvalClusterUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state K8sSthutrvalClusterUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8sSthutrvalClusterUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan K8sSthutrvalClusterUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state K8sSthutrvalClusterUserModel
+ 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.K8sSthutrvalClusterID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "admins": resources_core.FormatString(plan.Admins),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *K8sSthutrvalClusterUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state K8sSthutrvalClusterUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_user" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.K8sSthutrvalClusterID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "admins": resources_core.FormatString(state.Admins),
+ }
+ identityCodes := []string{
+ "admins",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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{
+ "admins": resources_core.FormatString(state.Admins),
+ })
+
+ 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, "delete_user", 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 *K8sSthutrvalClusterUserResource) 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
+}
diff --git a/generated/prod/go/163_llm_ai_resource.go b/generated/prod/go/163_llm_ai_resource.go
new file mode 100644
index 0000000..506e337
--- /dev/null
+++ b/generated/prod/go/163_llm_ai_resource.go
@@ -0,0 +1,345 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: llm_ai
+// Service ID: 163
+
+var _ resource.Resource = &LlmAiResource{}
+var _ resource.ResourceWithModifyPlan = &LlmAiResource{}
+var _ resource.ResourceWithImportState = &LlmAiResource{}
+
+type LlmAiResource struct {
+ client *core.UniversalClient
+}
+
+type LlmAiModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewLlmAiResource() resource.Resource {
+ return &LlmAiResource{}
+}
+
+func (r *LlmAiResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_llm_ai"
+}
+
+func (r *LlmAiResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("llmAi.aillm.ru"), MarkdownDescription: "Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *LlmAiResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *LlmAiModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *LlmAiModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan LlmAiModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 826: resources_core.FormatString(config.ResourceRealm),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 163, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *LlmAiResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data LlmAiModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 163, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 826: resources_core.FormatString(data.ResourceRealm),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 163, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 163, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *LlmAiResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state LlmAiModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 163, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *LlmAiResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan LlmAiModel
+ var state LlmAiModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 163, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *LlmAiResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state LlmAiModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *LlmAiResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *LlmAiResource) 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
+}
diff --git a/generated/prod/go/163_llm_ai_secrets_resource.go b/generated/prod/go/163_llm_ai_secrets_resource.go
new file mode 100644
index 0000000..063f1fb
--- /dev/null
+++ b/generated/prod/go/163_llm_ai_secrets_resource.go
@@ -0,0 +1,389 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: llm_ai
+// Subresource: secrets
+
+var _ resource.Resource = &LlmAiSecretsResource{}
+
+// LlmAiSecretsResource manages a subresource via operations.
+type LlmAiSecretsResource struct {
+ client *core.UniversalClient
+}
+
+type LlmAiSecretsModel struct {
+ ID types.String `tfsdk:"id"`
+ LlmAiID types.String `tfsdk:"llm_ai_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"`
+ KeyName types.String `tfsdk:"key_name"`
+ Models types.String `tfsdk:"models"`
+ RpmLimit types.Int64 `tfsdk:"rpm_limit"`
+ TeamName types.String `tfsdk:"team_name"`
+ TpmLimit types.Int64 `tfsdk:"tpm_limit"`
+}
+
+func NewLlmAiSecretsResource() resource.Resource {
+ return &LlmAiSecretsResource{}
+}
+
+func (r *LlmAiSecretsResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_llm_ai_secrets"
+}
+
+func (r *LlmAiSecretsResource) 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()}},
+ "llm_ai_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."},
+ "key_name": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "models": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "rpm_limit": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(60), MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.Int64{int64planmodifier.RequiresReplace()}},
+ "team_name": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "tpm_limit": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100000), MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.Int64{int64planmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *LlmAiSecretsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan LlmAiSecretsModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if plan.KeyName.IsNull() || plan.KeyName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "key_name is required.")
+ return
+ }
+ if plan.Models.IsNull() || plan.Models.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "models is required.")
+ return
+ }
+ if plan.TeamName.IsNull() || plan.TeamName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "team_name is required.")
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.LlmAiID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "keyName": resources_core.FormatString(plan.KeyName),
+ "models": resources_core.FormatString(plan.Models),
+ "rpmLimit": resources_core.FormatInt64(plan.RpmLimit),
+ "teamName": resources_core.FormatString(plan.TeamName),
+ "tpmLimit": resources_core.FormatInt64(plan.TpmLimit),
+ }
+ identityCodes := []string{
+ "keyName",
+ "models",
+ "rpmLimit",
+ "teamName",
+ "tpmLimit",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("secrets", 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, "secrets", 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{
+ "models": resources_core.FormatString(plan.Models),
+ "rpmLimit": resources_core.FormatInt64(plan.RpmLimit),
+ "tpmLimit": resources_core.FormatInt64(plan.TpmLimit),
+ "keyName": resources_core.FormatString(plan.KeyName),
+ "teamName": resources_core.FormatString(plan.TeamName),
+ })
+
+ 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, "create_secrets", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_secrets", 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, "secrets", 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, "secrets", 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, "secrets", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *LlmAiSecretsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state LlmAiSecretsModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *LlmAiSecretsResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan LlmAiSecretsModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state LlmAiSecretsModel
+ 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.LlmAiID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "keyName": resources_core.FormatString(plan.KeyName),
+ "models": resources_core.FormatString(plan.Models),
+ "rpmLimit": resources_core.FormatInt64(plan.RpmLimit),
+ "teamName": resources_core.FormatString(plan.TeamName),
+ "tpmLimit": resources_core.FormatInt64(plan.TpmLimit),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "secrets", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *LlmAiSecretsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state LlmAiSecretsModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_secrets" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.LlmAiID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "keyName": resources_core.FormatString(state.KeyName),
+ "models": resources_core.FormatString(state.Models),
+ "rpmLimit": resources_core.FormatInt64(state.RpmLimit),
+ "teamName": resources_core.FormatString(state.TeamName),
+ "tpmLimit": resources_core.FormatInt64(state.TpmLimit),
+ }
+ identityCodes := []string{
+ "keyName",
+ "models",
+ "rpmLimit",
+ "teamName",
+ "tpmLimit",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("secrets", 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{
+ "keyName": resources_core.FormatString(state.KeyName),
+ "teamName": resources_core.FormatString(state.TeamName),
+ })
+
+ 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, "delete_secrets", 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 *LlmAiSecretsResource) 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
+}
diff --git a/generated/prod/go/163_llm_ai_team_resource.go b/generated/prod/go/163_llm_ai_team_resource.go
new file mode 100644
index 0000000..e411362
--- /dev/null
+++ b/generated/prod/go/163_llm_ai_team_resource.go
@@ -0,0 +1,365 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: llm_ai
+// Subresource: team
+
+var _ resource.Resource = &LlmAiTeamResource{}
+
+// LlmAiTeamResource manages a subresource via operations.
+type LlmAiTeamResource struct {
+ client *core.UniversalClient
+}
+
+type LlmAiTeamModel struct {
+ ID types.String `tfsdk:"id"`
+ LlmAiID types.String `tfsdk:"llm_ai_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"`
+ Models types.String `tfsdk:"models"`
+ RpmLimit types.Int64 `tfsdk:"rpm_limit"`
+ TeamName types.String `tfsdk:"team_name"`
+ TpmLimit types.Int64 `tfsdk:"tpm_limit"`
+}
+
+func NewLlmAiTeamResource() resource.Resource {
+ return &LlmAiTeamResource{}
+}
+
+func (r *LlmAiTeamResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_llm_ai_team"
+}
+
+func (r *LlmAiTeamResource) 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()}},
+ "llm_ai_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."},
+ "models": schema.StringAttribute{Required: true},
+ "rpm_limit": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(60)},
+ "team_name": schema.StringAttribute{Required: true, MarkdownDescription: "Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "tpm_limit": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100000)},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *LlmAiTeamResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan LlmAiTeamModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if plan.Models.IsNull() || plan.Models.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "models is required.")
+ return
+ }
+ if plan.TeamName.IsNull() || plan.TeamName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "team_name is required.")
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.LlmAiID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "teamName": resources_core.FormatString(plan.TeamName),
+ }
+ identityCodes := []string{
+ "teamName",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("team", 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, "team", 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{
+ "teamName": resources_core.FormatString(plan.TeamName),
+ "models": resources_core.FormatString(plan.Models),
+ "rpmLimit": resources_core.FormatInt64(plan.RpmLimit),
+ "tpmLimit": resources_core.FormatInt64(plan.TpmLimit),
+ })
+
+ 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, "create_team", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_team", 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, "team", 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, "team", 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, "team", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *LlmAiTeamResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state LlmAiTeamModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *LlmAiTeamResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan LlmAiTeamModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "modify_team" == "" {
+ var state LlmAiTeamModel
+ 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.LlmAiID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{
+ "teamName": resources_core.FormatString(plan.TeamName),
+ "models": resources_core.FormatString(plan.Models),
+ "rpmLimit": resources_core.FormatInt64(plan.RpmLimit),
+ "tpmLimit": resources_core.FormatInt64(plan.TpmLimit),
+ })
+
+ idParams := map[string]string{
+ "teamName": resources_core.FormatString(plan.TeamName),
+ }
+
+ 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, "modify_team", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "team", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *LlmAiTeamResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state LlmAiTeamModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_team" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.LlmAiID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "teamName": resources_core.FormatString(state.TeamName),
+ }
+ identityCodes := []string{
+ "teamName",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("team", 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{
+ "teamName": resources_core.FormatString(state.TeamName),
+ })
+
+ 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, "delete_team", 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 *LlmAiTeamResource) 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
+}
diff --git a/generated/prod/go/19_vc_org_resource.go b/generated/prod/go/19_vc_org_resource.go
new file mode 100644
index 0000000..b1e8dc9
--- /dev/null
+++ b/generated/prod/go/19_vc_org_resource.go
@@ -0,0 +1,391 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_org
+// Service ID: 19
+
+var _ resource.Resource = &VcOrgResource{}
+var _ resource.ResourceWithModifyPlan = &VcOrgResource{}
+var _ resource.ResourceWithImportState = &VcOrgResource{}
+
+type VcOrgResource struct {
+ client *core.UniversalClient
+}
+
+type VcOrgModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ OrganizationType types.String `tfsdk:"organization_type"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ VIPConfigure types.String `tfsdk:"v_i_p_configure"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVcOrgResource() resource.Resource {
+ return &VcOrgResource{}
+}
+
+func (r *VcOrgResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_org"
+}
+
+func (r *VcOrgResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "organization_type": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("iaas"), MarkdownDescription: "Доступ к организации предоставляется только при выборе `iaas` Не изменяется после создания."},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("ngcloud.ru"), MarkdownDescription: "Облако в котором будет развёрнут тенант Не изменяется после создания."},
+ "v_i_p_configure": schema.StringAttribute{Required: true},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcOrgResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VcOrgModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VcOrgModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VcOrgModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.OrganizationType.IsNull() && !plan.OrganizationType.IsUnknown() && !state.OrganizationType.IsNull() && !state.OrganizationType.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.OrganizationType.ValueString(), state.OrganizationType.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить organization_type", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.VIPConfigure.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "v_i_p_configure is required.")
+ return
+ }
+ if config.VIPConfigure.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 418: resources_core.FormatString(config.ResourceRealm),
+ 556: resources_core.FormatString(config.OrganizationType),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 19, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VcOrgResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VcOrgModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.VIPConfigure.IsNull() || data.VIPConfigure.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "v_i_p_configure is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 19, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 418: resources_core.FormatString(data.ResourceRealm),
+ 556: resources_core.FormatString(data.OrganizationType),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 19, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 19, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "organizationType", Field: "OrganizationType", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "vIPConfigure", Field: "VIPConfigure", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcOrgResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcOrgModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 19, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "organizationType", Field: "OrganizationType", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "vIPConfigure", Field: "VIPConfigure", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VcOrgResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcOrgModel
+ var state VcOrgModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.VIPConfigure.IsNull() != state.VIPConfigure.IsNull() || plan.VIPConfigure.IsUnknown() != state.VIPConfigure.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.VIPConfigure.IsNull() && !plan.VIPConfigure.IsUnknown() && !state.VIPConfigure.IsNull() && !state.VIPConfigure.IsUnknown() {
+ if !strings.EqualFold(plan.VIPConfigure.ValueString(), state.VIPConfigure.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 662: resources_core.FormatString(plan.VIPConfigure),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 19, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "organizationType", Field: "OrganizationType", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "vIPConfigure", Field: "VIPConfigure", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcOrgResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcOrgModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VcOrgResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VcOrgResource) 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
+}
diff --git a/generated/prod/go/19_vc_org_user_resource.go b/generated/prod/go/19_vc_org_user_resource.go
new file mode 100644
index 0000000..b18fcdd
--- /dev/null
+++ b/generated/prod/go/19_vc_org_user_resource.go
@@ -0,0 +1,343 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_org
+// Subresource: user
+
+var _ resource.Resource = &VcOrgUserResource{}
+
+// VcOrgUserResource manages a subresource via operations.
+type VcOrgUserResource struct {
+ client *core.UniversalClient
+}
+
+type VcOrgUserModel struct {
+ ID types.String `tfsdk:"id"`
+ VcOrgID types.String `tfsdk:"vc_org_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"`
+ Admins types.String `tfsdk:"admins"`
+}
+
+func NewVcOrgUserResource() resource.Resource {
+ return &VcOrgUserResource{}
+}
+
+func (r *VcOrgUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_org_user"
+}
+
+func (r *VcOrgUserResource) 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()}},
+ "vc_org_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."},
+ "admins": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcOrgUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan VcOrgUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.VcOrgID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "admins": resources_core.FormatString(plan.Admins),
+ }
+ identityCodes := []string{
+ "admins",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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, "user", 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{
+ "admins": resources_core.FormatString(plan.Admins),
+ })
+
+ 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, "create_user", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", 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, "user", 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, "user", 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, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *VcOrgUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcOrgUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcOrgUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcOrgUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state VcOrgUserModel
+ 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.VcOrgID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "admins": resources_core.FormatString(plan.Admins),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *VcOrgUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcOrgUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_user" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.VcOrgID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "admins": resources_core.FormatString(state.Admins),
+ }
+ identityCodes := []string{
+ "admins",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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{
+ "admins": resources_core.FormatString(state.Admins),
+ })
+
+ 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, "delete_user", 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 *VcOrgUserResource) 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
+}
diff --git a/generated/prod/go/1_dummy_resource.go b/generated/prod/go/1_dummy_resource.go
new file mode 100644
index 0000000..9867e09
--- /dev/null
+++ b/generated/prod/go/1_dummy_resource.go
@@ -0,0 +1,593 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: dummy
+// Service ID: 1
+
+var _ resource.Resource = &DummyResource{}
+var _ resource.ResourceWithModifyPlan = &DummyResource{}
+var _ resource.ResourceWithImportState = &DummyResource{}
+
+type DummyResource struct {
+ client *core.UniversalClient
+}
+
+// DummyMapExampleModel — вложенная модель для map-fixed параметра mapExample.
+type DummyMapExampleModel struct {
+ Subparam1 types.String `tfsdk:"subparam1" json:"subparam1"`
+ Secret types.String `tfsdk:"secret" json:"secret"`
+ Subparam2 types.String `tfsdk:"subparam2" json:"subparam2"`
+}
+
+// DummyMapFixedModel — вложенная модель для map-fixed параметра mapFixed.
+type DummyMapFixedModel struct {
+ Bol1 types.Bool `tfsdk:"bol1" json:"bol1"`
+ MinStr1 types.String `tfsdk:"min_str1" json:"minStr1"`
+ Param1 types.Int64 `tfsdk:"param1" json:"param1"`
+ Param2 types.String `tfsdk:"param2" json:"param2"`
+ Param3 types.Int64 `tfsdk:"param3" json:"param3"`
+}
+
+type DummyModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // git_revision: при изменении вызывает redeploy вместо modify.
+ // Опциональное поле — если не задано, modify работает как обычно.
+ GitRevision types.String `tfsdk:"git_revision"`
+ Arr types.String `tfsdk:"arr"`
+ ArrayMapFixedExample types.String `tfsdk:"array_map_fixed_example"`
+ Bodymessage types.String `tfsdk:"bodymessage"`
+ DurationMs types.Int64 `tfsdk:"duration_ms"`
+ FailAtStart types.Bool `tfsdk:"fail_at_start"`
+ FailInProgress types.Bool `tfsdk:"fail_in_progress"`
+ JsonExample types.String `tfsdk:"json_example"`
+ MapExample *DummyMapExampleModel `tfsdk:"map_example"`
+ MapFixed *DummyMapFixedModel `tfsdk:"map_fixed"`
+ NestedRefExample types.String `tfsdk:"nested_ref_example"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ WhereFail types.Int64 `tfsdk:"where_fail"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewDummyResource() resource.Resource {
+ return &DummyResource{}
+}
+
+func (r *DummyResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_dummy"
+}
+
+func (r *DummyResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "arr": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания."},
+ "array_map_fixed_example": schema.StringAttribute{Optional: true, MarkdownDescription: "Array of map-fixed Не изменяется после создания."},
+ "bodymessage": schema.StringAttribute{Optional: true, MarkdownDescription: "Дата, уходящая в Vault"},
+ "duration_ms": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "перед завершением подождать (sleep) столько миллисекунд"},
+ "fail_at_start": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "если true, завершить джоб, ничего не записывая в cmdb"},
+ "fail_in_progress": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "если true, корректно начать выполнение джоба, но в процессе завершить аварийно, ничего не записывая в cmdb"},
+ "json_example": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Для тестирования интерфейса", PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()}},
+ "map_example": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "subparam1": schema.StringAttribute{Optional: true, MarkdownDescription: "это для администратора"},
+ "secret": schema.StringAttribute{Optional: true, MarkdownDescription: "это секрет"},
+ "subparam2": schema.StringAttribute{Optional: true},
+ }},
+ "map_fixed": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "bol1": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Булевский, обязательный"},
+ "min_str1": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("abracadabra"), MarkdownDescription: "не меньше 10 символов"},
+ "param1": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Integer, secret"},
+ "param2": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("1"), MarkdownDescription: "Дескрипшен"},
+ "param3": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "целое положительное"},
+ }},
+ "nested_ref_example": schema.StringAttribute{Optional: true, MarkdownDescription: "Не изменяется после создания."},
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ "where_fail": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "принимает 1,2,3. Работает с failInProgress. 1 - упасть в prepare. 2 - упасть внутри блока Data fill. 3 - упасть после vault write"},
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // При изменении git_revision вызывается redeploy вместо modify.
+ // Если не задано — modify работает без изменений.
+ "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *DummyResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *DummyModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *DummyModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan DummyModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.Arr.IsNull() && !plan.Arr.IsUnknown() && !state.Arr.IsNull() && !state.Arr.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Arr.ValueString(), state.Arr.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить arr", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ArrayMapFixedExample.IsNull() && !plan.ArrayMapFixedExample.IsUnknown() && !state.ArrayMapFixedExample.IsNull() && !state.ArrayMapFixedExample.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ArrayMapFixedExample.ValueString(), state.ArrayMapFixedExample.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить array_map_fixed_example", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.NestedRefExample.IsNull() && !plan.NestedRefExample.IsUnknown() && !state.NestedRefExample.IsNull() && !state.NestedRefExample.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.NestedRefExample.ValueString(), state.NestedRefExample.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить nested_ref_example", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.Arr.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "arr is required.")
+ return
+ }
+ if config.Arr.IsUnknown() {
+ return
+ }
+ if config.ResourceRealm.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+ if config.ResourceRealm.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 198: resources_core.FormatInt64(config.DurationMs),
+ 199: resources_core.FormatBool(config.FailAtStart),
+ 200: resources_core.FormatBool(config.FailInProgress),
+ 201: resources_core.FormatInt64(config.WhereFail),
+ 242: resources_core.FormatString(config.ResourceRealm),
+ 286: resources_core.FormatString(config.Bodymessage),
+ 321: resources_core.BuildJSON(map[string]string{"subparam1": fmt.Sprintf("\"%s\"", config.MapExample.Subparam1.ValueString()), "secret": fmt.Sprintf("\"%s\"", config.MapExample.Secret.ValueString()), "subparam2": fmt.Sprintf("\"%s\"", config.MapExample.Subparam2.ValueString())}),
+ 322: resources_core.FormatString(config.JsonExample),
+ 396: resources_core.FormatString(config.NestedRefExample),
+ 647: resources_core.BuildJSON(map[string]string{"bol1": fmt.Sprintf("%v", config.MapFixed.Bol1.ValueBool()), "minStr1": fmt.Sprintf("\"%s\"", config.MapFixed.MinStr1.ValueString()), "param1": fmt.Sprintf("%d", config.MapFixed.Param1.ValueInt64()), "param2": fmt.Sprintf("\"%s\"", config.MapFixed.Param2.ValueString()), "param3": fmt.Sprintf("%d", config.MapFixed.Param3.ValueInt64())}),
+ 654: resources_core.FormatString(config.ArrayMapFixedExample),
+ 863: resources_core.FormatString(config.Arr),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 1, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *DummyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data DummyModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.Arr.IsNull() || data.Arr.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "arr is required.")
+ return
+ }
+ if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 1, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 198: resources_core.FormatInt64(data.DurationMs),
+ 199: resources_core.FormatBool(data.FailAtStart),
+ 200: resources_core.FormatBool(data.FailInProgress),
+ 201: resources_core.FormatInt64(data.WhereFail),
+ 242: resources_core.FormatString(data.ResourceRealm),
+ 286: resources_core.FormatString(data.Bodymessage),
+ 321: resources_core.BuildJSON(map[string]string{"subparam1": fmt.Sprintf("\"%s\"", data.MapExample.Subparam1.ValueString()), "secret": fmt.Sprintf("\"%s\"", data.MapExample.Secret.ValueString()), "subparam2": fmt.Sprintf("\"%s\"", data.MapExample.Subparam2.ValueString())}),
+ 322: resources_core.FormatString(data.JsonExample),
+ 396: resources_core.FormatString(data.NestedRefExample),
+ 647: resources_core.BuildJSON(map[string]string{"bol1": fmt.Sprintf("%v", data.MapFixed.Bol1.ValueBool()), "minStr1": fmt.Sprintf("\"%s\"", data.MapFixed.MinStr1.ValueString()), "param1": fmt.Sprintf("%d", data.MapFixed.Param1.ValueInt64()), "param2": fmt.Sprintf("\"%s\"", data.MapFixed.Param2.ValueString()), "param3": fmt.Sprintf("%d", data.MapFixed.Param3.ValueInt64())}),
+ 654: resources_core.FormatString(data.ArrayMapFixedExample),
+ 863: resources_core.FormatString(data.Arr),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 1, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 1, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "arr", Field: "Arr", Type: "string"},
+ {Code: "arrayMapFixedExample", Field: "ArrayMapFixedExample", Type: "array-map-fixed"},
+ {Code: "bodymessage", Field: "Bodymessage", Type: "string"},
+ {Code: "durationMs", Field: "DurationMs", Type: "int64"},
+ {Code: "failAtStart", Field: "FailAtStart", Type: "bool"},
+ {Code: "failInProgress", Field: "FailInProgress", Type: "bool"},
+ {Code: "jsonExample", Field: "JsonExample", Type: "string"},
+ {Code: "mapExample", Field: "MapExample", Type: "string"},
+ {Code: "mapFixed", Field: "MapFixed", Type: "map-fixed"},
+ {Code: "nestedRefExample", Field: "NestedRefExample", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "whereFail", Field: "WhereFail", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *DummyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state DummyModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 1, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "arr", Field: "Arr", Type: "string"},
+ {Code: "arrayMapFixedExample", Field: "ArrayMapFixedExample", Type: "array-map-fixed"},
+ {Code: "bodymessage", Field: "Bodymessage", Type: "string"},
+ {Code: "durationMs", Field: "DurationMs", Type: "int64"},
+ {Code: "failAtStart", Field: "FailAtStart", Type: "bool"},
+ {Code: "failInProgress", Field: "FailInProgress", Type: "bool"},
+ {Code: "jsonExample", Field: "JsonExample", Type: "string"},
+ {Code: "mapExample", Field: "MapExample", Type: "string"},
+ {Code: "mapFixed", Field: "MapFixed", Type: "map-fixed"},
+ {Code: "nestedRefExample", Field: "NestedRefExample", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "whereFail", Field: "WhereFail", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *DummyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan DummyModel
+ var state DummyModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.DurationMs.IsNull() != state.DurationMs.IsNull() || plan.DurationMs.IsUnknown() != state.DurationMs.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.DurationMs.IsNull() && !plan.DurationMs.IsUnknown() && !state.DurationMs.IsNull() && !state.DurationMs.IsUnknown() {
+ if plan.DurationMs.ValueInt64() != state.DurationMs.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.FailAtStart.IsNull() != state.FailAtStart.IsNull() || plan.FailAtStart.IsUnknown() != state.FailAtStart.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.FailAtStart.IsNull() && !plan.FailAtStart.IsUnknown() && !state.FailAtStart.IsNull() && !state.FailAtStart.IsUnknown() {
+ if plan.FailAtStart.ValueBool() != state.FailAtStart.ValueBool() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.FailInProgress.IsNull() != state.FailInProgress.IsNull() || plan.FailInProgress.IsUnknown() != state.FailInProgress.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.FailInProgress.IsNull() && !plan.FailInProgress.IsUnknown() && !state.FailInProgress.IsNull() && !state.FailInProgress.IsUnknown() {
+ if plan.FailInProgress.ValueBool() != state.FailInProgress.ValueBool() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.WhereFail.IsNull() != state.WhereFail.IsNull() || plan.WhereFail.IsUnknown() != state.WhereFail.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.WhereFail.IsNull() && !plan.WhereFail.IsUnknown() && !state.WhereFail.IsNull() && !state.WhereFail.IsUnknown() {
+ if plan.WhereFail.ValueInt64() != state.WhereFail.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.Bodymessage.IsNull() != state.Bodymessage.IsNull() || plan.Bodymessage.IsUnknown() != state.Bodymessage.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.Bodymessage.IsNull() && !plan.Bodymessage.IsUnknown() && !state.Bodymessage.IsNull() && !state.Bodymessage.IsUnknown() {
+ if !strings.EqualFold(plan.Bodymessage.ValueString(), state.Bodymessage.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.JsonExample.IsNull() != state.JsonExample.IsNull() || plan.JsonExample.IsUnknown() != state.JsonExample.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.JsonExample.IsNull() && !plan.JsonExample.IsUnknown() && !state.JsonExample.IsNull() && !state.JsonExample.IsUnknown() {
+ if !strings.EqualFold(plan.JsonExample.ValueString(), state.JsonExample.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // Проверяем изменился ли git_revision.
+ // Если да — вызываем redeploy вместо modify.
+ // git_revision = "" (не задано) → обычный modify.
+ redeployRequested := false
+ if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() {
+ if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() {
+ redeployRequested = true
+ }
+ }
+
+ if !hasServiceParamChanges && !redeployRequested {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ if redeployRequested {
+ // Вызываем redeploy — пустой params (операция не требует CFS-параметров)
+ if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ } else {
+ params := map[int]string{
+ 287: resources_core.FormatInt64(plan.DurationMs),
+ 288: resources_core.FormatBool(plan.FailAtStart),
+ 289: resources_core.FormatBool(plan.FailInProgress),
+ 290: resources_core.FormatInt64(plan.WhereFail),
+ 291: resources_core.FormatString(plan.Bodymessage),
+ 373: resources_core.BuildJSON(map[string]string{"secret": fmt.Sprintf("\"%s\"", plan.MapExample.Secret.ValueString())}),
+ 374: resources_core.FormatString(plan.JsonExample),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 1, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "arr", Field: "Arr", Type: "string"},
+ {Code: "arrayMapFixedExample", Field: "ArrayMapFixedExample", Type: "array-map-fixed"},
+ {Code: "bodymessage", Field: "Bodymessage", Type: "string"},
+ {Code: "durationMs", Field: "DurationMs", Type: "int64"},
+ {Code: "failAtStart", Field: "FailAtStart", Type: "bool"},
+ {Code: "failInProgress", Field: "FailInProgress", Type: "bool"},
+ {Code: "jsonExample", Field: "JsonExample", Type: "string"},
+ {Code: "mapExample", Field: "MapExample", Type: "string"},
+ {Code: "mapFixed", Field: "MapFixed", Type: "map-fixed"},
+ {Code: "nestedRefExample", Field: "NestedRefExample", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "whereFail", Field: "WhereFail", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *DummyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state DummyModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *DummyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *DummyResource) 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
+}
diff --git a/generated/prod/go/20_vc_org_saas_resource.go b/generated/prod/go/20_vc_org_saas_resource.go
new file mode 100644
index 0000000..7f53d87
--- /dev/null
+++ b/generated/prod/go/20_vc_org_saas_resource.go
@@ -0,0 +1,348 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_org_saas
+// Service ID: 20
+
+var _ resource.Resource = &VcOrgSaasResource{}
+var _ resource.ResourceWithModifyPlan = &VcOrgSaasResource{}
+var _ resource.ResourceWithImportState = &VcOrgSaasResource{}
+
+type VcOrgSaasResource struct {
+ client *core.UniversalClient
+}
+
+type VcOrgSaasModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVcOrgSaasResource() resource.Resource {
+ return &VcOrgSaasResource{}
+}
+
+func (r *VcOrgSaasResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_org_saas"
+}
+
+func (r *VcOrgSaasResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("ngcloud.ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcOrgSaasResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VcOrgSaasModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VcOrgSaasModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VcOrgSaasModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 185: resources_core.FormatString(config.ResourceRealm),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 20, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VcOrgSaasResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VcOrgSaasModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 20, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 185: resources_core.FormatString(data.ResourceRealm),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 20, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 20, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcOrgSaasResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcOrgSaasModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 20, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VcOrgSaasResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcOrgSaasModel
+ var state VcOrgSaasModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 20, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcOrgSaasResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcOrgSaasModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VcOrgSaasResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VcOrgSaasResource) 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
+}
diff --git a/generated/prod/go/21_vc_vdc_resource.go b/generated/prod/go/21_vc_vdc_resource.go
new file mode 100644
index 0000000..9ebce30
--- /dev/null
+++ b/generated/prod/go/21_vc_vdc_resource.go
@@ -0,0 +1,573 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_vdc
+// Service ID: 21
+
+var _ resource.Resource = &VcVdcResource{}
+var _ resource.ResourceWithModifyPlan = &VcVdcResource{}
+var _ resource.ResourceWithImportState = &VcVdcResource{}
+
+type VcVdcResource struct {
+ client *core.UniversalClient
+}
+
+type VcVdcModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ CpuAllocated types.Int64 `tfsdk:"cpu_allocated"`
+ CpuGuaranteed types.Int64 `tfsdk:"cpu_guaranteed"`
+ MemAllocated types.Int64 `tfsdk:"mem_allocated"`
+ NetworkProvider types.String `tfsdk:"network_provider"`
+ OrganizationUid types.String `tfsdk:"organization_uid"`
+ ProviderVdc types.String `tfsdk:"provider_vdc"`
+ StorageConfig types.String `tfsdk:"storage_config"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVcVdcResource() resource.Resource {
+ return &VcVdcResource{}
+}
+
+func (r *VcVdcResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_vdc"
+}
+
+func (r *VcVdcResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "cpu_allocated": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "шт"},
+ "cpu_guaranteed": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(20), MarkdownDescription: "% резервирования Не изменяется после создания."},
+ "mem_allocated": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(20), MarkdownDescription: "gb"},
+ "network_provider": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания."},
+ "organization_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Для создания VDC предварительно необходимо создать организацию через услугу `Организация в Cloud Director` Не изменяется после создания."},
+ "provider_vdc": schema.StringAttribute{Required: true, MarkdownDescription: "Приписка `2.8` - Скорость vCPU - 2800MHz
И так далее...
Не изменяется после создания."},
+ "storage_config": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Первая политика в списке указывается как "По умолчанию""},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcVdcResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VcVdcModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VcVdcModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VcVdcModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.CpuGuaranteed.IsNull() && !plan.CpuGuaranteed.IsUnknown() && !state.CpuGuaranteed.IsNull() && !state.CpuGuaranteed.IsUnknown() {
+ if plan.CpuGuaranteed.ValueInt64() != state.CpuGuaranteed.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить cpu_guaranteed", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.NetworkProvider.IsNull() && !plan.NetworkProvider.IsUnknown() && !state.NetworkProvider.IsNull() && !state.NetworkProvider.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.NetworkProvider.ValueString(), state.NetworkProvider.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить network_provider", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.OrganizationUid.IsNull() && !plan.OrganizationUid.IsUnknown() && !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.OrganizationUid.ValueString(), state.OrganizationUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить organization_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ProviderVdc.IsNull() && !plan.ProviderVdc.IsUnknown() && !state.ProviderVdc.IsNull() && !state.ProviderVdc.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ProviderVdc.ValueString(), state.ProviderVdc.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить provider_vdc", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.NetworkProvider.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "network_provider is required.")
+ return
+ }
+ if config.NetworkProvider.IsUnknown() {
+ return
+ }
+ if config.OrganizationUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "organization_uid is required.")
+ return
+ }
+ if config.OrganizationUid.IsUnknown() {
+ return
+ }
+ if config.ProviderVdc.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "provider_vdc is required.")
+ return
+ }
+ if config.ProviderVdc.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.OrganizationUid.IsNull() && !config.OrganizationUid.IsUnknown() {
+ resolvedOrganizationUid, err := r.client.ResolveRefSvcParamValue(ctx, 19, config.OrganizationUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve organization_uid", err.Error())
+ } else if resolvedOrganizationUid != "" && resolvedOrganizationUid != config.OrganizationUid.ValueString() {
+ config.OrganizationUid = types.StringValue(resolvedOrganizationUid)
+ }
+ }
+ params := map[int]string{
+ 30: resources_core.FormatString(config.OrganizationUid),
+ 335: resources_core.FormatString(config.ProviderVdc),
+ 361: resources_core.FormatString(config.StorageConfig),
+ 397: resources_core.FormatInt64(config.CpuGuaranteed),
+ 557: resources_core.FormatInt64(config.CpuAllocated),
+ 558: resources_core.FormatInt64(config.MemAllocated),
+ 746: resources_core.FormatString(config.NetworkProvider),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 21, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VcVdcResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VcVdcModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.NetworkProvider.IsNull() || data.NetworkProvider.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "network_provider is required.")
+ return
+ }
+ if data.OrganizationUid.IsNull() || data.OrganizationUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "organization_uid is required.")
+ return
+ }
+ if data.ProviderVdc.IsNull() || data.ProviderVdc.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "provider_vdc is required.")
+ return
+ }
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalOrganizationUid := data.OrganizationUid
+ if !data.OrganizationUid.IsNull() && !data.OrganizationUid.IsUnknown() {
+ resolvedOrganizationUid, err := r.client.ResolveRefSvcParamValue(ctx, 19, data.OrganizationUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedOrganizationUid != "" && resolvedOrganizationUid != data.OrganizationUid.ValueString() {
+ data.OrganizationUid = types.StringValue(resolvedOrganizationUid)
+ }
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 21, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 30: resources_core.FormatString(data.OrganizationUid),
+ 335: resources_core.FormatString(data.ProviderVdc),
+ 361: resources_core.FormatString(data.StorageConfig),
+ 397: resources_core.FormatInt64(data.CpuGuaranteed),
+ 557: resources_core.FormatInt64(data.CpuAllocated),
+ 558: resources_core.FormatInt64(data.MemAllocated),
+ 746: resources_core.FormatString(data.NetworkProvider),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 21, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 21, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "cpuAllocated", Field: "CpuAllocated", Type: "int64"},
+ {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"},
+ {Code: "memAllocated", Field: "MemAllocated", Type: "int64"},
+ {Code: "networkProvider", Field: "NetworkProvider", Type: "string"},
+ {Code: "organizationUid", Field: "OrganizationUid", Type: "string"},
+ {Code: "providerVdc", Field: "ProviderVdc", Type: "string"},
+ {Code: "storageConfig", Field: "StorageConfig", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() {
+ resolvedOrganizationUid, err := r.client.ResolveRefSvcParamValue(ctx, 19, state.OrganizationUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve organization_uid", err.Error())
+ } else if resolvedOrganizationUid != "" && resolvedOrganizationUid != state.OrganizationUid.ValueString() {
+ state.OrganizationUid = types.StringValue(resolvedOrganizationUid)
+ }
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+ if !originalOrganizationUid.IsNull() && !originalOrganizationUid.IsUnknown() && !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() {
+ if strings.EqualFold(state.OrganizationUid.ValueString(), originalOrganizationUid.ValueString()) {
+ state.OrganizationUid = originalOrganizationUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcVdcResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcVdcModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 21, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "cpuAllocated", Field: "CpuAllocated", Type: "int64"},
+ {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"},
+ {Code: "memAllocated", Field: "MemAllocated", Type: "int64"},
+ {Code: "networkProvider", Field: "NetworkProvider", Type: "string"},
+ {Code: "organizationUid", Field: "OrganizationUid", Type: "string"},
+ {Code: "providerVdc", Field: "ProviderVdc", Type: "string"},
+ {Code: "storageConfig", Field: "StorageConfig", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !newState.OrganizationUid.IsNull() && !newState.OrganizationUid.IsUnknown() {
+ resolvedOrganizationUid, err := r.client.ResolveRefSvcParamValue(ctx, 19, newState.OrganizationUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve organization_uid", err.Error())
+ } else if resolvedOrganizationUid != "" && resolvedOrganizationUid != newState.OrganizationUid.ValueString() {
+ newState.OrganizationUid = types.StringValue(resolvedOrganizationUid)
+ }
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+ if !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() && !newState.OrganizationUid.IsNull() && !newState.OrganizationUid.IsUnknown() {
+ if strings.EqualFold(newState.OrganizationUid.ValueString(), state.OrganizationUid.ValueString()) {
+ newState.OrganizationUid = state.OrganizationUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VcVdcResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcVdcModel
+ var state VcVdcModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.CpuAllocated.IsNull() != state.CpuAllocated.IsNull() || plan.CpuAllocated.IsUnknown() != state.CpuAllocated.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.CpuAllocated.IsNull() && !plan.CpuAllocated.IsUnknown() && !state.CpuAllocated.IsNull() && !state.CpuAllocated.IsUnknown() {
+ if plan.CpuAllocated.ValueInt64() != state.CpuAllocated.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.MemAllocated.IsNull() != state.MemAllocated.IsNull() || plan.MemAllocated.IsUnknown() != state.MemAllocated.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.MemAllocated.IsNull() && !plan.MemAllocated.IsUnknown() && !state.MemAllocated.IsNull() && !state.MemAllocated.IsUnknown() {
+ if plan.MemAllocated.ValueInt64() != state.MemAllocated.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.StorageConfig.IsNull() != state.StorageConfig.IsNull() || plan.StorageConfig.IsUnknown() != state.StorageConfig.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.StorageConfig.IsNull() && !plan.StorageConfig.IsUnknown() && !state.StorageConfig.IsNull() && !state.StorageConfig.IsUnknown() {
+ if !strings.EqualFold(plan.StorageConfig.ValueString(), state.StorageConfig.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 560: resources_core.FormatInt64(plan.CpuAllocated),
+ 561: resources_core.FormatInt64(plan.MemAllocated),
+ 562: resources_core.FormatString(plan.StorageConfig),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 21, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "cpuAllocated", Field: "CpuAllocated", Type: "int64"},
+ {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"},
+ {Code: "memAllocated", Field: "MemAllocated", Type: "int64"},
+ {Code: "networkProvider", Field: "NetworkProvider", Type: "string"},
+ {Code: "organizationUid", Field: "OrganizationUid", Type: "string"},
+ {Code: "providerVdc", Field: "ProviderVdc", Type: "string"},
+ {Code: "storageConfig", Field: "StorageConfig", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() {
+ resolvedOrganizationUid, err := r.client.ResolveRefSvcParamValue(ctx, 19, state.OrganizationUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve organization_uid", err.Error())
+ } else if resolvedOrganizationUid != "" && resolvedOrganizationUid != state.OrganizationUid.ValueString() {
+ state.OrganizationUid = types.StringValue(resolvedOrganizationUid)
+ }
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+ if !plan.OrganizationUid.IsNull() && !plan.OrganizationUid.IsUnknown() && !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() {
+ if strings.EqualFold(state.OrganizationUid.ValueString(), plan.OrganizationUid.ValueString()) {
+ state.OrganizationUid = plan.OrganizationUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcVdcResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcVdcModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VcVdcResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VcVdcResource) 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
+}
diff --git a/generated/prod/go/22_vc_nsxt_resource.go b/generated/prod/go/22_vc_nsxt_resource.go
new file mode 100644
index 0000000..443f3a6
--- /dev/null
+++ b/generated/prod/go/22_vc_nsxt_resource.go
@@ -0,0 +1,614 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_nsxt
+// Service ID: 22
+
+var _ resource.Resource = &VcNsxtResource{}
+var _ resource.ResourceWithModifyPlan = &VcNsxtResource{}
+var _ resource.ResourceWithImportState = &VcNsxtResource{}
+
+type VcNsxtResource struct {
+ client *core.UniversalClient
+}
+
+type VcNsxtModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ IpSpaceName types.String `tfsdk:"ip_space_name"`
+ NeedEnableAVI types.Bool `tfsdk:"need_enable_a_v_i"`
+ QosProfile types.String `tfsdk:"qos_profile"`
+ VdcGroupUid types.String `tfsdk:"vdc_group_uid"`
+ VdcType types.String `tfsdk:"vdc_type"`
+ VdcUid types.String `tfsdk:"vdc_uid"`
+ VirtualServicesCount types.Int64 `tfsdk:"virtual_services_count"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVcNsxtResource() resource.Resource {
+ return &VcNsxtResource{}
+}
+
+func (r *VcNsxtResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_nsxt"
+}
+
+func (r *VcNsxtResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "ip_space_name": schema.StringAttribute{Optional: true, MarkdownDescription: "Имя ip Space для внешнего IP"},
+ "need_enable_a_v_i": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включение Load Balancer"},
+ "qos_profile": schema.StringAttribute{Optional: true, MarkdownDescription: "Параметр пока не работает
Должен возвращать поле из ресурсной платформы типа vc из .vcd.hardware.gatewayQoSProfiles
Как по идее должен работать.
Нужно заполнить или CFS vdcUid, или vdcGroupUid
Идеально конечно проверять `if (params.vdcType == "vdc" && params.vdcUid != "")` или `if (params.vdcType == "vdcGroup" && params.vdcGroupUid != "")`
Далее надо пойти по стейту vdc -> org -> resPlatform, взять gatewayQoSProfiles
Или пойти по стейту vdcgroup -> vdc -> org -> resPlatform, взять gatewayQoSProfiles
Регулярку могу написать (Виталя)"},
+ "vdc_group_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID Услуги `Группа виртуальных датацентров (groupvDC)` Не изменяется после создания."},
+ "vdc_type": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("vdc"), MarkdownDescription: "Тип родительской услуги Не изменяется после создания."},
+ "vdc_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID Услуги `Виртуальный датацентр (vDC)` Не изменяется после создания."},
+ "virtual_services_count": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Кол-во виртуальных сервисов, которые **резервируются** на AVI"},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcNsxtResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VcNsxtModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VcNsxtModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VcNsxtModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.VdcGroupUid.IsNull() && !plan.VdcGroupUid.IsUnknown() && !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VdcGroupUid.ValueString(), state.VdcGroupUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vdc_group_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VdcType.IsNull() && !plan.VdcType.IsUnknown() && !state.VdcType.IsNull() && !state.VdcType.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VdcType.ValueString(), state.VdcType.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vdc_type", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VdcUid.ValueString(), state.VdcUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vdc_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.VdcGroupUid.IsNull() && !config.VdcGroupUid.IsUnknown() {
+ resolvedVdcGroupUid, err := r.client.ResolveRefSvcParamValue(ctx, 29, config.VdcGroupUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_group_uid", err.Error())
+ } else if resolvedVdcGroupUid != "" && resolvedVdcGroupUid != config.VdcGroupUid.ValueString() {
+ config.VdcGroupUid = types.StringValue(resolvedVdcGroupUid)
+ }
+ }
+ if !config.VdcUid.IsNull() && !config.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, config.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != config.VdcUid.ValueString() {
+ config.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ params := map[int]string{
+ 8: resources_core.FormatString(config.VdcUid),
+ 340: resources_core.FormatBool(config.NeedEnableAVI),
+ 341: resources_core.FormatInt64(config.VirtualServicesCount),
+ 621: resources_core.FormatString(config.VdcType),
+ 622: resources_core.FormatString(config.VdcGroupUid),
+ 825: resources_core.FormatString(config.QosProfile),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 22, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VcNsxtResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VcNsxtModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalVdcGroupUid := data.VdcGroupUid
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalVdcUid := data.VdcUid
+ if !data.VdcGroupUid.IsNull() && !data.VdcGroupUid.IsUnknown() {
+ resolvedVdcGroupUid, err := r.client.ResolveRefSvcParamValue(ctx, 29, data.VdcGroupUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedVdcGroupUid != "" && resolvedVdcGroupUid != data.VdcGroupUid.ValueString() {
+ data.VdcGroupUid = types.StringValue(resolvedVdcGroupUid)
+ }
+ }
+ if !data.VdcUid.IsNull() && !data.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, data.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedVdcUid != "" && resolvedVdcUid != data.VdcUid.ValueString() {
+ data.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 22, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 8: resources_core.FormatString(data.VdcUid),
+ 340: resources_core.FormatBool(data.NeedEnableAVI),
+ 341: resources_core.FormatInt64(data.VirtualServicesCount),
+ 621: resources_core.FormatString(data.VdcType),
+ 622: resources_core.FormatString(data.VdcGroupUid),
+ 825: resources_core.FormatString(data.QosProfile),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 22, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 22, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"},
+ {Code: "qosProfile", Field: "QosProfile", Type: "string"},
+ {Code: "vdcGroupUid", Field: "VdcGroupUid", Type: "string"},
+ {Code: "vdcType", Field: "VdcType", Type: "string"},
+ {Code: "vdcUid", Field: "VdcUid", Type: "string"},
+ {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() {
+ resolvedVdcGroupUid, err := r.client.ResolveRefSvcParamValue(ctx, 29, state.VdcGroupUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_group_uid", err.Error())
+ } else if resolvedVdcGroupUid != "" && resolvedVdcGroupUid != state.VdcGroupUid.ValueString() {
+ state.VdcGroupUid = types.StringValue(resolvedVdcGroupUid)
+ }
+ }
+ if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() {
+ state.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+ if !originalVdcGroupUid.IsNull() && !originalVdcGroupUid.IsUnknown() && !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() {
+ if strings.EqualFold(state.VdcGroupUid.ValueString(), originalVdcGroupUid.ValueString()) {
+ state.VdcGroupUid = originalVdcGroupUid
+ }
+ }
+ if !originalVdcUid.IsNull() && !originalVdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ if strings.EqualFold(state.VdcUid.ValueString(), originalVdcUid.ValueString()) {
+ state.VdcUid = originalVdcUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcNsxtResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcNsxtModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 22, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"},
+ {Code: "qosProfile", Field: "QosProfile", Type: "string"},
+ {Code: "vdcGroupUid", Field: "VdcGroupUid", Type: "string"},
+ {Code: "vdcType", Field: "VdcType", Type: "string"},
+ {Code: "vdcUid", Field: "VdcUid", Type: "string"},
+ {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !newState.VdcGroupUid.IsNull() && !newState.VdcGroupUid.IsUnknown() {
+ resolvedVdcGroupUid, err := r.client.ResolveRefSvcParamValue(ctx, 29, newState.VdcGroupUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_group_uid", err.Error())
+ } else if resolvedVdcGroupUid != "" && resolvedVdcGroupUid != newState.VdcGroupUid.ValueString() {
+ newState.VdcGroupUid = types.StringValue(resolvedVdcGroupUid)
+ }
+ }
+ if !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, newState.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != newState.VdcUid.ValueString() {
+ newState.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+ if !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() && !newState.VdcGroupUid.IsNull() && !newState.VdcGroupUid.IsUnknown() {
+ if strings.EqualFold(newState.VdcGroupUid.ValueString(), state.VdcGroupUid.ValueString()) {
+ newState.VdcGroupUid = state.VdcGroupUid
+ }
+ }
+ if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() && !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() {
+ if strings.EqualFold(newState.VdcUid.ValueString(), state.VdcUid.ValueString()) {
+ newState.VdcUid = state.VdcUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VcNsxtResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcNsxtModel
+ var state VcNsxtModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.NeedEnableAVI.IsNull() != state.NeedEnableAVI.IsNull() || plan.NeedEnableAVI.IsUnknown() != state.NeedEnableAVI.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.NeedEnableAVI.IsNull() && !plan.NeedEnableAVI.IsUnknown() && !state.NeedEnableAVI.IsNull() && !state.NeedEnableAVI.IsUnknown() {
+ if plan.NeedEnableAVI.ValueBool() != state.NeedEnableAVI.ValueBool() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.VirtualServicesCount.IsNull() != state.VirtualServicesCount.IsNull() || plan.VirtualServicesCount.IsUnknown() != state.VirtualServicesCount.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.VirtualServicesCount.IsNull() && !plan.VirtualServicesCount.IsUnknown() && !state.VirtualServicesCount.IsNull() && !state.VirtualServicesCount.IsUnknown() {
+ if plan.VirtualServicesCount.ValueInt64() != state.VirtualServicesCount.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.IpSpaceName.IsNull() != state.IpSpaceName.IsNull() || plan.IpSpaceName.IsUnknown() != state.IpSpaceName.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.IpSpaceName.IsNull() && !plan.IpSpaceName.IsUnknown() && !state.IpSpaceName.IsNull() && !state.IpSpaceName.IsUnknown() {
+ if !strings.EqualFold(plan.IpSpaceName.ValueString(), state.IpSpaceName.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.QosProfile.IsNull() != state.QosProfile.IsNull() || plan.QosProfile.IsUnknown() != state.QosProfile.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.QosProfile.IsNull() && !plan.QosProfile.IsUnknown() && !state.QosProfile.IsNull() && !state.QosProfile.IsUnknown() {
+ if !strings.EqualFold(plan.QosProfile.ValueString(), state.QosProfile.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 368: resources_core.FormatBool(plan.NeedEnableAVI),
+ 369: resources_core.FormatInt64(plan.VirtualServicesCount),
+ 372: resources_core.FormatString(plan.IpSpaceName),
+ 856: resources_core.FormatString(plan.QosProfile),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 22, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"},
+ {Code: "qosProfile", Field: "QosProfile", Type: "string"},
+ {Code: "vdcGroupUid", Field: "VdcGroupUid", Type: "string"},
+ {Code: "vdcType", Field: "VdcType", Type: "string"},
+ {Code: "vdcUid", Field: "VdcUid", Type: "string"},
+ {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() {
+ resolvedVdcGroupUid, err := r.client.ResolveRefSvcParamValue(ctx, 29, state.VdcGroupUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_group_uid", err.Error())
+ } else if resolvedVdcGroupUid != "" && resolvedVdcGroupUid != state.VdcGroupUid.ValueString() {
+ state.VdcGroupUid = types.StringValue(resolvedVdcGroupUid)
+ }
+ }
+ if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() {
+ state.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+ if !plan.VdcGroupUid.IsNull() && !plan.VdcGroupUid.IsUnknown() && !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() {
+ if strings.EqualFold(state.VdcGroupUid.ValueString(), plan.VdcGroupUid.ValueString()) {
+ state.VdcGroupUid = plan.VdcGroupUid
+ }
+ }
+ if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ if strings.EqualFold(state.VdcUid.ValueString(), plan.VdcUid.ValueString()) {
+ state.VdcUid = plan.VdcUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcNsxtResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcNsxtModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VcNsxtResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VcNsxtResource) 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
+}
diff --git a/generated/prod/go/23_vc_vm_resource.go b/generated/prod/go/23_vc_vm_resource.go
new file mode 100644
index 0000000..ff0b8c6
--- /dev/null
+++ b/generated/prod/go/23_vc_vm_resource.go
@@ -0,0 +1,581 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_vm
+// Service ID: 23
+
+var _ resource.Resource = &VcVmResource{}
+var _ resource.ResourceWithModifyPlan = &VcVmResource{}
+var _ resource.ResourceWithImportState = &VcVmResource{}
+
+type VcVmResource struct {
+ client *core.UniversalClient
+}
+
+type VcVmModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AccessIpList types.String `tfsdk:"access_ip_list"`
+ IpSpaceName types.String `tfsdk:"ip_space_name"`
+ TemplateName types.String `tfsdk:"template_name"`
+ VappUid types.String `tfsdk:"vapp_uid"`
+ VmCpu types.String `tfsdk:"vm_cpu"`
+ VmDisksize types.String `tfsdk:"vm_disksize"`
+ VmName types.String `tfsdk:"vm_name"`
+ VmRam types.String `tfsdk:"vm_ram"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVcVmResource() resource.Resource {
+ return &VcVmResource{}
+}
+
+func (r *VcVmResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_vm"
+}
+
+func (r *VcVmResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_ip_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("79.111.119.78/32 185.247.187.1/32"), MarkdownDescription: "Вставить содержимое из 'curl ifconfig.io'
IP адреса в CIDR формате через пробел для настройки аксес листов"},
+ "ip_space_name": schema.StringAttribute{Required: true, MarkdownDescription: "Брать из свойств ресурсной платформы
.vcd.ipSpace
Имя IP-space для выделения внешнего IP (IP выделяется обязательно) Не изменяется после создания."},
+ "template_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("RockyLinux_9-16G"), MarkdownDescription: "ОС Не изменяется после создания."},
+ "vapp_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "InstanceUid vApp. vApp должен быть создан Не изменяется после создания."},
+ "vm_cpu": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("2"), MarkdownDescription: "CPU"},
+ "vm_disksize": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("10"), MarkdownDescription: "Размер диска под сервис (GB)"},
+ "vm_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("vm0001"), MarkdownDescription: "Имя ВМ. Не поддерживает спец-символы Не изменяется после создания."},
+ "vm_ram": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("2"), MarkdownDescription: "RAM (GB)"},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcVmResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VcVmModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VcVmModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VcVmModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.IpSpaceName.IsNull() && !plan.IpSpaceName.IsUnknown() && !state.IpSpaceName.IsNull() && !state.IpSpaceName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.IpSpaceName.ValueString(), state.IpSpaceName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить ip_space_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.TemplateName.IsNull() && !plan.TemplateName.IsUnknown() && !state.TemplateName.IsNull() && !state.TemplateName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.TemplateName.ValueString(), state.TemplateName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить template_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VappUid.IsNull() && !plan.VappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VappUid.ValueString(), state.VappUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vapp_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VmName.IsNull() && !plan.VmName.IsUnknown() && !state.VmName.IsNull() && !state.VmName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VmName.ValueString(), state.VmName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vm_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.IpSpaceName.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.")
+ return
+ }
+ if config.IpSpaceName.IsUnknown() {
+ return
+ }
+ if config.VappUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "vapp_uid is required.")
+ return
+ }
+ if config.VappUid.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.VappUid.IsNull() && !config.VappUid.IsUnknown() {
+ resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, config.VappUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error())
+ } else if resolvedVappUid != "" && resolvedVappUid != config.VappUid.ValueString() {
+ config.VappUid = types.StringValue(resolvedVappUid)
+ }
+ }
+ params := map[int]string{
+ 33: resources_core.FormatString(config.VappUid),
+ 35: resources_core.FormatString(config.VmName),
+ 36: resources_core.FormatString(config.VmCpu),
+ 37: resources_core.FormatString(config.VmRam),
+ 38: resources_core.FormatString(config.VmDisksize),
+ 176: resources_core.FormatString(config.TemplateName),
+ 177: resources_core.FormatString(config.AccessIpList),
+ 406: resources_core.FormatString(config.IpSpaceName),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 23, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VcVmResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VcVmModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.IpSpaceName.IsNull() || data.IpSpaceName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.")
+ return
+ }
+ if data.VappUid.IsNull() || data.VappUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "vapp_uid is required.")
+ return
+ }
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalVappUid := data.VappUid
+ if !data.VappUid.IsNull() && !data.VappUid.IsUnknown() {
+ resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, data.VappUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedVappUid != "" && resolvedVappUid != data.VappUid.ValueString() {
+ data.VappUid = types.StringValue(resolvedVappUid)
+ }
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 23, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 33: resources_core.FormatString(data.VappUid),
+ 35: resources_core.FormatString(data.VmName),
+ 36: resources_core.FormatString(data.VmCpu),
+ 37: resources_core.FormatString(data.VmRam),
+ 38: resources_core.FormatString(data.VmDisksize),
+ 176: resources_core.FormatString(data.TemplateName),
+ 177: resources_core.FormatString(data.AccessIpList),
+ 406: resources_core.FormatString(data.IpSpaceName),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 23, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 23, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessIpList", Field: "AccessIpList", Type: "string"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "templateName", Field: "TemplateName", Type: "string"},
+ {Code: "vappUid", Field: "VappUid", Type: "string"},
+ {Code: "vmCpu", Field: "VmCpu", Type: "string"},
+ {Code: "vmDisksize", Field: "VmDisksize", Type: "string"},
+ {Code: "vmName", Field: "VmName", Type: "string"},
+ {Code: "vmRam", Field: "VmRam", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() {
+ resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, state.VappUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error())
+ } else if resolvedVappUid != "" && resolvedVappUid != state.VappUid.ValueString() {
+ state.VappUid = types.StringValue(resolvedVappUid)
+ }
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+ if !originalVappUid.IsNull() && !originalVappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() {
+ if strings.EqualFold(state.VappUid.ValueString(), originalVappUid.ValueString()) {
+ state.VappUid = originalVappUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcVmResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcVmModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 23, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessIpList", Field: "AccessIpList", Type: "string"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "templateName", Field: "TemplateName", Type: "string"},
+ {Code: "vappUid", Field: "VappUid", Type: "string"},
+ {Code: "vmCpu", Field: "VmCpu", Type: "string"},
+ {Code: "vmDisksize", Field: "VmDisksize", Type: "string"},
+ {Code: "vmName", Field: "VmName", Type: "string"},
+ {Code: "vmRam", Field: "VmRam", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !newState.VappUid.IsNull() && !newState.VappUid.IsUnknown() {
+ resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, newState.VappUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error())
+ } else if resolvedVappUid != "" && resolvedVappUid != newState.VappUid.ValueString() {
+ newState.VappUid = types.StringValue(resolvedVappUid)
+ }
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+ if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() && !newState.VappUid.IsNull() && !newState.VappUid.IsUnknown() {
+ if strings.EqualFold(newState.VappUid.ValueString(), state.VappUid.ValueString()) {
+ newState.VappUid = state.VappUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VcVmResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcVmModel
+ var state VcVmModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.VmCpu.IsNull() != state.VmCpu.IsNull() || plan.VmCpu.IsUnknown() != state.VmCpu.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.VmCpu.IsNull() && !plan.VmCpu.IsUnknown() && !state.VmCpu.IsNull() && !state.VmCpu.IsUnknown() {
+ if !strings.EqualFold(plan.VmCpu.ValueString(), state.VmCpu.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.VmRam.IsNull() != state.VmRam.IsNull() || plan.VmRam.IsUnknown() != state.VmRam.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.VmRam.IsNull() && !plan.VmRam.IsUnknown() && !state.VmRam.IsNull() && !state.VmRam.IsUnknown() {
+ if !strings.EqualFold(plan.VmRam.ValueString(), state.VmRam.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.VmDisksize.IsNull() != state.VmDisksize.IsNull() || plan.VmDisksize.IsUnknown() != state.VmDisksize.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.VmDisksize.IsNull() && !plan.VmDisksize.IsUnknown() && !state.VmDisksize.IsNull() && !state.VmDisksize.IsUnknown() {
+ if !strings.EqualFold(plan.VmDisksize.ValueString(), state.VmDisksize.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.AccessIpList.IsNull() != state.AccessIpList.IsNull() || plan.AccessIpList.IsUnknown() != state.AccessIpList.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.AccessIpList.IsNull() && !plan.AccessIpList.IsUnknown() && !state.AccessIpList.IsNull() && !state.AccessIpList.IsUnknown() {
+ if !strings.EqualFold(plan.AccessIpList.ValueString(), state.AccessIpList.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 186: resources_core.FormatString(plan.VmCpu),
+ 187: resources_core.FormatString(plan.VmRam),
+ 188: resources_core.FormatString(plan.VmDisksize),
+ 189: resources_core.FormatString(plan.AccessIpList),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 23, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessIpList", Field: "AccessIpList", Type: "string"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "templateName", Field: "TemplateName", Type: "string"},
+ {Code: "vappUid", Field: "VappUid", Type: "string"},
+ {Code: "vmCpu", Field: "VmCpu", Type: "string"},
+ {Code: "vmDisksize", Field: "VmDisksize", Type: "string"},
+ {Code: "vmName", Field: "VmName", Type: "string"},
+ {Code: "vmRam", Field: "VmRam", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() {
+ resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, state.VappUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error())
+ } else if resolvedVappUid != "" && resolvedVappUid != state.VappUid.ValueString() {
+ state.VappUid = types.StringValue(resolvedVappUid)
+ }
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+ if !plan.VappUid.IsNull() && !plan.VappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() {
+ if strings.EqualFold(state.VappUid.ValueString(), plan.VappUid.ValueString()) {
+ state.VappUid = plan.VappUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcVmResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcVmModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VcVmResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VcVmResource) 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
+}
diff --git a/generated/prod/go/25_vcexternalip_resource.go b/generated/prod/go/25_vcexternalip_resource.go
new file mode 100644
index 0000000..d11a84c
--- /dev/null
+++ b/generated/prod/go/25_vcexternalip_resource.go
@@ -0,0 +1,604 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vcexternalip
+// Service ID: 25
+
+var _ resource.Resource = &VcexternalipResource{}
+var _ resource.ResourceWithModifyPlan = &VcexternalipResource{}
+var _ resource.ResourceWithImportState = &VcexternalipResource{}
+
+type VcexternalipResource struct {
+ client *core.UniversalClient
+}
+
+type VcexternalipModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ DnatCreate types.Bool `tfsdk:"dnat_create"`
+ FromServiceCloudEdgeName types.String `tfsdk:"from_service_cloud_edge_name"`
+ FromServiceCloudEdgeScope types.String `tfsdk:"from_service_cloud_edge_scope"`
+ FromServiceCloudOrgName types.String `tfsdk:"from_service_cloud_org_name"`
+ FromServiceCloudVdcName types.String `tfsdk:"from_service_cloud_vdc_name"`
+ FromServiceCloudVmwareUrl types.String `tfsdk:"from_service_cloud_vmware_url"`
+ FromServiceNamespace types.String `tfsdk:"from_service_namespace"`
+ FromServiceVdcGroupName types.String `tfsdk:"from_service_vdc_group_name"`
+ InternalAddrAccess types.String `tfsdk:"internal_addr_access"`
+ InternalPortAccess types.String `tfsdk:"internal_port_access"`
+ IpSpaceName types.String `tfsdk:"ip_space_name"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ ServiceUid types.String `tfsdk:"service_uid"`
+ SnatCreate types.Bool `tfsdk:"snat_create"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVcexternalipResource() resource.Resource {
+ return &VcexternalipResource{}
+}
+
+func (r *VcexternalipResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vcexternalip"
+}
+
+func (r *VcexternalipResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "dnat_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Необходимо создать DNAT-правило."},
+ "from_service_cloud_edge_name": schema.StringAttribute{Optional: true, MarkdownDescription: "Edge составной услуги (указывать, если resourceRealm связан с CloudDirector) Не изменяется после создания."},
+ "from_service_cloud_edge_scope": schema.StringAttribute{Optional: true, MarkdownDescription: "Не изменяется после создания."},
+ "from_service_cloud_org_name": schema.StringAttribute{Optional: true, MarkdownDescription: "Org составной услуги (указывать, если resourceRealm связан с CloudDirector) Не изменяется после создания."},
+ "from_service_cloud_vdc_name": schema.StringAttribute{Optional: true, MarkdownDescription: "Vdc составной услуги (указывать, если resourceRealm связан с CloudDirector) Не изменяется после создания."},
+ "from_service_cloud_vmware_url": schema.StringAttribute{Optional: true, MarkdownDescription: "Api-Url составной услуги (указывать, если resourceRealm связан с CloudDirector) Не изменяется после создания."},
+ "from_service_namespace": schema.StringAttribute{Required: true, MarkdownDescription: "Namespace составной услуги (для отображения правил FW) Не изменяется после создания."},
+ "from_service_vdc_group_name": schema.StringAttribute{Optional: true, MarkdownDescription: "Не изменяется после создания."},
+ "internal_addr_access": schema.StringAttribute{Required: true, MarkdownDescription: "Внутренний адрес для связки ната. Требует dnatCreate=true или snatCreate=true"},
+ "internal_port_access": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("5432"), MarkdownDescription: "Порт для связки ната с адресом. Несколько значений можно передавать через пробел. Если передать all, то dnat будет на все порты. Требует dnatCreate=true
"},
+ "ip_space_name": schema.StringAttribute{Required: true, MarkdownDescription: "Тип ip Space (internet-ipv4-v1 / internet-antiddos-v1 / internet-no-antiddos-v1)"},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("ngcloud.ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ "service_uid": schema.StringAttribute{Required: true},
+ "snat_create": schema.BoolAttribute{Optional: true, MarkdownDescription: "Необходимо создать SNAT-правило.
"},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcexternalipResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VcexternalipModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VcexternalipModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VcexternalipModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.FromServiceCloudEdgeName.IsNull() && !plan.FromServiceCloudEdgeName.IsUnknown() && !state.FromServiceCloudEdgeName.IsNull() && !state.FromServiceCloudEdgeName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.FromServiceCloudEdgeName.ValueString(), state.FromServiceCloudEdgeName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить from_service_cloud_edge_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.FromServiceCloudEdgeScope.IsNull() && !plan.FromServiceCloudEdgeScope.IsUnknown() && !state.FromServiceCloudEdgeScope.IsNull() && !state.FromServiceCloudEdgeScope.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.FromServiceCloudEdgeScope.ValueString(), state.FromServiceCloudEdgeScope.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить from_service_cloud_edge_scope", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.FromServiceCloudOrgName.IsNull() && !plan.FromServiceCloudOrgName.IsUnknown() && !state.FromServiceCloudOrgName.IsNull() && !state.FromServiceCloudOrgName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.FromServiceCloudOrgName.ValueString(), state.FromServiceCloudOrgName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить from_service_cloud_org_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.FromServiceCloudVdcName.IsNull() && !plan.FromServiceCloudVdcName.IsUnknown() && !state.FromServiceCloudVdcName.IsNull() && !state.FromServiceCloudVdcName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.FromServiceCloudVdcName.ValueString(), state.FromServiceCloudVdcName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить from_service_cloud_vdc_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.FromServiceCloudVmwareUrl.IsNull() && !plan.FromServiceCloudVmwareUrl.IsUnknown() && !state.FromServiceCloudVmwareUrl.IsNull() && !state.FromServiceCloudVmwareUrl.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.FromServiceCloudVmwareUrl.ValueString(), state.FromServiceCloudVmwareUrl.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить from_service_cloud_vmware_url", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.FromServiceNamespace.IsNull() && !plan.FromServiceNamespace.IsUnknown() && !state.FromServiceNamespace.IsNull() && !state.FromServiceNamespace.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.FromServiceNamespace.ValueString(), state.FromServiceNamespace.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить from_service_namespace", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.FromServiceVdcGroupName.IsNull() && !plan.FromServiceVdcGroupName.IsUnknown() && !state.FromServiceVdcGroupName.IsNull() && !state.FromServiceVdcGroupName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.FromServiceVdcGroupName.ValueString(), state.FromServiceVdcGroupName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить from_service_vdc_group_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.FromServiceNamespace.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "from_service_namespace is required.")
+ return
+ }
+ if config.FromServiceNamespace.IsUnknown() {
+ return
+ }
+ if config.InternalAddrAccess.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "internal_addr_access is required.")
+ return
+ }
+ if config.InternalAddrAccess.IsUnknown() {
+ return
+ }
+ if config.IpSpaceName.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.")
+ return
+ }
+ if config.IpSpaceName.IsUnknown() {
+ return
+ }
+ if config.ServiceUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "service_uid is required.")
+ return
+ }
+ if config.ServiceUid.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 140: resources_core.FormatString(config.ServiceUid),
+ 141: resources_core.FormatBool(config.DnatCreate),
+ 142: resources_core.FormatString(config.InternalPortAccess),
+ 143: resources_core.FormatBool(config.SnatCreate),
+ 144: resources_core.FormatString(config.InternalAddrAccess),
+ 324: resources_core.FormatString(config.FromServiceNamespace),
+ 325: resources_core.FormatString(config.FromServiceCloudEdgeName),
+ 326: resources_core.FormatString(config.FromServiceCloudVdcName),
+ 327: resources_core.FormatString(config.FromServiceCloudOrgName),
+ 328: resources_core.FormatString(config.FromServiceCloudVmwareUrl),
+ 336: resources_core.FormatString(config.IpSpaceName),
+ 488: resources_core.FormatString(config.ResourceRealm),
+ 624: resources_core.FormatString(config.FromServiceCloudEdgeScope),
+ 626: resources_core.FormatString(config.FromServiceVdcGroupName),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 25, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VcexternalipResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VcexternalipModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.FromServiceNamespace.IsNull() || data.FromServiceNamespace.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "from_service_namespace is required.")
+ return
+ }
+ if data.InternalAddrAccess.IsNull() || data.InternalAddrAccess.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "internal_addr_access is required.")
+ return
+ }
+ if data.IpSpaceName.IsNull() || data.IpSpaceName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.")
+ return
+ }
+ if data.ServiceUid.IsNull() || data.ServiceUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "service_uid is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 25, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 140: resources_core.FormatString(data.ServiceUid),
+ 141: resources_core.FormatBool(data.DnatCreate),
+ 142: resources_core.FormatString(data.InternalPortAccess),
+ 143: resources_core.FormatBool(data.SnatCreate),
+ 144: resources_core.FormatString(data.InternalAddrAccess),
+ 324: resources_core.FormatString(data.FromServiceNamespace),
+ 325: resources_core.FormatString(data.FromServiceCloudEdgeName),
+ 326: resources_core.FormatString(data.FromServiceCloudVdcName),
+ 327: resources_core.FormatString(data.FromServiceCloudOrgName),
+ 328: resources_core.FormatString(data.FromServiceCloudVmwareUrl),
+ 336: resources_core.FormatString(data.IpSpaceName),
+ 488: resources_core.FormatString(data.ResourceRealm),
+ 624: resources_core.FormatString(data.FromServiceCloudEdgeScope),
+ 626: resources_core.FormatString(data.FromServiceVdcGroupName),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 25, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 25, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "dnatCreate", Field: "DnatCreate", Type: "bool"},
+ {Code: "fromServiceCloudEdgeName", Field: "FromServiceCloudEdgeName", Type: "string"},
+ {Code: "fromServiceCloudEdgeScope", Field: "FromServiceCloudEdgeScope", Type: "string"},
+ {Code: "fromServiceCloudOrgName", Field: "FromServiceCloudOrgName", Type: "string"},
+ {Code: "fromServiceCloudVdcName", Field: "FromServiceCloudVdcName", Type: "string"},
+ {Code: "fromServiceCloudVmwareUrl", Field: "FromServiceCloudVmwareUrl", Type: "string"},
+ {Code: "fromServiceNamespace", Field: "FromServiceNamespace", Type: "string"},
+ {Code: "fromServiceVdcGroupName", Field: "FromServiceVdcGroupName", Type: "string"},
+ {Code: "internalAddrAccess", Field: "InternalAddrAccess", Type: "string"},
+ {Code: "internalPortAccess", Field: "InternalPortAccess", Type: "string"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "serviceUid", Field: "ServiceUid", Type: "string"},
+ {Code: "snatCreate", Field: "SnatCreate", Type: "bool"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcexternalipResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcexternalipModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 25, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "dnatCreate", Field: "DnatCreate", Type: "bool"},
+ {Code: "fromServiceCloudEdgeName", Field: "FromServiceCloudEdgeName", Type: "string"},
+ {Code: "fromServiceCloudEdgeScope", Field: "FromServiceCloudEdgeScope", Type: "string"},
+ {Code: "fromServiceCloudOrgName", Field: "FromServiceCloudOrgName", Type: "string"},
+ {Code: "fromServiceCloudVdcName", Field: "FromServiceCloudVdcName", Type: "string"},
+ {Code: "fromServiceCloudVmwareUrl", Field: "FromServiceCloudVmwareUrl", Type: "string"},
+ {Code: "fromServiceNamespace", Field: "FromServiceNamespace", Type: "string"},
+ {Code: "fromServiceVdcGroupName", Field: "FromServiceVdcGroupName", Type: "string"},
+ {Code: "internalAddrAccess", Field: "InternalAddrAccess", Type: "string"},
+ {Code: "internalPortAccess", Field: "InternalPortAccess", Type: "string"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "serviceUid", Field: "ServiceUid", Type: "string"},
+ {Code: "snatCreate", Field: "SnatCreate", Type: "bool"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VcexternalipResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcexternalipModel
+ var state VcexternalipModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.ServiceUid.IsNull() != state.ServiceUid.IsNull() || plan.ServiceUid.IsUnknown() != state.ServiceUid.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ServiceUid.IsNull() && !plan.ServiceUid.IsUnknown() && !state.ServiceUid.IsNull() && !state.ServiceUid.IsUnknown() {
+ if !strings.EqualFold(plan.ServiceUid.ValueString(), state.ServiceUid.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.IpSpaceName.IsNull() != state.IpSpaceName.IsNull() || plan.IpSpaceName.IsUnknown() != state.IpSpaceName.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.IpSpaceName.IsNull() && !plan.IpSpaceName.IsUnknown() && !state.IpSpaceName.IsNull() && !state.IpSpaceName.IsUnknown() {
+ if !strings.EqualFold(plan.IpSpaceName.ValueString(), state.IpSpaceName.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.DnatCreate.IsNull() != state.DnatCreate.IsNull() || plan.DnatCreate.IsUnknown() != state.DnatCreate.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.DnatCreate.IsNull() && !plan.DnatCreate.IsUnknown() && !state.DnatCreate.IsNull() && !state.DnatCreate.IsUnknown() {
+ if plan.DnatCreate.ValueBool() != state.DnatCreate.ValueBool() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.SnatCreate.IsNull() != state.SnatCreate.IsNull() || plan.SnatCreate.IsUnknown() != state.SnatCreate.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.SnatCreate.IsNull() && !plan.SnatCreate.IsUnknown() && !state.SnatCreate.IsNull() && !state.SnatCreate.IsUnknown() {
+ if plan.SnatCreate.ValueBool() != state.SnatCreate.ValueBool() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.InternalPortAccess.IsNull() != state.InternalPortAccess.IsNull() || plan.InternalPortAccess.IsUnknown() != state.InternalPortAccess.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.InternalPortAccess.IsNull() && !plan.InternalPortAccess.IsUnknown() && !state.InternalPortAccess.IsNull() && !state.InternalPortAccess.IsUnknown() {
+ if !strings.EqualFold(plan.InternalPortAccess.ValueString(), state.InternalPortAccess.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.InternalAddrAccess.IsNull() != state.InternalAddrAccess.IsNull() || plan.InternalAddrAccess.IsUnknown() != state.InternalAddrAccess.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.InternalAddrAccess.IsNull() && !plan.InternalAddrAccess.IsUnknown() && !state.InternalAddrAccess.IsNull() && !state.InternalAddrAccess.IsUnknown() {
+ if !strings.EqualFold(plan.InternalAddrAccess.ValueString(), state.InternalAddrAccess.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 627: resources_core.FormatString(plan.ServiceUid),
+ 628: resources_core.FormatString(plan.IpSpaceName),
+ 629: resources_core.FormatBool(plan.DnatCreate),
+ 630: resources_core.FormatBool(plan.SnatCreate),
+ 631: resources_core.FormatString(plan.InternalPortAccess),
+ 632: resources_core.FormatString(plan.InternalAddrAccess),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 25, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "dnatCreate", Field: "DnatCreate", Type: "bool"},
+ {Code: "fromServiceCloudEdgeName", Field: "FromServiceCloudEdgeName", Type: "string"},
+ {Code: "fromServiceCloudEdgeScope", Field: "FromServiceCloudEdgeScope", Type: "string"},
+ {Code: "fromServiceCloudOrgName", Field: "FromServiceCloudOrgName", Type: "string"},
+ {Code: "fromServiceCloudVdcName", Field: "FromServiceCloudVdcName", Type: "string"},
+ {Code: "fromServiceCloudVmwareUrl", Field: "FromServiceCloudVmwareUrl", Type: "string"},
+ {Code: "fromServiceNamespace", Field: "FromServiceNamespace", Type: "string"},
+ {Code: "fromServiceVdcGroupName", Field: "FromServiceVdcGroupName", Type: "string"},
+ {Code: "internalAddrAccess", Field: "InternalAddrAccess", Type: "string"},
+ {Code: "internalPortAccess", Field: "InternalPortAccess", Type: "string"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "serviceUid", Field: "ServiceUid", Type: "string"},
+ {Code: "snatCreate", Field: "SnatCreate", Type: "bool"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcexternalipResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcexternalipModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VcexternalipResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VcexternalipResource) 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
+}
diff --git a/generated/prod/go/26_vapp_resource.go b/generated/prod/go/26_vapp_resource.go
new file mode 100644
index 0000000..158be46
--- /dev/null
+++ b/generated/prod/go/26_vapp_resource.go
@@ -0,0 +1,571 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vapp
+// Service ID: 26
+
+var _ resource.Resource = &VappResource{}
+var _ resource.ResourceWithModifyPlan = &VappResource{}
+var _ resource.ResourceWithImportState = &VappResource{}
+
+type VappResource struct {
+ client *core.UniversalClient
+}
+
+type VappModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ NsxtUid types.String `tfsdk:"nsxt_uid"`
+ VappName types.String `tfsdk:"vapp_name"`
+ VdcUid types.String `tfsdk:"vdc_uid"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVappResource() resource.Resource {
+ return &VappResource{}
+}
+
+func (r *VappResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vapp"
+}
+
+func (r *VappResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "nsxt_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID Услуги `Сетевой шлюз периметра (Edge)` Не изменяется после создания."},
+ "vapp_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("3ecobacg"), MarkdownDescription: "Уникальное в рамках организации Не изменяется после создания."},
+ "vdc_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID Услуги `Виртуальный датацентр (vDC)` Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VappResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VappModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VappModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VappModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.NsxtUid.IsNull() && !plan.NsxtUid.IsUnknown() && !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.NsxtUid.ValueString(), state.NsxtUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить nsxt_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VappName.IsNull() && !plan.VappName.IsUnknown() && !state.VappName.IsNull() && !state.VappName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VappName.ValueString(), state.VappName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vapp_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VdcUid.ValueString(), state.VdcUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vdc_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.NsxtUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "nsxt_uid is required.")
+ return
+ }
+ if config.NsxtUid.IsUnknown() {
+ return
+ }
+ if config.VdcUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "vdc_uid is required.")
+ return
+ }
+ if config.VdcUid.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.NsxtUid.IsNull() && !config.NsxtUid.IsUnknown() {
+ resolvedNsxtUid, err := r.client.ResolveRefSvcParamValue(ctx, 22, config.NsxtUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve nsxt_uid", err.Error())
+ } else if resolvedNsxtUid != "" && resolvedNsxtUid != config.NsxtUid.ValueString() {
+ config.NsxtUid = types.StringValue(resolvedNsxtUid)
+ }
+ }
+ if !config.VdcUid.IsNull() && !config.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, config.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != config.VdcUid.ValueString() {
+ config.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ params := map[int]string{
+ 190: resources_core.FormatString(config.NsxtUid),
+ 191: resources_core.FormatString(config.VappName),
+ 623: resources_core.FormatString(config.VdcUid),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 26, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VappResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VappModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.NsxtUid.IsNull() || data.NsxtUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "nsxt_uid is required.")
+ return
+ }
+ if data.VdcUid.IsNull() || data.VdcUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "vdc_uid is required.")
+ return
+ }
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalNsxtUid := data.NsxtUid
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalVdcUid := data.VdcUid
+ if !data.NsxtUid.IsNull() && !data.NsxtUid.IsUnknown() {
+ resolvedNsxtUid, err := r.client.ResolveRefSvcParamValue(ctx, 22, data.NsxtUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedNsxtUid != "" && resolvedNsxtUid != data.NsxtUid.ValueString() {
+ data.NsxtUid = types.StringValue(resolvedNsxtUid)
+ }
+ }
+ if !data.VdcUid.IsNull() && !data.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, data.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedVdcUid != "" && resolvedVdcUid != data.VdcUid.ValueString() {
+ data.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 26, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 190: resources_core.FormatString(data.NsxtUid),
+ 191: resources_core.FormatString(data.VappName),
+ 623: resources_core.FormatString(data.VdcUid),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 26, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 26, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "nsxtUid", Field: "NsxtUid", Type: "string"},
+ {Code: "vappName", Field: "VappName", Type: "string"},
+ {Code: "vdcUid", Field: "VdcUid", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() {
+ resolvedNsxtUid, err := r.client.ResolveRefSvcParamValue(ctx, 22, state.NsxtUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve nsxt_uid", err.Error())
+ } else if resolvedNsxtUid != "" && resolvedNsxtUid != state.NsxtUid.ValueString() {
+ state.NsxtUid = types.StringValue(resolvedNsxtUid)
+ }
+ }
+ if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() {
+ state.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+ if !originalNsxtUid.IsNull() && !originalNsxtUid.IsUnknown() && !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() {
+ if strings.EqualFold(state.NsxtUid.ValueString(), originalNsxtUid.ValueString()) {
+ state.NsxtUid = originalNsxtUid
+ }
+ }
+ if !originalVdcUid.IsNull() && !originalVdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ if strings.EqualFold(state.VdcUid.ValueString(), originalVdcUid.ValueString()) {
+ state.VdcUid = originalVdcUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VappResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VappModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 26, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "nsxtUid", Field: "NsxtUid", Type: "string"},
+ {Code: "vappName", Field: "VappName", Type: "string"},
+ {Code: "vdcUid", Field: "VdcUid", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !newState.NsxtUid.IsNull() && !newState.NsxtUid.IsUnknown() {
+ resolvedNsxtUid, err := r.client.ResolveRefSvcParamValue(ctx, 22, newState.NsxtUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve nsxt_uid", err.Error())
+ } else if resolvedNsxtUid != "" && resolvedNsxtUid != newState.NsxtUid.ValueString() {
+ newState.NsxtUid = types.StringValue(resolvedNsxtUid)
+ }
+ }
+ if !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, newState.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != newState.VdcUid.ValueString() {
+ newState.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+ if !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() && !newState.NsxtUid.IsNull() && !newState.NsxtUid.IsUnknown() {
+ if strings.EqualFold(newState.NsxtUid.ValueString(), state.NsxtUid.ValueString()) {
+ newState.NsxtUid = state.NsxtUid
+ }
+ }
+ if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() && !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() {
+ if strings.EqualFold(newState.VdcUid.ValueString(), state.VdcUid.ValueString()) {
+ newState.VdcUid = state.VdcUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VappResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VappModel
+ var state VappModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 26, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "nsxtUid", Field: "NsxtUid", Type: "string"},
+ {Code: "vappName", Field: "VappName", Type: "string"},
+ {Code: "vdcUid", Field: "VdcUid", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() {
+ resolvedNsxtUid, err := r.client.ResolveRefSvcParamValue(ctx, 22, state.NsxtUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve nsxt_uid", err.Error())
+ } else if resolvedNsxtUid != "" && resolvedNsxtUid != state.NsxtUid.ValueString() {
+ state.NsxtUid = types.StringValue(resolvedNsxtUid)
+ }
+ }
+ if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() {
+ state.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+ if !plan.NsxtUid.IsNull() && !plan.NsxtUid.IsUnknown() && !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() {
+ if strings.EqualFold(state.NsxtUid.ValueString(), plan.NsxtUid.ValueString()) {
+ state.NsxtUid = plan.NsxtUid
+ }
+ }
+ if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ if strings.EqualFold(state.VdcUid.ValueString(), plan.VdcUid.ValueString()) {
+ state.VdcUid = plan.VdcUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VappResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VappModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VappResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VappResource) 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
+}
diff --git a/generated/prod/go/27_vc_vm_v2_resource.go b/generated/prod/go/27_vc_vm_v2_resource.go
new file mode 100644
index 0000000..afd6fec
--- /dev/null
+++ b/generated/prod/go/27_vc_vm_v2_resource.go
@@ -0,0 +1,350 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_vm_v2
+// Service ID: 27
+
+var _ resource.Resource = &VcVmV2Resource{}
+var _ resource.ResourceWithModifyPlan = &VcVmV2Resource{}
+var _ resource.ResourceWithImportState = &VcVmV2Resource{}
+
+type VcVmV2Resource struct {
+ client *core.UniversalClient
+}
+
+type VcVmV2Model struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ ServerListJson types.String `tfsdk:"server_list_json"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVcVmV2Resource() resource.Resource {
+ return &VcVmV2Resource{}
+}
+
+func (r *VcVmV2Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_vm_v2"
+}
+
+func (r *VcVmV2Resource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "server_list_json": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Необходимо изменить vappUid и ipSpaceName
Пример конфигурации взять по ссылке ниже
Объект серверов вида [{}] (Map(Object))
Более подробнее смотреть в README.md (https://gitea.mgmt.nubes.ru/cloudServices/vcVmV2)", PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()}},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcVmV2Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VcVmV2Model
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VcVmV2Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VcVmV2Model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 301: resources_core.FormatString(config.ServerListJson),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 27, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VcVmV2Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VcVmV2Model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 27, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 301: resources_core.FormatString(data.ServerListJson),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 27, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 27, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "serverListJson", Field: "ServerListJson", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcVmV2Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcVmV2Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 27, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "serverListJson", Field: "ServerListJson", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VcVmV2Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcVmV2Model
+ var state VcVmV2Model
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.ServerListJson.IsNull() != state.ServerListJson.IsNull() || plan.ServerListJson.IsUnknown() != state.ServerListJson.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ServerListJson.IsNull() && !plan.ServerListJson.IsUnknown() && !state.ServerListJson.IsNull() && !state.ServerListJson.IsUnknown() {
+ if !strings.EqualFold(plan.ServerListJson.ValueString(), state.ServerListJson.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 302: resources_core.FormatString(plan.ServerListJson),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 27, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "serverListJson", Field: "ServerListJson", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcVmV2Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcVmV2Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VcVmV2Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VcVmV2Resource) 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
+}
diff --git a/generated/prod/go/28_vc_vm_v3_resource.go b/generated/prod/go/28_vc_vm_v3_resource.go
new file mode 100644
index 0000000..89e6d1e
--- /dev/null
+++ b/generated/prod/go/28_vc_vm_v3_resource.go
@@ -0,0 +1,697 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_vm_v3
+// Service ID: 28
+
+var _ resource.Resource = &VcVmV3Resource{}
+var _ resource.ResourceWithModifyPlan = &VcVmV3Resource{}
+var _ resource.ResourceWithImportState = &VcVmV3Resource{}
+
+type VcVmV3Resource struct {
+ client *core.UniversalClient
+}
+
+type VcVmV3Model struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // git_revision: при изменении вызывает redeploy вместо modify.
+ // Опциональное поле — если не задано, modify работает как обычно.
+ GitRevision types.String `tfsdk:"git_revision"`
+ AccessIpList types.String `tfsdk:"access_ip_list"`
+ AccessPortList types.String `tfsdk:"access_port_list"`
+ CloudInit types.String `tfsdk:"cloud_init"`
+ ImageVm types.String `tfsdk:"image_vm"`
+ IpSpaceName types.String `tfsdk:"ip_space_name"`
+ UserLogin types.String `tfsdk:"user_login"`
+ UserPublicKey types.String `tfsdk:"user_public_key"`
+ VappUid types.String `tfsdk:"vapp_uid"`
+ VmCpu types.Int64 `tfsdk:"vm_cpu"`
+ VmDisk types.Int64 `tfsdk:"vm_disk"`
+ VmName types.String `tfsdk:"vm_name"`
+ VmRam types.Int64 `tfsdk:"vm_ram"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVcVmV3Resource() resource.Resource {
+ return &VcVmV3Resource{}
+}
+
+func (r *VcVmV3Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_vm_v3"
+}
+
+func (r *VcVmV3Resource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_ip_list": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Формат массива
Каким адресам будет предоставлен доступ до вм
Требует выделенного внешнего IP
Если ничего не передано, то выставлено ["0.0.0.0/0"]", PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()}},
+ "access_port_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[\"22\"]"), MarkdownDescription: "По каким портам открыть доступ к ВМ.
Порты 22, 10050 проставляются автоматически
Пока доступно:
22 - SSH,
80 - HTTP,
443 - HTTPS,
10050 - ZABBIX"},
+ "cloud_init": schema.StringAttribute{Optional: true, MarkdownDescription: "Можно передать создание дополнительных пользователей.
На текущий момент не реализовано выполнения в cloud-init, установки пакетов, выполнения команд и записи файлов. Не изменяется после создания."},
+ "image_vm": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания."},
+ "ip_space_name": schema.StringAttribute{Required: true},
+ "user_login": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("myuser"), MarkdownDescription: "Учётка SSH Не изменяется после создания."},
+ "user_public_key": schema.StringAttribute{Required: true, MarkdownDescription: "Публичная часть SSH-Ключа для пользователя Не изменяется после создания."},
+ "vapp_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UI display_name for service_id=26 (mapped to UUID in core) Не изменяется после создания."},
+ "vm_cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "шт"},
+ "vm_disk": schema.Int64Attribute{Optional: true, MarkdownDescription: "Основной диск у vm зависит от образа (см. документацию)
Если необходимо дополнительное место для vm, введите колличество Gb в эту форму"},
+ "vm_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("web01"), MarkdownDescription: "Не изменяется после создания."},
+ "vm_ram": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Gb"},
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // При изменении git_revision вызывается redeploy вместо modify.
+ // Если не задано — modify работает без изменений.
+ "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcVmV3Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VcVmV3Model
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VcVmV3Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VcVmV3Model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.CloudInit.IsNull() && !plan.CloudInit.IsUnknown() && !state.CloudInit.IsNull() && !state.CloudInit.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.CloudInit.ValueString(), state.CloudInit.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить cloud_init", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ImageVm.IsNull() && !plan.ImageVm.IsUnknown() && !state.ImageVm.IsNull() && !state.ImageVm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ImageVm.ValueString(), state.ImageVm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить image_vm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.UserLogin.IsNull() && !plan.UserLogin.IsUnknown() && !state.UserLogin.IsNull() && !state.UserLogin.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.UserLogin.ValueString(), state.UserLogin.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить user_login", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.UserPublicKey.IsNull() && !plan.UserPublicKey.IsUnknown() && !state.UserPublicKey.IsNull() && !state.UserPublicKey.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.UserPublicKey.ValueString(), state.UserPublicKey.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить user_public_key", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VappUid.IsNull() && !plan.VappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VappUid.ValueString(), state.VappUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vapp_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VmName.IsNull() && !plan.VmName.IsUnknown() && !state.VmName.IsNull() && !state.VmName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VmName.ValueString(), state.VmName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vm_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.ImageVm.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "image_vm is required.")
+ return
+ }
+ if config.ImageVm.IsUnknown() {
+ return
+ }
+ if config.IpSpaceName.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.")
+ return
+ }
+ if config.IpSpaceName.IsUnknown() {
+ return
+ }
+ if config.UserPublicKey.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "user_public_key is required.")
+ return
+ }
+ if config.UserPublicKey.IsUnknown() {
+ return
+ }
+ if config.VappUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "vapp_uid is required.")
+ return
+ }
+ if config.VappUid.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.VappUid.IsNull() && !config.VappUid.IsUnknown() {
+ resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, config.VappUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error())
+ } else if resolvedVappUid != "" && resolvedVappUid != config.VappUid.ValueString() {
+ config.VappUid = types.StringValue(resolvedVappUid)
+ }
+ }
+ params := map[int]string{
+ 407: resources_core.FormatString(config.VappUid),
+ 408: resources_core.FormatString(config.VmName),
+ 409: resources_core.FormatInt64(config.VmCpu),
+ 410: resources_core.FormatInt64(config.VmRam),
+ 411: resources_core.FormatInt64(config.VmDisk),
+ 412: resources_core.FormatString(config.IpSpaceName),
+ 413: resources_core.FormatString(config.AccessIpList),
+ 414: resources_core.FormatString(config.ImageVm),
+ 415: resources_core.FormatString(config.CloudInit),
+ 416: resources_core.FormatString(config.UserLogin),
+ 417: resources_core.FormatString(config.UserPublicKey),
+ 448: resources_core.FormatString(config.AccessPortList),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 28, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VcVmV3Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VcVmV3Model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.ImageVm.IsNull() || data.ImageVm.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "image_vm is required.")
+ return
+ }
+ if data.IpSpaceName.IsNull() || data.IpSpaceName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.")
+ return
+ }
+ if data.UserPublicKey.IsNull() || data.UserPublicKey.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "user_public_key is required.")
+ return
+ }
+ if data.VappUid.IsNull() || data.VappUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "vapp_uid is required.")
+ return
+ }
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalVappUid := data.VappUid
+ if !data.VappUid.IsNull() && !data.VappUid.IsUnknown() {
+ resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, data.VappUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedVappUid != "" && resolvedVappUid != data.VappUid.ValueString() {
+ data.VappUid = types.StringValue(resolvedVappUid)
+ }
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 28, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 407: resources_core.FormatString(data.VappUid),
+ 408: resources_core.FormatString(data.VmName),
+ 409: resources_core.FormatInt64(data.VmCpu),
+ 410: resources_core.FormatInt64(data.VmRam),
+ 411: resources_core.FormatInt64(data.VmDisk),
+ 412: resources_core.FormatString(data.IpSpaceName),
+ 413: resources_core.FormatString(data.AccessIpList),
+ 414: resources_core.FormatString(data.ImageVm),
+ 415: resources_core.FormatString(data.CloudInit),
+ 416: resources_core.FormatString(data.UserLogin),
+ 417: resources_core.FormatString(data.UserPublicKey),
+ 448: resources_core.FormatString(data.AccessPortList),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 28, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 28, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessIpList", Field: "AccessIpList", Type: "string"},
+ {Code: "accessPortList", Field: "AccessPortList", Type: "string"},
+ {Code: "cloudInit", Field: "CloudInit", Type: "string"},
+ {Code: "imageVm", Field: "ImageVm", Type: "string"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "userLogin", Field: "UserLogin", Type: "string"},
+ {Code: "userPublicKey", Field: "UserPublicKey", Type: "string"},
+ {Code: "vappUid", Field: "VappUid", Type: "string"},
+ {Code: "vmCpu", Field: "VmCpu", Type: "int64"},
+ {Code: "vmDisk", Field: "VmDisk", Type: "int64"},
+ {Code: "vmName", Field: "VmName", Type: "string"},
+ {Code: "vmRam", Field: "VmRam", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() {
+ resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, state.VappUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error())
+ } else if resolvedVappUid != "" && resolvedVappUid != state.VappUid.ValueString() {
+ state.VappUid = types.StringValue(resolvedVappUid)
+ }
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+ if !originalVappUid.IsNull() && !originalVappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() {
+ if strings.EqualFold(state.VappUid.ValueString(), originalVappUid.ValueString()) {
+ state.VappUid = originalVappUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcVmV3Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcVmV3Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 28, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessIpList", Field: "AccessIpList", Type: "string"},
+ {Code: "accessPortList", Field: "AccessPortList", Type: "string"},
+ {Code: "cloudInit", Field: "CloudInit", Type: "string"},
+ {Code: "imageVm", Field: "ImageVm", Type: "string"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "userLogin", Field: "UserLogin", Type: "string"},
+ {Code: "userPublicKey", Field: "UserPublicKey", Type: "string"},
+ {Code: "vappUid", Field: "VappUid", Type: "string"},
+ {Code: "vmCpu", Field: "VmCpu", Type: "int64"},
+ {Code: "vmDisk", Field: "VmDisk", Type: "int64"},
+ {Code: "vmName", Field: "VmName", Type: "string"},
+ {Code: "vmRam", Field: "VmRam", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !newState.VappUid.IsNull() && !newState.VappUid.IsUnknown() {
+ resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, newState.VappUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error())
+ } else if resolvedVappUid != "" && resolvedVappUid != newState.VappUid.ValueString() {
+ newState.VappUid = types.StringValue(resolvedVappUid)
+ }
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+ if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() && !newState.VappUid.IsNull() && !newState.VappUid.IsUnknown() {
+ if strings.EqualFold(newState.VappUid.ValueString(), state.VappUid.ValueString()) {
+ newState.VappUid = state.VappUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VcVmV3Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcVmV3Model
+ var state VcVmV3Model
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.VmCpu.IsNull() != state.VmCpu.IsNull() || plan.VmCpu.IsUnknown() != state.VmCpu.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.VmCpu.IsNull() && !plan.VmCpu.IsUnknown() && !state.VmCpu.IsNull() && !state.VmCpu.IsUnknown() {
+ if plan.VmCpu.ValueInt64() != state.VmCpu.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.VmRam.IsNull() != state.VmRam.IsNull() || plan.VmRam.IsUnknown() != state.VmRam.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.VmRam.IsNull() && !plan.VmRam.IsUnknown() && !state.VmRam.IsNull() && !state.VmRam.IsUnknown() {
+ if plan.VmRam.ValueInt64() != state.VmRam.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.VmDisk.IsNull() != state.VmDisk.IsNull() || plan.VmDisk.IsUnknown() != state.VmDisk.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.VmDisk.IsNull() && !plan.VmDisk.IsUnknown() && !state.VmDisk.IsNull() && !state.VmDisk.IsUnknown() {
+ if plan.VmDisk.ValueInt64() != state.VmDisk.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.IpSpaceName.IsNull() != state.IpSpaceName.IsNull() || plan.IpSpaceName.IsUnknown() != state.IpSpaceName.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.IpSpaceName.IsNull() && !plan.IpSpaceName.IsUnknown() && !state.IpSpaceName.IsNull() && !state.IpSpaceName.IsUnknown() {
+ if !strings.EqualFold(plan.IpSpaceName.ValueString(), state.IpSpaceName.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.AccessIpList.IsNull() != state.AccessIpList.IsNull() || plan.AccessIpList.IsUnknown() != state.AccessIpList.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.AccessIpList.IsNull() && !plan.AccessIpList.IsUnknown() && !state.AccessIpList.IsNull() && !state.AccessIpList.IsUnknown() {
+ if !strings.EqualFold(plan.AccessIpList.ValueString(), state.AccessIpList.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.AccessPortList.IsNull() != state.AccessPortList.IsNull() || plan.AccessPortList.IsUnknown() != state.AccessPortList.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.AccessPortList.IsNull() && !plan.AccessPortList.IsUnknown() && !state.AccessPortList.IsNull() && !state.AccessPortList.IsUnknown() {
+ if !strings.EqualFold(plan.AccessPortList.ValueString(), state.AccessPortList.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // Проверяем изменился ли git_revision.
+ // Если да — вызываем redeploy вместо modify.
+ // git_revision = "" (не задано) → обычный modify.
+ redeployRequested := false
+ if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() {
+ if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() {
+ redeployRequested = true
+ }
+ }
+
+ if !hasServiceParamChanges && !redeployRequested {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ if redeployRequested {
+ // Вызываем redeploy — пустой params (операция не требует CFS-параметров)
+ if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ } else {
+ params := map[int]string{
+ 493: resources_core.FormatInt64(plan.VmCpu),
+ 494: resources_core.FormatInt64(plan.VmRam),
+ 495: resources_core.FormatInt64(plan.VmDisk),
+ 496: resources_core.FormatString(plan.IpSpaceName),
+ 497: resources_core.FormatString(plan.AccessIpList),
+ 498: resources_core.FormatString(plan.AccessPortList),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 28, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessIpList", Field: "AccessIpList", Type: "string"},
+ {Code: "accessPortList", Field: "AccessPortList", Type: "string"},
+ {Code: "cloudInit", Field: "CloudInit", Type: "string"},
+ {Code: "imageVm", Field: "ImageVm", Type: "string"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "userLogin", Field: "UserLogin", Type: "string"},
+ {Code: "userPublicKey", Field: "UserPublicKey", Type: "string"},
+ {Code: "vappUid", Field: "VappUid", Type: "string"},
+ {Code: "vmCpu", Field: "VmCpu", Type: "int64"},
+ {Code: "vmDisk", Field: "VmDisk", Type: "int64"},
+ {Code: "vmName", Field: "VmName", Type: "string"},
+ {Code: "vmRam", Field: "VmRam", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() {
+ resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, state.VappUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error())
+ } else if resolvedVappUid != "" && resolvedVappUid != state.VappUid.ValueString() {
+ state.VappUid = types.StringValue(resolvedVappUid)
+ }
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+ if !plan.VappUid.IsNull() && !plan.VappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() {
+ if strings.EqualFold(state.VappUid.ValueString(), plan.VappUid.ValueString()) {
+ state.VappUid = plan.VappUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcVmV3Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcVmV3Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VcVmV3Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VcVmV3Resource) 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
+}
diff --git a/generated/prod/go/29_vc_vdc_group_resource.go b/generated/prod/go/29_vc_vdc_group_resource.go
new file mode 100644
index 0000000..f5a5213
--- /dev/null
+++ b/generated/prod/go/29_vc_vdc_group_resource.go
@@ -0,0 +1,449 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_vdc_group
+// Service ID: 29
+
+var _ resource.Resource = &VcVdcGroupResource{}
+var _ resource.ResourceWithModifyPlan = &VcVdcGroupResource{}
+var _ resource.ResourceWithImportState = &VcVdcGroupResource{}
+
+type VcVdcGroupResource struct {
+ client *core.UniversalClient
+}
+
+type VcVdcGroupModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ VdcUid types.String `tfsdk:"vdc_uid"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVcVdcGroupResource() resource.Resource {
+ return &VcVdcGroupResource{}
+}
+
+func (r *VcVdcGroupResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_vdc_group"
+}
+
+func (r *VcVdcGroupResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "vdc_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UI display_name for service_id=21 (mapped to UUID in core) Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcVdcGroupResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VcVdcGroupModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VcVdcGroupModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VcVdcGroupModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VdcUid.ValueString(), state.VdcUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vdc_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.VdcUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "vdc_uid is required.")
+ return
+ }
+ if config.VdcUid.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.VdcUid.IsNull() && !config.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, config.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != config.VdcUid.ValueString() {
+ config.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ params := map[int]string{
+ 618: resources_core.FormatString(config.VdcUid),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 29, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VcVdcGroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VcVdcGroupModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.VdcUid.IsNull() || data.VdcUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "vdc_uid is required.")
+ return
+ }
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalVdcUid := data.VdcUid
+ if !data.VdcUid.IsNull() && !data.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, data.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedVdcUid != "" && resolvedVdcUid != data.VdcUid.ValueString() {
+ data.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 29, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 618: resources_core.FormatString(data.VdcUid),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 29, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 29, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "vdcUid", Field: "VdcUid", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() {
+ state.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+ if !originalVdcUid.IsNull() && !originalVdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ if strings.EqualFold(state.VdcUid.ValueString(), originalVdcUid.ValueString()) {
+ state.VdcUid = originalVdcUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcVdcGroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcVdcGroupModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 29, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "vdcUid", Field: "VdcUid", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, newState.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != newState.VdcUid.ValueString() {
+ newState.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+ if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() && !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() {
+ if strings.EqualFold(newState.VdcUid.ValueString(), state.VdcUid.ValueString()) {
+ newState.VdcUid = state.VdcUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VcVdcGroupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcVdcGroupModel
+ var state VcVdcGroupModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 29, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "vdcUid", Field: "VdcUid", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() {
+ state.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+ if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ if strings.EqualFold(state.VdcUid.ValueString(), plan.VdcUid.ValueString()) {
+ state.VdcUid = plan.VdcUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcVdcGroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcVdcGroupModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VcVdcGroupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VcVdcGroupResource) 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
+}
diff --git a/generated/prod/go/29_vc_vdc_group_vdc_resource.go b/generated/prod/go/29_vc_vdc_group_vdc_resource.go
new file mode 100644
index 0000000..3e3e8ef
--- /dev/null
+++ b/generated/prod/go/29_vc_vdc_group_vdc_resource.go
@@ -0,0 +1,326 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_vdc_group
+// Subresource: vdc
+
+var _ resource.Resource = &VcVdcGroupVdcResource{}
+
+// VcVdcGroupVdcResource manages a subresource via operations.
+type VcVdcGroupVdcResource struct {
+ client *core.UniversalClient
+}
+
+type VcVdcGroupVdcModel struct {
+ ID types.String `tfsdk:"id"`
+ VcVdcGroupID types.String `tfsdk:"vc_vdc_group_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"`
+}
+
+func NewVcVdcGroupVdcResource() resource.Resource {
+ return &VcVdcGroupVdcResource{}
+}
+
+func (r *VcVdcGroupVdcResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_vdc_group_vdc"
+}
+
+func (r *VcVdcGroupVdcResource) 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()}},
+ "vc_vdc_group_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."},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcVdcGroupVdcResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan VcVdcGroupVdcModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.VcVdcGroupID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{}
+ identityCodes := []string{}
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("vdc", 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, "vdc", 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{})
+
+ 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, "", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", 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, "vdc", 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, "vdc", 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, "vdc", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *VcVdcGroupVdcResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcVdcGroupVdcModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcVdcGroupVdcResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcVdcGroupVdcModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state VcVdcGroupVdcModel
+ 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.VcVdcGroupID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{}
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "vdc", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *VcVdcGroupVdcResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcVdcGroupVdcModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.VcVdcGroupID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{}
+ identityCodes := []string{}
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("vdc", 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{})
+
+ 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, "", 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 *VcVdcGroupVdcResource) 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
+}
diff --git a/generated/prod/go/2_template_resource.go b/generated/prod/go/2_template_resource.go
new file mode 100644
index 0000000..a7a5bbb
--- /dev/null
+++ b/generated/prod/go/2_template_resource.go
@@ -0,0 +1,397 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: template
+// Service ID: 2
+
+var _ resource.Resource = &TemplateResource{}
+var _ resource.ResourceWithModifyPlan = &TemplateResource{}
+var _ resource.ResourceWithImportState = &TemplateResource{}
+
+type TemplateResource struct {
+ client *core.UniversalClient
+}
+
+// TemplateAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type TemplateAccessConfigurationModel struct {
+ SlaveAccessList types.String `tfsdk:"slave_access_list" json:"slaveAccessList"`
+ Type types.String `tfsdk:"type" json:"type"`
+ Domain types.String `tfsdk:"domain" json:"domain"`
+ MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"`
+ MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"`
+ SlaveIpSpace types.String `tfsdk:"slave_ip_space" json:"slaveIpSpace"`
+}
+
+// TemplateClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type TemplateClusterConfigurationModel struct {
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+ Disk types.Int64 `tfsdk:"disk" json:"disk"`
+}
+
+// TemplateStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type TemplateStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+}
+
+type TemplateModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AccessConfiguration *TemplateAccessConfigurationModel `tfsdk:"access_configuration"`
+ ClusterConfiguration *TemplateClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ StartupConfiguration *TemplateStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewTemplateResource() resource.Resource {
+ return &TemplateResource{}
+}
+
+func (r *TemplateResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_template"
+}
+
+func (r *TemplateResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "slave_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]")},
+ "type": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("ingress")},
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("abracadabra-8htr2l6c")},
+ "master_ip_space": schema.StringAttribute{Required: true},
+ "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]")},
+ "slave_ip_space": schema.StringAttribute{Required: true},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(128)},
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100)},
+ "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(5)},
+ }},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru")},
+ }},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *TemplateResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *TemplateModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *TemplateModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan TemplateModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 813: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64())}),
+ 814: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}),
+ 815: resources_core.BuildJSON(map[string]string{"slaveAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveAccessList.ValueString()), "type": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Type.ValueString()), "domain": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Domain.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveIpSpace.ValueString())}),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 2, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *TemplateResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data TemplateModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 2, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 813: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64())}),
+ 814: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}),
+ 815: resources_core.BuildJSON(map[string]string{"slaveAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveAccessList.ValueString()), "type": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Type.ValueString()), "domain": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Domain.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveIpSpace.ValueString())}),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 2, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 2, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *TemplateResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state TemplateModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 2, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan TemplateModel
+ var state TemplateModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 816: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64())}),
+ 817: resources_core.BuildJSON(map[string]string{"type": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.Type.ValueString()), "domain": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.Domain.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveAccessList.ValueString())}),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 2, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *TemplateResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state TemplateModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *TemplateResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *TemplateResource) 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
+}
diff --git a/generated/prod/go/50_nextcloud_backup_resource.go b/generated/prod/go/50_nextcloud_backup_resource.go
new file mode 100644
index 0000000..feb1f0e
--- /dev/null
+++ b/generated/prod/go/50_nextcloud_backup_resource.go
@@ -0,0 +1,344 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: nextcloud
+// Subresource: backup
+
+var _ resource.Resource = &NextcloudBackupResource{}
+
+// NextcloudBackupResource manages a subresource via operations.
+type NextcloudBackupResource struct {
+ client *core.UniversalClient
+}
+
+type NextcloudBackupModel struct {
+ ID types.String `tfsdk:"id"`
+ NextcloudID types.String `tfsdk:"nextcloud_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"`
+ StartupConfiguration types.String `tfsdk:"startup_configuration"`
+}
+
+func NewNextcloudBackupResource() resource.Resource {
+ return &NextcloudBackupResource{}
+}
+
+func (r *NextcloudBackupResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_nextcloud_backup"
+}
+
+func (r *NextcloudBackupResource) 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()}},
+ "nextcloud_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."},
+ "startup_configuration": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *NextcloudBackupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan NextcloudBackupModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if plan.StartupConfiguration.IsNull() || plan.StartupConfiguration.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "startup_configuration is required.")
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.NextcloudID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "startupConfiguration": resources_core.FormatString(plan.StartupConfiguration),
+ }
+ identityCodes := []string{
+ "startupConfiguration",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("backup", 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, "backup", 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{
+ "startupConfiguration": resources_core.FormatString(plan.StartupConfiguration),
+ })
+
+ 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, "create_backup", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_backup", 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, "backup", 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, "backup", 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, "backup", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *NextcloudBackupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state NextcloudBackupModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *NextcloudBackupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan NextcloudBackupModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state NextcloudBackupModel
+ 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.NextcloudID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "startupConfiguration": resources_core.FormatString(plan.StartupConfiguration),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *NextcloudBackupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state NextcloudBackupModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.NextcloudID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "startupConfiguration": resources_core.FormatString(state.StartupConfiguration),
+ }
+ identityCodes := []string{
+ "startupConfiguration",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("backup", 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{})
+
+ 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, "", 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 *NextcloudBackupResource) 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
+}
diff --git a/generated/prod/go/50_nextcloud_resource.go b/generated/prod/go/50_nextcloud_resource.go
new file mode 100644
index 0000000..94badb7
--- /dev/null
+++ b/generated/prod/go/50_nextcloud_resource.go
@@ -0,0 +1,414 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: nextcloud
+// Service ID: 50
+
+var _ resource.Resource = &NextcloudResource{}
+var _ resource.ResourceWithModifyPlan = &NextcloudResource{}
+var _ resource.ResourceWithImportState = &NextcloudResource{}
+
+type NextcloudResource struct {
+ client *core.UniversalClient
+}
+
+// NextcloudAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type NextcloudAccessConfigurationModel struct {
+ Domain types.String `tfsdk:"domain" json:"domain"`
+}
+
+// NextcloudAppConfigurationModel — вложенная модель для map-fixed параметра appConfiguration.
+type NextcloudAppConfigurationModel struct {
+ Version types.String `tfsdk:"version" json:"version"`
+ DocumentServer types.String `tfsdk:"document_server" json:"documentServer"`
+ LogLevel types.String `tfsdk:"log_level" json:"logLevel"`
+}
+
+// NextcloudClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type NextcloudClusterConfigurationModel struct {
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+ Disk types.Int64 `tfsdk:"disk" json:"disk"`
+}
+
+// NextcloudStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type NextcloudStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+ PsqlEnabled types.Bool `tfsdk:"psql_enabled" json:"psqlEnabled"`
+ RedisEnabled types.Bool `tfsdk:"redis_enabled" json:"redisEnabled"`
+ PsqlUid types.String `tfsdk:"psql_uid" json:"psqlUid"`
+ RedisUid types.String `tfsdk:"redis_uid" json:"redisUid"`
+}
+
+type NextcloudModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AccessConfiguration *NextcloudAccessConfigurationModel `tfsdk:"access_configuration"`
+ AppConfiguration *NextcloudAppConfigurationModel `tfsdk:"app_configuration"`
+ ClusterConfiguration *NextcloudClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ StartupConfiguration *NextcloudStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewNextcloudResource() resource.Resource {
+ return &NextcloudResource{}
+}
+
+func (r *NextcloudResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_nextcloud"
+}
+
+func (r *NextcloudResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("nextcloud-f6pb9obd"), MarkdownDescription: "Если указан FQDN в формате: `myapp.example.com`, предварительно будет проверяться DNS-запись (А-запись **должна смотреть на IP Ingress кластера ресурсной платформы**)
Если указан в формате `myapp`, DNS-запись будет автоматически создана в служебной DNS-зоне"},
+ }},
+ "app_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("33.0.4")},
+ "document_server": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("collabora"), MarkdownDescription: "Выбор сервера документов (onlyoffice)/(collabora). onlyoffice пока не доступен
Размер пода collabora:
1.8 ядра. 2Gi ОЗУ"},
+ "log_level": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("INFO"), MarkdownDescription: "Включает отображение логов самого nextcloud
Логи уходят в /dev/stdout контейнера
`NONE` отключает логирование"},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1000), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1024), MarkdownDescription: "Указывается в Megabytes"},
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Указывается в GIGAbytes
На данном диске хранятся данные Nextcloud"},
+ }},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"},
+ "psql_enabled": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Инстанс обязан быть развёрнут на той же ресурсной платформе, где и будет располагаться Nextcloud"},
+ "redis_enabled": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Инстанс обязан быть развёрнут на той же ресурсной платформе, где и будет располагаться Nextcloud"},
+ "psql_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Необходимо выбирать, когда выбран параметр `Подключить PostgreSQL из Личного кабинета`"},
+ "redis_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Необходимо выбирать, когда выбран параметр `Подключить Redis из Личного кабинета`"},
+ }},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *NextcloudResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *NextcloudModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *NextcloudModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan NextcloudModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 1055: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString()), "psqlEnabled": fmt.Sprintf("%v", config.StartupConfiguration.PsqlEnabled.ValueBool()), "redisEnabled": fmt.Sprintf("%v", config.StartupConfiguration.RedisEnabled.ValueBool()), "psqlUid": fmt.Sprintf("\"%s\"", config.StartupConfiguration.PsqlUid.ValueString()), "redisUid": fmt.Sprintf("\"%s\"", config.StartupConfiguration.RedisUid.ValueString())}),
+ 1056: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64())}),
+ 1057: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Domain.ValueString())}),
+ 1058: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.AppConfiguration.Version.ValueString()), "documentServer": fmt.Sprintf("\"%s\"", config.AppConfiguration.DocumentServer.ValueString()), "logLevel": fmt.Sprintf("\"%s\"", config.AppConfiguration.LogLevel.ValueString())}),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 50, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *NextcloudResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data NextcloudModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 50, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 1055: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString()), "psqlEnabled": fmt.Sprintf("%v", data.StartupConfiguration.PsqlEnabled.ValueBool()), "redisEnabled": fmt.Sprintf("%v", data.StartupConfiguration.RedisEnabled.ValueBool()), "psqlUid": fmt.Sprintf("\"%s\"", data.StartupConfiguration.PsqlUid.ValueString()), "redisUid": fmt.Sprintf("\"%s\"", data.StartupConfiguration.RedisUid.ValueString())}),
+ 1056: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64())}),
+ 1057: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Domain.ValueString())}),
+ 1058: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.AppConfiguration.Version.ValueString()), "documentServer": fmt.Sprintf("\"%s\"", data.AppConfiguration.DocumentServer.ValueString()), "logLevel": fmt.Sprintf("\"%s\"", data.AppConfiguration.LogLevel.ValueString())}),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 50, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 50, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *NextcloudResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state NextcloudModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 50, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *NextcloudResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan NextcloudModel
+ var state NextcloudModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 1059: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64())}),
+ 1060: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.AppConfiguration.Version.ValueString()), "documentServer": fmt.Sprintf("\"%s\"", plan.AppConfiguration.DocumentServer.ValueString()), "logLevel": fmt.Sprintf("\"%s\"", plan.AppConfiguration.LogLevel.ValueString())}),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 50, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *NextcloudResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state NextcloudModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *NextcloudResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *NextcloudResource) 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
+}
diff --git a/generated/prod/go/81_superset_resource.go b/generated/prod/go/81_superset_resource.go
new file mode 100644
index 0000000..0e08f09
--- /dev/null
+++ b/generated/prod/go/81_superset_resource.go
@@ -0,0 +1,436 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: superset
+// Service ID: 81
+
+var _ resource.Resource = &SupersetResource{}
+var _ resource.ResourceWithModifyPlan = &SupersetResource{}
+var _ resource.ResourceWithImportState = &SupersetResource{}
+
+type SupersetResource struct {
+ client *core.UniversalClient
+}
+
+type SupersetModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ Domain types.String `tfsdk:"domain"`
+ Emails types.String `tfsdk:"emails"`
+ ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"`
+ ResourceDisk types.Int64 `tfsdk:"resource_disk"`
+ ResourceInstances types.Int64 `tfsdk:"resource_instances"`
+ ResourceMemory types.Int64 `tfsdk:"resource_memory"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewSupersetResource() resource.Resource {
+ return &SupersetResource{}
+}
+
+func (r *SupersetResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_superset"
+}
+
+func (r *SupersetResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("superset-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."},
+ "emails": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("testuser1"), MarkdownDescription: "Учётная запись, которой будут выданы админские права. В формате email [ну не совсем. Для доменных учетных записей - без доменного суффикса]. Обязана находиться в Keycloak. Можно передавать несколько через пробел Не изменяется после создания."},
+ "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Квота ядра пода (Указывается в mili). 1000 Mili = 1 ядро Не изменяется после создания."},
+ "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Квота диска (Указывается в GIGAbytes) Не изменяется после создания."},
+ "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов Не изменяется после создания."},
+ "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Квота памяти пода (Указывается в Megabytes) Не изменяется после создания."},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *SupersetResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *SupersetModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *SupersetModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan SupersetModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.Emails.IsNull() && !plan.Emails.IsUnknown() && !state.Emails.IsNull() && !state.Emails.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Emails.ValueString(), state.Emails.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить emails", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() {
+ if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_c_p_u", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() {
+ if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_disk", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() {
+ if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_instances", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() {
+ if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_memory", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 14: resources_core.FormatString(config.Domain),
+ 20: resources_core.FormatString(config.Emails),
+ 96: resources_core.FormatInt64(config.ResourceCPU),
+ 97: resources_core.FormatInt64(config.ResourceMemory),
+ 98: resources_core.FormatInt64(config.ResourceDisk),
+ 109: resources_core.FormatString(config.ResourceRealm),
+ 227: resources_core.FormatInt64(config.ResourceInstances),
+ }
+ desiredDomain := ""
+ if !config.Domain.IsNull() && !config.Domain.IsUnknown() {
+ desiredDomain = config.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 81, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *SupersetResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data SupersetModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ if !data.Domain.IsNull() && !data.Domain.IsUnknown() {
+ desiredDomain = data.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 81, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 14: resources_core.FormatString(data.Domain),
+ 20: resources_core.FormatString(data.Emails),
+ 96: resources_core.FormatInt64(data.ResourceCPU),
+ 97: resources_core.FormatInt64(data.ResourceMemory),
+ 98: resources_core.FormatInt64(data.ResourceDisk),
+ 109: resources_core.FormatString(data.ResourceRealm),
+ 227: resources_core.FormatInt64(data.ResourceInstances),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 81, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 81, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "emails", Field: "Emails", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *SupersetResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state SupersetModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 81, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "emails", Field: "Emails", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *SupersetResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan SupersetModel
+ var state SupersetModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 81, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "emails", Field: "Emails", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *SupersetResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state SupersetModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *SupersetResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *SupersetResource) 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
+}
diff --git a/generated/prod/go/82_harbor_resource.go b/generated/prod/go/82_harbor_resource.go
new file mode 100644
index 0000000..63306e1
--- /dev/null
+++ b/generated/prod/go/82_harbor_resource.go
@@ -0,0 +1,458 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: harbor
+// Service ID: 82
+
+var _ resource.Resource = &HarborResource{}
+var _ resource.ResourceWithModifyPlan = &HarborResource{}
+var _ resource.ResourceWithImportState = &HarborResource{}
+
+type HarborResource struct {
+ client *core.UniversalClient
+}
+
+type HarborModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ Domain types.String `tfsdk:"domain"`
+ ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"`
+ ResourceInstances types.Int64 `tfsdk:"resource_instances"`
+ ResourceMemory types.Int64 `tfsdk:"resource_memory"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ S3Uid types.String `tfsdk:"s3_uid"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewHarborResource() resource.Resource {
+ return &HarborResource{}
+}
+
+func (r *HarborResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_harbor"
+}
+
+func (r *HarborResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("harbor-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."},
+ "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100), MarkdownDescription: "Квота ядра пода (Указывается в mili). 1000Mili = 1 Ядро Не изменяется после создания."},
+ "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов Не изменяется после создания."},
+ "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(200), MarkdownDescription: "Квота памяти пода (Указывается в Megabytes) Не изменяется после создания."},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ "s3_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID экземпляра услуги S3 Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *HarborResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *HarborModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *HarborModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan HarborModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() {
+ if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_c_p_u", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() {
+ if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_instances", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() {
+ if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_memory", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.S3Uid.IsNull() && !plan.S3Uid.IsUnknown() && !state.S3Uid.IsNull() && !state.S3Uid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.S3Uid.ValueString(), state.S3Uid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить s3_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.S3Uid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "s3_uid is required.")
+ return
+ }
+ if config.S3Uid.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.S3Uid.IsNull() && !config.S3Uid.IsUnknown() {
+ resolvedS3Uid, err := r.client.ResolveRefSvcParamValue(ctx, 12, config.S3Uid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve s3_uid", err.Error())
+ } else if resolvedS3Uid != "" && resolvedS3Uid != config.S3Uid.ValueString() {
+ config.S3Uid = types.StringValue(resolvedS3Uid)
+ }
+ }
+ params := map[int]string{
+ 16: resources_core.FormatString(config.Domain),
+ 110: resources_core.FormatString(config.ResourceRealm),
+ 228: resources_core.FormatInt64(config.ResourceCPU),
+ 229: resources_core.FormatInt64(config.ResourceMemory),
+ 230: resources_core.FormatInt64(config.ResourceInstances),
+ 231: resources_core.FormatString(config.S3Uid),
+ }
+ desiredDomain := ""
+ if !config.Domain.IsNull() && !config.Domain.IsUnknown() {
+ desiredDomain = config.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 82, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *HarborResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data HarborModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.S3Uid.IsNull() || data.S3Uid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "s3_uid is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ if !data.Domain.IsNull() && !data.Domain.IsUnknown() {
+ desiredDomain = data.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 82, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 16: resources_core.FormatString(data.Domain),
+ 110: resources_core.FormatString(data.ResourceRealm),
+ 228: resources_core.FormatInt64(data.ResourceCPU),
+ 229: resources_core.FormatInt64(data.ResourceMemory),
+ 230: resources_core.FormatInt64(data.ResourceInstances),
+ 231: resources_core.FormatString(data.S3Uid),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 82, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 82, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *HarborResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state HarborModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 82, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *HarborResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan HarborModel
+ var state HarborModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 82, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *HarborResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state HarborModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *HarborResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *HarborResource) 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
+}
diff --git a/generated/prod/go/86_k8s_velero_resource.go b/generated/prod/go/86_k8s_velero_resource.go
new file mode 100644
index 0000000..950e65b
--- /dev/null
+++ b/generated/prod/go/86_k8s_velero_resource.go
@@ -0,0 +1,402 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: k8s_velero
+// Service ID: 86
+
+var _ resource.Resource = &K8sVeleroResource{}
+var _ resource.ResourceWithModifyPlan = &K8sVeleroResource{}
+var _ resource.ResourceWithImportState = &K8sVeleroResource{}
+
+type K8sVeleroResource struct {
+ client *core.UniversalClient
+}
+
+// K8sVeleroBackupConfigurationModel — вложенная модель для map-fixed параметра backupConfiguration.
+type K8sVeleroBackupConfigurationModel struct {
+ S3BucketUid types.String `tfsdk:"s3_bucket_uid" json:"s3BucketUid"`
+}
+
+// K8sVeleroClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type K8sVeleroClusterConfigurationModel struct {
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+}
+
+// K8sVeleroNodeAgentConfigurationModel — вложенная модель для map-fixed параметра nodeAgentConfiguration.
+type K8sVeleroNodeAgentConfigurationModel struct {
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+}
+
+// K8sVeleroStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type K8sVeleroStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+}
+
+type K8sVeleroModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ BackupConfiguration *K8sVeleroBackupConfigurationModel `tfsdk:"backup_configuration"`
+ ClusterConfiguration *K8sVeleroClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ NodeAgentConfiguration *K8sVeleroNodeAgentConfigurationModel `tfsdk:"node_agent_configuration"`
+ StartupConfiguration *K8sVeleroStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewK8sVeleroResource() resource.Resource {
+ return &K8sVeleroResource{}
+}
+
+func (r *K8sVeleroResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_k8s_velero"
+}
+
+func (r *K8sVeleroResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "backup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "s3_bucket_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Экземпляр S3-Бакета обязателен
Учётная запись будет создана автоматически, если её нету"},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1000), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2048), MarkdownDescription: "Указывается в Megabytes"},
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ }},
+ "node_agent_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2000), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2048), MarkdownDescription: "Указывается в Megabytes"},
+ }},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru")},
+ }},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *K8sVeleroResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *K8sVeleroModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *K8sVeleroModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan K8sVeleroModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 962: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64())}),
+ 963: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}),
+ 964: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.NodeAgentConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.NodeAgentConfiguration.Memory.ValueInt64())}),
+ 1076: resources_core.BuildJSON(map[string]string{"s3BucketUid": fmt.Sprintf("\"%s\"", config.BackupConfiguration.S3BucketUid.ValueString())}),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 86, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *K8sVeleroResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data K8sVeleroModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 86, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 962: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64())}),
+ 963: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}),
+ 964: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.NodeAgentConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.NodeAgentConfiguration.Memory.ValueInt64())}),
+ 1076: resources_core.BuildJSON(map[string]string{"s3BucketUid": fmt.Sprintf("\"%s\"", data.BackupConfiguration.S3BucketUid.ValueString())}),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 86, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 86, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "nodeAgentConfiguration", Field: "NodeAgentConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8sVeleroResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state K8sVeleroModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 86, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "nodeAgentConfiguration", Field: "NodeAgentConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *K8sVeleroResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan K8sVeleroModel
+ var state K8sVeleroModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 965: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64())}),
+ 966: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", plan.NodeAgentConfiguration.Memory.ValueInt64()), "cpu": fmt.Sprintf("%d", plan.NodeAgentConfiguration.Cpu.ValueInt64())}),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 86, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "nodeAgentConfiguration", Field: "NodeAgentConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8sVeleroResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state K8sVeleroModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *K8sVeleroResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *K8sVeleroResource) 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
+}
diff --git a/generated/prod/go/88_k8s_ziti_controller_resource.go b/generated/prod/go/88_k8s_ziti_controller_resource.go
new file mode 100644
index 0000000..a3dd491
--- /dev/null
+++ b/generated/prod/go/88_k8s_ziti_controller_resource.go
@@ -0,0 +1,420 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: k8s_ziti_controller
+// Service ID: 88
+
+var _ resource.Resource = &K8sZitiControllerResource{}
+var _ resource.ResourceWithModifyPlan = &K8sZitiControllerResource{}
+var _ resource.ResourceWithImportState = &K8sZitiControllerResource{}
+
+type K8sZitiControllerResource struct {
+ client *core.UniversalClient
+}
+
+type K8sZitiControllerModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ Domain types.String `tfsdk:"domain"`
+ ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"`
+ ResourceDisk types.Int64 `tfsdk:"resource_disk"`
+ ResourceInstances types.Int64 `tfsdk:"resource_instances"`
+ ResourceMemory types.Int64 `tfsdk:"resource_memory"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewK8sZitiControllerResource() resource.Resource {
+ return &K8sZitiControllerResource{}
+}
+
+func (r *K8sZitiControllerResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_k8s_ziti_controller"
+}
+
+func (r *K8sZitiControllerResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8sZitiController-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."},
+ "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(300), MarkdownDescription: "Квота ядра пода (milicores) Не изменяется после создания."},
+ "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Квота диска (Указывается в GIGAbytes) Не изменяется после создания."},
+ "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов Не изменяется после создания."},
+ "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Квота памяти пода (MBytes) Не изменяется после создания."},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *K8sZitiControllerResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *K8sZitiControllerModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *K8sZitiControllerModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan K8sZitiControllerModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() {
+ if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_c_p_u", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() {
+ if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_disk", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() {
+ if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_instances", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() {
+ if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_memory", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 294: resources_core.FormatInt64(config.ResourceCPU),
+ 295: resources_core.FormatInt64(config.ResourceMemory),
+ 296: resources_core.FormatInt64(config.ResourceDisk),
+ 297: resources_core.FormatInt64(config.ResourceInstances),
+ 298: resources_core.FormatString(config.ResourceRealm),
+ 299: resources_core.FormatString(config.Domain),
+ }
+ desiredDomain := ""
+ if !config.Domain.IsNull() && !config.Domain.IsUnknown() {
+ desiredDomain = config.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 88, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *K8sZitiControllerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data K8sZitiControllerModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ if !data.Domain.IsNull() && !data.Domain.IsUnknown() {
+ desiredDomain = data.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 88, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 294: resources_core.FormatInt64(data.ResourceCPU),
+ 295: resources_core.FormatInt64(data.ResourceMemory),
+ 296: resources_core.FormatInt64(data.ResourceDisk),
+ 297: resources_core.FormatInt64(data.ResourceInstances),
+ 298: resources_core.FormatString(data.ResourceRealm),
+ 299: resources_core.FormatString(data.Domain),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 88, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 88, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8sZitiControllerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state K8sZitiControllerModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 88, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *K8sZitiControllerResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan K8sZitiControllerModel
+ var state K8sZitiControllerModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 88, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8sZitiControllerResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state K8sZitiControllerModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *K8sZitiControllerResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *K8sZitiControllerResource) 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
+}
diff --git a/generated/prod/go/89_flask_resource.go b/generated/prod/go/89_flask_resource.go
new file mode 100644
index 0000000..0951b0b
--- /dev/null
+++ b/generated/prod/go/89_flask_resource.go
@@ -0,0 +1,449 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: flask
+// Service ID: 89
+
+var _ resource.Resource = &FlaskResource{}
+var _ resource.ResourceWithModifyPlan = &FlaskResource{}
+var _ resource.ResourceWithImportState = &FlaskResource{}
+
+type FlaskResource struct {
+ client *core.UniversalClient
+}
+
+// FlaskAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type FlaskAccessConfigurationModel struct {
+ Domain types.String `tfsdk:"domain" json:"domain"`
+}
+
+// FlaskAppConfigurationModel — вложенная модель для map-fixed параметра appConfiguration.
+type FlaskAppConfigurationModel struct {
+ Version types.String `tfsdk:"version" json:"version"`
+ GitPath types.String `tfsdk:"git_path" json:"gitPath"`
+ HealthPath types.String `tfsdk:"health_path" json:"healthPath"`
+}
+
+// FlaskClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type FlaskClusterConfigurationModel struct {
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+}
+
+// FlaskStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type FlaskStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+}
+
+type FlaskModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // git_revision: при изменении вызывает redeploy вместо modify.
+ // Опциональное поле — если не задано, modify работает как обычно.
+ GitRevision types.String `tfsdk:"git_revision"`
+ AccessConfiguration *FlaskAccessConfigurationModel `tfsdk:"access_configuration"`
+ AppConfiguration *FlaskAppConfigurationModel `tfsdk:"app_configuration"`
+ ClusterConfiguration *FlaskClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ JsonEnv types.String `tfsdk:"json_env"`
+ StartupConfiguration *FlaskStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewFlaskResource() resource.Resource {
+ return &FlaskResource{}
+}
+
+func (r *FlaskResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_flask"
+}
+
+func (r *FlaskResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("flask-whmcbvdz"), MarkdownDescription: "Если указан FQDN в формате: `myapp.example.com`, предварительно будет проверяться DNS-запись (А-запись **должна смотреть на IP Ingress кластера ресурсной платформы**)
Если указан в формате `myapp`, DNS-запись будет автоматически создана в служебной DNS-зоне"},
+ }},
+ "app_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("3.12")},
+ "git_path": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("https://github.com/Foxyhhd/Baldurs-Gate-test.git"), MarkdownDescription: "Путь до репозитория приложения
Обязательно должен быть передан как URL: `https://....`
Должен быть сетевой доступ до URL для скачивания исходников"},
+ "health_path": schema.StringAttribute{Optional: true, MarkdownDescription: "Путь до healtcheck для запуска пода в Kubernetes, который отдаст 200 код
Пустое значение означает, что будет проверяться исключительно доступность приложения по порту"},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(200), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(256), MarkdownDescription: "Указывается в Megabytes"},
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ }},
+ "json_env": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("{}")},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"},
+ }},
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // При изменении git_revision вызывается redeploy вместо modify.
+ // Если не задано — modify работает без изменений.
+ "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *FlaskResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *FlaskModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *FlaskModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan FlaskModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 1037: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1038: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64())}),
+ 1039: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Domain.ValueString())}),
+ 1040: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.HealthPath.ValueString())}),
+ 1041: resources_core.FormatString(config.JsonEnv),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 89, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *FlaskResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data FlaskModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 89, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 1037: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1038: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64())}),
+ 1039: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Domain.ValueString())}),
+ 1040: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.HealthPath.ValueString())}),
+ 1041: resources_core.FormatString(data.JsonEnv),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 89, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 89, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *FlaskResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state FlaskModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 89, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *FlaskResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan FlaskModel
+ var state FlaskModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.JsonEnv.IsNull() != state.JsonEnv.IsNull() || plan.JsonEnv.IsUnknown() != state.JsonEnv.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.JsonEnv.IsNull() && !plan.JsonEnv.IsUnknown() && !state.JsonEnv.IsNull() && !state.JsonEnv.IsUnknown() {
+ if !strings.EqualFold(plan.JsonEnv.ValueString(), state.JsonEnv.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // Проверяем изменился ли git_revision.
+ // Если да — вызываем redeploy вместо modify.
+ // git_revision = "" (не задано) → обычный modify.
+ redeployRequested := false
+ if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() {
+ if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() {
+ redeployRequested = true
+ }
+ }
+
+ if !hasServiceParamChanges && !redeployRequested {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ if redeployRequested {
+ // Вызываем redeploy — пустой params (операция не требует CFS-параметров)
+ if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ } else {
+ params := map[int]string{
+ 1042: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64())}),
+ 1043: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.HealthPath.ValueString())}),
+ 1044: resources_core.FormatString(plan.JsonEnv),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 89, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *FlaskResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state FlaskModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *FlaskResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *FlaskResource) 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
+}
diff --git a/generated/prod/go/90_postgres_backup_resource.go b/generated/prod/go/90_postgres_backup_resource.go
new file mode 100644
index 0000000..957eb3e
--- /dev/null
+++ b/generated/prod/go/90_postgres_backup_resource.go
@@ -0,0 +1,326 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: postgres
+// Subresource: backup
+
+var _ resource.Resource = &PostgresBackupResource{}
+
+// PostgresBackupResource manages a subresource via operations.
+type PostgresBackupResource struct {
+ client *core.UniversalClient
+}
+
+type PostgresBackupModel struct {
+ ID types.String `tfsdk:"id"`
+ PostgresID types.String `tfsdk:"postgres_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"`
+}
+
+func NewPostgresBackupResource() resource.Resource {
+ return &PostgresBackupResource{}
+}
+
+func (r *PostgresBackupResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_postgres_backup"
+}
+
+func (r *PostgresBackupResource) 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()}},
+ "postgres_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."},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *PostgresBackupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan PostgresBackupModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.PostgresID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{}
+ identityCodes := []string{}
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("backup", 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, "backup", 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{})
+
+ 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, "create_backup", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_backup", 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, "backup", 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, "backup", 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, "backup", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *PostgresBackupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state PostgresBackupModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *PostgresBackupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan PostgresBackupModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state PostgresBackupModel
+ 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.PostgresID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{}
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *PostgresBackupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state PostgresBackupModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.PostgresID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{}
+ identityCodes := []string{}
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("backup", 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{})
+
+ 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, "", 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 *PostgresBackupResource) 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
+}
diff --git a/generated/prod/go/90_postgres_database_resource.go b/generated/prod/go/90_postgres_database_resource.go
new file mode 100644
index 0000000..7310720
--- /dev/null
+++ b/generated/prod/go/90_postgres_database_resource.go
@@ -0,0 +1,358 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: postgres
+// Subresource: database
+
+var _ resource.Resource = &PostgresDatabaseResource{}
+
+// PostgresDatabaseResource manages a subresource via operations.
+type PostgresDatabaseResource struct {
+ client *core.UniversalClient
+}
+
+type PostgresDatabaseModel struct {
+ ID types.String `tfsdk:"id"`
+ PostgresID types.String `tfsdk:"postgres_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"`
+ DbName types.String `tfsdk:"db_name"`
+ DbOwner types.String `tfsdk:"db_owner"`
+}
+
+func NewPostgresDatabaseResource() resource.Resource {
+ return &PostgresDatabaseResource{}
+}
+
+func (r *PostgresDatabaseResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_postgres_database"
+}
+
+func (r *PostgresDatabaseResource) 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()}},
+ "postgres_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."},
+ "db_name": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "db_owner": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *PostgresDatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan PostgresDatabaseModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if plan.DbName.IsNull() || plan.DbName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "db_name is required.")
+ return
+ }
+ if plan.DbOwner.IsNull() || plan.DbOwner.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "db_owner is required.")
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.PostgresID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "dbName": resources_core.FormatString(plan.DbName),
+ "dbOwner": resources_core.FormatString(plan.DbOwner),
+ }
+ identityCodes := []string{
+ "dbName",
+ "dbOwner",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", 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, "database", 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{
+ "dbName": resources_core.FormatString(plan.DbName),
+ "dbOwner": resources_core.FormatString(plan.DbOwner),
+ })
+
+ 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, "create_database", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_database", 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, "database", 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, "database", 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, "database", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *PostgresDatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state PostgresDatabaseModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *PostgresDatabaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan PostgresDatabaseModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state PostgresDatabaseModel
+ 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.PostgresID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "dbName": resources_core.FormatString(plan.DbName),
+ "dbOwner": resources_core.FormatString(plan.DbOwner),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *PostgresDatabaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state PostgresDatabaseModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_database" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.PostgresID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "dbName": resources_core.FormatString(state.DbName),
+ "dbOwner": resources_core.FormatString(state.DbOwner),
+ }
+ identityCodes := []string{
+ "dbName",
+ "dbOwner",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", 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{
+ "dbName": resources_core.FormatString(state.DbName),
+ })
+
+ 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, "delete_database", 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 *PostgresDatabaseResource) 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
+}
diff --git a/generated/prod/go/90_postgres_resource.go b/generated/prod/go/90_postgres_resource.go
new file mode 100644
index 0000000..6ef10dd
--- /dev/null
+++ b/generated/prod/go/90_postgres_resource.go
@@ -0,0 +1,475 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: postgres
+// Service ID: 90
+
+var _ resource.Resource = &PostgresResource{}
+var _ resource.ResourceWithModifyPlan = &PostgresResource{}
+var _ resource.ResourceWithImportState = &PostgresResource{}
+
+type PostgresResource struct {
+ client *core.UniversalClient
+}
+
+// PostgresAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type PostgresAccessConfigurationModel struct {
+ SlaveIpSpace types.String `tfsdk:"slave_ip_space" json:"slaveIpSpace"`
+ SlaveAccessList types.String `tfsdk:"slave_access_list" json:"slaveAccessList"`
+ MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"`
+ MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"`
+}
+
+// PostgresAutoscaleConfigurationModel — вложенная модель для map-fixed параметра autoscaleConfiguration.
+type PostgresAutoscaleConfigurationModel struct {
+ Schedule types.Int64 `tfsdk:"schedule" json:"schedule"`
+ Quota types.Int64 `tfsdk:"quota" json:"quota"`
+ Percent types.Int64 `tfsdk:"percent" json:"percent"`
+ Enabled types.Bool `tfsdk:"enabled" json:"enabled"`
+}
+
+// PostgresBackupConfigurationModel — вложенная модель для map-fixed параметра backupConfiguration.
+type PostgresBackupConfigurationModel struct {
+ S3Uid types.String `tfsdk:"s3_uid" json:"s3Uid"`
+ Retain types.Int64 `tfsdk:"retain" json:"retain"`
+ Schedule types.String `tfsdk:"schedule" json:"schedule"`
+}
+
+// PostgresClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type PostgresClusterConfigurationModel struct {
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+ Disk types.Int64 `tfsdk:"disk" json:"disk"`
+}
+
+// PostgresPostgresConfigurationModel — вложенная модель для map-fixed параметра postgresConfiguration.
+type PostgresPostgresConfigurationModel struct {
+ Version types.String `tfsdk:"version" json:"version"`
+ SslRequired types.Bool `tfsdk:"ssl_required" json:"sslRequired"`
+ PoolerMaster types.Bool `tfsdk:"pooler_master" json:"poolerMaster"`
+ PoolerSlave types.Bool `tfsdk:"pooler_slave" json:"poolerSlave"`
+}
+
+// PostgresStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type PostgresStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+}
+
+type PostgresModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AccessConfiguration *PostgresAccessConfigurationModel `tfsdk:"access_configuration"`
+ AutoscaleConfiguration *PostgresAutoscaleConfigurationModel `tfsdk:"autoscale_configuration"`
+ BackupConfiguration *PostgresBackupConfigurationModel `tfsdk:"backup_configuration"`
+ ClusterConfiguration *PostgresClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ PostgresConf types.String `tfsdk:"postgres_conf"`
+ PostgresConfiguration *PostgresPostgresConfigurationModel `tfsdk:"postgres_configuration"`
+ StartupConfiguration *PostgresStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewPostgresResource() resource.Resource {
+ return &PostgresResource{}
+}
+
+func (r *PostgresResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_postgres"
+}
+
+func (r *PostgresResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "slave_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"},
+ "slave_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"},
+ "master_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"},
+ "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"},
+ }},
+ "autoscale_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "schedule": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "Требует включения autoScale. Окно обновления (пока не реализовано). Указывается час в виде Integer (0 / 5 / 12 / 23)"},
+ "quota": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100), MarkdownDescription: "До какого размера возможно увеличивать диск, когда включен Autoscale"},
+ "percent": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Требует включения autoScale. Позволяет указать в процентах расширение от текущего макс объема
Минимальный размер на который расширяется - **1Gb**"},
+ "enabled": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает автоскейлинг PV. При включении параметра необходимо настроить autoScalePercentage и autoScaleTechWindow. Алерт при срабатывании вызывает modify у текущей услуги"},
+ }},
+ "backup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "s3_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Экземпляр S3 обязателен"},
+ "retain": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(7)},
+ "schedule": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("0 0 * * *")},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Указывается в Megabytes"},
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Указывается в GIGAbytes"},
+ }},
+ "postgres_conf": schema.StringAttribute{Optional: true},
+ "postgres_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("17")},
+ "ssl_required": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "По умолчанию подключение обязательное (sslmode=require)"},
+ "pooler_master": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает pgPooler (на запись). Будет создан новый под
Размер пода **200m/400Mi**"},
+ "pooler_slave": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает pgPooler (на чтение). Будет создан новый под
Размер пода **200m/400Mi**"},
+ }},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"},
+ }},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *PostgresResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *PostgresModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *PostgresModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan PostgresModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 788: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64())}),
+ 789: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}),
+ 790: resources_core.BuildJSON(map[string]string{"slaveIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveAccessList.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString())}),
+ 791: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.PostgresConfiguration.Version.ValueString()), "sslRequired": fmt.Sprintf("%v", config.PostgresConfiguration.SslRequired.ValueBool()), "poolerMaster": fmt.Sprintf("%v", config.PostgresConfiguration.PoolerMaster.ValueBool()), "poolerSlave": fmt.Sprintf("%v", config.PostgresConfiguration.PoolerSlave.ValueBool())}),
+ 792: resources_core.FormatString(config.PostgresConf),
+ 793: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", config.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", config.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", config.BackupConfiguration.Schedule.ValueString())}),
+ 794: resources_core.BuildJSON(map[string]string{"schedule": fmt.Sprintf("%d", config.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", config.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", config.AutoscaleConfiguration.Percent.ValueInt64()), "enabled": fmt.Sprintf("%v", config.AutoscaleConfiguration.Enabled.ValueBool())}),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 90, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *PostgresResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data PostgresModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 90, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 788: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64())}),
+ 789: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}),
+ 790: resources_core.BuildJSON(map[string]string{"slaveIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveAccessList.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString())}),
+ 791: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.PostgresConfiguration.Version.ValueString()), "sslRequired": fmt.Sprintf("%v", data.PostgresConfiguration.SslRequired.ValueBool()), "poolerMaster": fmt.Sprintf("%v", data.PostgresConfiguration.PoolerMaster.ValueBool()), "poolerSlave": fmt.Sprintf("%v", data.PostgresConfiguration.PoolerSlave.ValueBool())}),
+ 792: resources_core.FormatString(data.PostgresConf),
+ 793: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", data.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", data.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", data.BackupConfiguration.Schedule.ValueString())}),
+ 794: resources_core.BuildJSON(map[string]string{"schedule": fmt.Sprintf("%d", data.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", data.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", data.AutoscaleConfiguration.Percent.ValueInt64()), "enabled": fmt.Sprintf("%v", data.AutoscaleConfiguration.Enabled.ValueBool())}),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 90, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 90, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"},
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "postgresConf", Field: "PostgresConf", Type: "array-map-fixed"},
+ {Code: "postgresConfiguration", Field: "PostgresConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *PostgresResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state PostgresModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 90, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"},
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "postgresConf", Field: "PostgresConf", Type: "array-map-fixed"},
+ {Code: "postgresConfiguration", Field: "PostgresConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *PostgresResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan PostgresModel
+ var state PostgresModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.PostgresConf.IsNull() != state.PostgresConf.IsNull() || plan.PostgresConf.IsUnknown() != state.PostgresConf.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.PostgresConf.IsNull() && !plan.PostgresConf.IsUnknown() && !state.PostgresConf.IsNull() && !state.PostgresConf.IsUnknown() {
+ if !strings.EqualFold(plan.PostgresConf.ValueString(), state.PostgresConf.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 795: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64())}),
+ 796: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveAccessList.ValueString())}),
+ 797: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.PostgresConfiguration.Version.ValueString()), "sslRequired": fmt.Sprintf("%v", plan.PostgresConfiguration.SslRequired.ValueBool()), "poolerMaster": fmt.Sprintf("%v", plan.PostgresConfiguration.PoolerMaster.ValueBool()), "poolerSlave": fmt.Sprintf("%v", plan.PostgresConfiguration.PoolerSlave.ValueBool())}),
+ 798: resources_core.FormatString(plan.PostgresConf),
+ 799: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", plan.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.Schedule.ValueString())}),
+ 800: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", plan.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Percent.ValueInt64())}),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 90, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"},
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "postgresConf", Field: "PostgresConf", Type: "array-map-fixed"},
+ {Code: "postgresConfiguration", Field: "PostgresConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *PostgresResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state PostgresModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *PostgresResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *PostgresResource) 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
+}
diff --git a/generated/prod/go/90_postgres_user_resource.go b/generated/prod/go/90_postgres_user_resource.go
new file mode 100644
index 0000000..5101eca
--- /dev/null
+++ b/generated/prod/go/90_postgres_user_resource.go
@@ -0,0 +1,358 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: postgres
+// Subresource: user
+
+var _ resource.Resource = &PostgresUserResource{}
+
+// PostgresUserResource manages a subresource via operations.
+type PostgresUserResource struct {
+ client *core.UniversalClient
+}
+
+type PostgresUserModel struct {
+ ID types.String `tfsdk:"id"`
+ PostgresID types.String `tfsdk:"postgres_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"`
+ Role types.String `tfsdk:"role"`
+ Username types.String `tfsdk:"username"`
+}
+
+func NewPostgresUserResource() resource.Resource {
+ return &PostgresUserResource{}
+}
+
+func (r *PostgresUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_postgres_user"
+}
+
+func (r *PostgresUserResource) 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()}},
+ "postgres_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."},
+ "role": schema.StringAttribute{Required: true, MarkdownDescription: "Выбрать роль из списка Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "username": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *PostgresUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan PostgresUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if plan.Role.IsNull() || plan.Role.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "role is required.")
+ return
+ }
+ if plan.Username.IsNull() || plan.Username.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "username is required.")
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.PostgresID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "role": resources_core.FormatString(plan.Role),
+ "username": resources_core.FormatString(plan.Username),
+ }
+ identityCodes := []string{
+ "role",
+ "username",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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, "user", 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{
+ "username": resources_core.FormatString(plan.Username),
+ "role": resources_core.FormatString(plan.Role),
+ })
+
+ 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, "create_user", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", 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, "user", 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, "user", 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, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *PostgresUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state PostgresUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *PostgresUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan PostgresUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state PostgresUserModel
+ 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.PostgresID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "role": resources_core.FormatString(plan.Role),
+ "username": resources_core.FormatString(plan.Username),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *PostgresUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state PostgresUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_user" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.PostgresID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "role": resources_core.FormatString(state.Role),
+ "username": resources_core.FormatString(state.Username),
+ }
+ identityCodes := []string{
+ "role",
+ "username",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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{
+ "username": resources_core.FormatString(state.Username),
+ })
+
+ 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, "delete_user", 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 *PostgresUserResource) 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
+}
diff --git a/generated/prod/go/91_redis_resource.go b/generated/prod/go/91_redis_resource.go
new file mode 100644
index 0000000..852aaeb
--- /dev/null
+++ b/generated/prod/go/91_redis_resource.go
@@ -0,0 +1,393 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: redis
+// Service ID: 91
+
+var _ resource.Resource = &RedisResource{}
+var _ resource.ResourceWithModifyPlan = &RedisResource{}
+var _ resource.ResourceWithImportState = &RedisResource{}
+
+type RedisResource struct {
+ client *core.UniversalClient
+}
+
+// RedisAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type RedisAccessConfigurationModel struct {
+ MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"`
+ MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"`
+ SlaveIpSpace types.String `tfsdk:"slave_ip_space" json:"slaveIpSpace"`
+ SlaveAccessList types.String `tfsdk:"slave_access_list" json:"slaveAccessList"`
+}
+
+// RedisClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type RedisClusterConfigurationModel struct {
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+ Disk types.Int64 `tfsdk:"disk" json:"disk"`
+}
+
+// RedisStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type RedisStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+}
+
+type RedisModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AccessConfiguration *RedisAccessConfigurationModel `tfsdk:"access_configuration"`
+ ClusterConfiguration *RedisClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ StartupConfiguration *RedisStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewRedisResource() resource.Resource {
+ return &RedisResource{}
+}
+
+func (r *RedisResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_redis"
+}
+
+func (r *RedisResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "master_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"},
+ "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"},
+ "slave_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"},
+ "slave_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Указывается в Megabytes"},
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Указывается в GIGAbytes"},
+ }},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"},
+ }},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *RedisResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *RedisModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *RedisModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan RedisModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 1061: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1062: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64())}),
+ 1063: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveAccessList.ValueString())}),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 91, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *RedisResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data RedisModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 91, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 1061: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1062: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64())}),
+ 1063: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveAccessList.ValueString())}),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 91, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 91, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *RedisResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state RedisModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 91, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *RedisResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan RedisModel
+ var state RedisModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 1064: resources_core.BuildJSON(map[string]string{"replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64())}),
+ 1065: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveAccessList.ValueString())}),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 91, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *RedisResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state RedisModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *RedisResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *RedisResource) 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
+}
diff --git a/generated/prod/go/92_mongodb_resource.go b/generated/prod/go/92_mongodb_resource.go
new file mode 100644
index 0000000..e4f0135
--- /dev/null
+++ b/generated/prod/go/92_mongodb_resource.go
@@ -0,0 +1,495 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: mongodb
+// Service ID: 92
+
+var _ resource.Resource = &MongodbResource{}
+var _ resource.ResourceWithModifyPlan = &MongodbResource{}
+var _ resource.ResourceWithImportState = &MongodbResource{}
+
+type MongodbResource struct {
+ client *core.UniversalClient
+}
+
+type MongodbModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ IpSpaceNameMaster types.String `tfsdk:"ip_space_name_master"`
+ MasterAccessList types.String `tfsdk:"master_access_list"`
+ ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"`
+ ResourceDisk types.Int64 `tfsdk:"resource_disk"`
+ ResourceInstances types.Int64 `tfsdk:"resource_instances"`
+ ResourceMemory types.Int64 `tfsdk:"resource_memory"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewMongodbResource() resource.Resource {
+ return &MongodbResource{}
+}
+
+func (r *MongodbResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_mongodb"
+}
+
+func (r *MongodbResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "ip_space_name_master": schema.StringAttribute{Optional: true, MarkdownDescription: "Если включен предыдущий параметр, то поле необходимо заполнить. Пример: `internet-no-antiddos-v1`"},
+ "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Список IP-адресов, с которых разрешён доступ. Если оставить пустым — доступ открыт всем.", PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()}},
+ "resource_c_p_u": schema.Int64Attribute{Required: true, MarkdownDescription: "Пример: `1000`"},
+ "resource_disk": schema.Int64Attribute{Required: true, MarkdownDescription: "Пример: `20`"},
+ "resource_instances": schema.Int64Attribute{Required: true, MarkdownDescription: "Пример: `2`"},
+ "resource_memory": schema.Int64Attribute{Required: true, MarkdownDescription: "Пример: `2048`"},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Выбрать платформу из списка Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *MongodbResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *MongodbModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *MongodbModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan MongodbModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.ResourceCPU.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_c_p_u is required.")
+ return
+ }
+ if config.ResourceCPU.IsUnknown() {
+ return
+ }
+ if config.ResourceDisk.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_disk is required.")
+ return
+ }
+ if config.ResourceDisk.IsUnknown() {
+ return
+ }
+ if config.ResourceInstances.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_instances is required.")
+ return
+ }
+ if config.ResourceInstances.IsUnknown() {
+ return
+ }
+ if config.ResourceMemory.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_memory is required.")
+ return
+ }
+ if config.ResourceMemory.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 487: resources_core.FormatInt64(config.ResourceInstances),
+ 588: resources_core.FormatInt64(config.ResourceMemory),
+ 589: resources_core.FormatInt64(config.ResourceCPU),
+ 590: resources_core.FormatInt64(config.ResourceDisk),
+ 595: resources_core.FormatString(config.ResourceRealm),
+ 646: resources_core.FormatString(config.IpSpaceNameMaster),
+ 889: resources_core.FormatString(config.MasterAccessList),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 92, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *MongodbResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data MongodbModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.ResourceCPU.IsNull() || data.ResourceCPU.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_c_p_u is required.")
+ return
+ }
+ if data.ResourceDisk.IsNull() || data.ResourceDisk.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_disk is required.")
+ return
+ }
+ if data.ResourceInstances.IsNull() || data.ResourceInstances.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_instances is required.")
+ return
+ }
+ if data.ResourceMemory.IsNull() || data.ResourceMemory.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_memory is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 92, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 487: resources_core.FormatInt64(data.ResourceInstances),
+ 588: resources_core.FormatInt64(data.ResourceMemory),
+ 589: resources_core.FormatInt64(data.ResourceCPU),
+ 590: resources_core.FormatInt64(data.ResourceDisk),
+ 595: resources_core.FormatString(data.ResourceRealm),
+ 646: resources_core.FormatString(data.IpSpaceNameMaster),
+ 889: resources_core.FormatString(data.MasterAccessList),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 92, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 92, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"},
+ {Code: "masterAccessList", Field: "MasterAccessList", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *MongodbResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state MongodbModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 92, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"},
+ {Code: "masterAccessList", Field: "MasterAccessList", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *MongodbResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan MongodbModel
+ var state MongodbModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.ResourceInstances.IsNull() != state.ResourceInstances.IsNull() || plan.ResourceInstances.IsUnknown() != state.ResourceInstances.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() {
+ if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() {
+ if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() {
+ if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceDisk.IsNull() != state.ResourceDisk.IsNull() || plan.ResourceDisk.IsUnknown() != state.ResourceDisk.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() {
+ if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.IpSpaceNameMaster.IsNull() != state.IpSpaceNameMaster.IsNull() || plan.IpSpaceNameMaster.IsUnknown() != state.IpSpaceNameMaster.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.IpSpaceNameMaster.IsNull() && !plan.IpSpaceNameMaster.IsUnknown() && !state.IpSpaceNameMaster.IsNull() && !state.IpSpaceNameMaster.IsUnknown() {
+ if !strings.EqualFold(plan.IpSpaceNameMaster.ValueString(), state.IpSpaceNameMaster.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.MasterAccessList.IsNull() != state.MasterAccessList.IsNull() || plan.MasterAccessList.IsUnknown() != state.MasterAccessList.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.MasterAccessList.IsNull() && !plan.MasterAccessList.IsUnknown() && !state.MasterAccessList.IsNull() && !state.MasterAccessList.IsUnknown() {
+ if !strings.EqualFold(plan.MasterAccessList.ValueString(), state.MasterAccessList.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 648: resources_core.FormatInt64(plan.ResourceInstances),
+ 649: resources_core.FormatInt64(plan.ResourceMemory),
+ 650: resources_core.FormatInt64(plan.ResourceCPU),
+ 651: resources_core.FormatInt64(plan.ResourceDisk),
+ 653: resources_core.FormatString(plan.IpSpaceNameMaster),
+ 890: resources_core.FormatString(plan.MasterAccessList),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 92, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"},
+ {Code: "masterAccessList", Field: "MasterAccessList", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *MongodbResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state MongodbModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *MongodbResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *MongodbResource) 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
+}
diff --git a/generated/prod/go/92_mongodb_user_resource.go b/generated/prod/go/92_mongodb_user_resource.go
new file mode 100644
index 0000000..6f0e40d
--- /dev/null
+++ b/generated/prod/go/92_mongodb_user_resource.go
@@ -0,0 +1,364 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: mongodb
+// Subresource: user
+
+var _ resource.Resource = &MongodbUserResource{}
+
+// MongodbUserResource manages a subresource via operations.
+type MongodbUserResource struct {
+ client *core.UniversalClient
+}
+
+type MongodbUserModel struct {
+ ID types.String `tfsdk:"id"`
+ MongodbID types.String `tfsdk:"mongodb_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"`
+ DbName types.String `tfsdk:"db_name"`
+ Role types.String `tfsdk:"role"`
+ Username types.String `tfsdk:"username"`
+}
+
+func NewMongodbUserResource() resource.Resource {
+ return &MongodbUserResource{}
+}
+
+func (r *MongodbUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_mongodb_user"
+}
+
+func (r *MongodbUserResource) 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()}},
+ "mongodb_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."},
+ "db_name": schema.StringAttribute{Required: true},
+ "role": schema.StringAttribute{Required: true},
+ "username": schema.StringAttribute{Required: true, MarkdownDescription: "Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *MongodbUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan MongodbUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if plan.DbName.IsNull() || plan.DbName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "db_name is required.")
+ return
+ }
+ if plan.Role.IsNull() || plan.Role.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "role is required.")
+ return
+ }
+ if plan.Username.IsNull() || plan.Username.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "username is required.")
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.MongodbID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "username": resources_core.FormatString(plan.Username),
+ }
+ identityCodes := []string{
+ "username",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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, "user", 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{
+ "username": resources_core.FormatString(plan.Username),
+ "dbName": resources_core.FormatString(plan.DbName),
+ "role": resources_core.FormatString(plan.Role),
+ })
+
+ 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, "create_user", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", 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, "user", 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, "user", 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, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *MongodbUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state MongodbUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *MongodbUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan MongodbUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "modify_user" == "" {
+ var state MongodbUserModel
+ 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.MongodbID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{
+ "username": resources_core.FormatString(plan.Username),
+ "dbName": resources_core.FormatString(plan.DbName),
+ "role": resources_core.FormatString(plan.Role),
+ })
+
+ idParams := map[string]string{
+ "username": resources_core.FormatString(plan.Username),
+ }
+
+ 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, "modify_user", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *MongodbUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state MongodbUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_user" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.MongodbID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "username": resources_core.FormatString(state.Username),
+ }
+ identityCodes := []string{
+ "username",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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{
+ "username": resources_core.FormatString(state.Username),
+ })
+
+ 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, "delete_user", 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 *MongodbUserResource) 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
+}
diff --git a/generated/prod/go/93_rabbitmq_resource.go b/generated/prod/go/93_rabbitmq_resource.go
new file mode 100644
index 0000000..c26d54b
--- /dev/null
+++ b/generated/prod/go/93_rabbitmq_resource.go
@@ -0,0 +1,452 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: rabbitmq
+// Service ID: 93
+
+var _ resource.Resource = &RabbitmqResource{}
+var _ resource.ResourceWithModifyPlan = &RabbitmqResource{}
+var _ resource.ResourceWithImportState = &RabbitmqResource{}
+
+type RabbitmqResource struct {
+ client *core.UniversalClient
+}
+
+type RabbitmqModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ IpSpaceNameMaster types.String `tfsdk:"ip_space_name_master"`
+ NeedExternalAddressMaster types.Bool `tfsdk:"need_external_address_master"`
+ ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"`
+ ResourceDisk types.Int64 `tfsdk:"resource_disk"`
+ ResourceInstances types.Int64 `tfsdk:"resource_instances"`
+ ResourceMemory types.Int64 `tfsdk:"resource_memory"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewRabbitmqResource() resource.Resource {
+ return &RabbitmqResource{}
+}
+
+func (r *RabbitmqResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_rabbitmq"
+}
+
+func (r *RabbitmqResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "ip_space_name_master": schema.StringAttribute{Optional: true, MarkdownDescription: "Пример: `internet-no-antiddos-v1`"},
+ "need_external_address_master": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Флаг, определяющий, нужно ли выделять внешний IP для доступа к master. Пример: `true` или `false`"},
+ "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1000), MarkdownDescription: "Пример: `2000`"},
+ "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(30), MarkdownDescription: "Пример: `20`"},
+ "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Пример: `1`"},
+ "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1124), MarkdownDescription: "Пример: `2048`"},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Выбрать платформу из списка Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *RabbitmqResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *RabbitmqModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *RabbitmqModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan RabbitmqModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 116: resources_core.FormatInt64(config.ResourceCPU),
+ 117: resources_core.FormatInt64(config.ResourceMemory),
+ 118: resources_core.FormatInt64(config.ResourceDisk),
+ 119: resources_core.FormatString(config.ResourceRealm),
+ 224: resources_core.FormatInt64(config.ResourceInstances),
+ 225: resources_core.FormatBool(config.NeedExternalAddressMaster),
+ 543: resources_core.FormatString(config.IpSpaceNameMaster),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 93, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *RabbitmqResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data RabbitmqModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 93, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 116: resources_core.FormatInt64(data.ResourceCPU),
+ 117: resources_core.FormatInt64(data.ResourceMemory),
+ 118: resources_core.FormatInt64(data.ResourceDisk),
+ 119: resources_core.FormatString(data.ResourceRealm),
+ 224: resources_core.FormatInt64(data.ResourceInstances),
+ 225: resources_core.FormatBool(data.NeedExternalAddressMaster),
+ 543: resources_core.FormatString(data.IpSpaceNameMaster),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 93, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 93, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"},
+ {Code: "needExternalAddressMaster", Field: "NeedExternalAddressMaster", Type: "bool"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *RabbitmqResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state RabbitmqModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 93, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"},
+ {Code: "needExternalAddressMaster", Field: "NeedExternalAddressMaster", Type: "bool"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *RabbitmqResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan RabbitmqModel
+ var state RabbitmqModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() {
+ if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() {
+ if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceDisk.IsNull() != state.ResourceDisk.IsNull() || plan.ResourceDisk.IsUnknown() != state.ResourceDisk.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() {
+ if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceInstances.IsNull() != state.ResourceInstances.IsNull() || plan.ResourceInstances.IsUnknown() != state.ResourceInstances.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() {
+ if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.NeedExternalAddressMaster.IsNull() != state.NeedExternalAddressMaster.IsNull() || plan.NeedExternalAddressMaster.IsUnknown() != state.NeedExternalAddressMaster.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.NeedExternalAddressMaster.IsNull() && !plan.NeedExternalAddressMaster.IsUnknown() && !state.NeedExternalAddressMaster.IsNull() && !state.NeedExternalAddressMaster.IsUnknown() {
+ if plan.NeedExternalAddressMaster.ValueBool() != state.NeedExternalAddressMaster.ValueBool() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.IpSpaceNameMaster.IsNull() != state.IpSpaceNameMaster.IsNull() || plan.IpSpaceNameMaster.IsUnknown() != state.IpSpaceNameMaster.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.IpSpaceNameMaster.IsNull() && !plan.IpSpaceNameMaster.IsUnknown() && !state.IpSpaceNameMaster.IsNull() && !state.IpSpaceNameMaster.IsUnknown() {
+ if !strings.EqualFold(plan.IpSpaceNameMaster.ValueString(), state.IpSpaceNameMaster.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 304: resources_core.FormatInt64(plan.ResourceCPU),
+ 305: resources_core.FormatInt64(plan.ResourceMemory),
+ 306: resources_core.FormatInt64(plan.ResourceDisk),
+ 307: resources_core.FormatInt64(plan.ResourceInstances),
+ 308: resources_core.FormatBool(plan.NeedExternalAddressMaster),
+ 545: resources_core.FormatString(plan.IpSpaceNameMaster),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 93, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"},
+ {Code: "needExternalAddressMaster", Field: "NeedExternalAddressMaster", Type: "bool"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *RabbitmqResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state RabbitmqModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *RabbitmqResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *RabbitmqResource) 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
+}
diff --git a/generated/prod/go/94_lucee_resource.go b/generated/prod/go/94_lucee_resource.go
new file mode 100644
index 0000000..34033fe
--- /dev/null
+++ b/generated/prod/go/94_lucee_resource.go
@@ -0,0 +1,449 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: lucee
+// Service ID: 94
+
+var _ resource.Resource = &LuceeResource{}
+var _ resource.ResourceWithModifyPlan = &LuceeResource{}
+var _ resource.ResourceWithImportState = &LuceeResource{}
+
+type LuceeResource struct {
+ client *core.UniversalClient
+}
+
+// LuceeAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type LuceeAccessConfigurationModel struct {
+ Domain types.String `tfsdk:"domain" json:"domain"`
+}
+
+// LuceeAppConfigurationModel — вложенная модель для map-fixed параметра appConfiguration.
+type LuceeAppConfigurationModel struct {
+ Version types.String `tfsdk:"version" json:"version"`
+ GitPath types.String `tfsdk:"git_path" json:"gitPath"`
+ HealthPath types.String `tfsdk:"health_path" json:"healthPath"`
+}
+
+// LuceeClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type LuceeClusterConfigurationModel struct {
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+ Cpu types.String `tfsdk:"cpu" json:"cpu"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+}
+
+// LuceeStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type LuceeStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+}
+
+type LuceeModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // git_revision: при изменении вызывает redeploy вместо modify.
+ // Опциональное поле — если не задано, modify работает как обычно.
+ GitRevision types.String `tfsdk:"git_revision"`
+ AccessConfiguration *LuceeAccessConfigurationModel `tfsdk:"access_configuration"`
+ AppConfiguration *LuceeAppConfigurationModel `tfsdk:"app_configuration"`
+ ClusterConfiguration *LuceeClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ JsonEnv types.String `tfsdk:"json_env"`
+ StartupConfiguration *LuceeStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewLuceeResource() resource.Resource {
+ return &LuceeResource{}
+}
+
+func (r *LuceeResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_lucee"
+}
+
+func (r *LuceeResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("lucee-un0nzrwz"), MarkdownDescription: "Если указан FQDN в формате: `myapp.example.com`, предварительно будет проверяться DNS-запись (А-запись **должна смотреть на IP Ingress кластера ресурсной платформы**)
Если указан в формате `myapp`, DNS-запись будет автоматически создана в служебной DNS-зоне"},
+ }},
+ "app_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("6.0")},
+ "git_path": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("https://github.com/xahys/testlucee"), MarkdownDescription: "Путь до репозитория приложения
Обязательно должен быть передан как URL: `https://....`
Должен быть сетевой доступ до URL для скачивания исходников"},
+ "health_path": schema.StringAttribute{Optional: true, MarkdownDescription: "Путь до healtcheck для запуска пода в Kubernetes, который отдаст 200 код
Пустое значение означает, что будет проверяться исключительно доступность приложения по порту"},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ "cpu": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("200"), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(256), MarkdownDescription: "Указывается в Megabytes"},
+ }},
+ "json_env": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("{}"), MarkdownDescription: "Передаётся в формате КЛЮЧ:ЗНАЧЕНИЕ"},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"},
+ }},
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // При изменении git_revision вызывается redeploy вместо modify.
+ // Если не задано — modify работает без изменений.
+ "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *LuceeResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *LuceeModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *LuceeModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan LuceeModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 1019: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1020: resources_core.BuildJSON(map[string]string{"replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("\"%s\"", config.ClusterConfiguration.Cpu.ValueString()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64())}),
+ 1021: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Domain.ValueString())}),
+ 1022: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.HealthPath.ValueString())}),
+ 1023: resources_core.FormatString(config.JsonEnv),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 94, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *LuceeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data LuceeModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 94, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 1019: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1020: resources_core.BuildJSON(map[string]string{"replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("\"%s\"", data.ClusterConfiguration.Cpu.ValueString()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64())}),
+ 1021: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Domain.ValueString())}),
+ 1022: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.HealthPath.ValueString())}),
+ 1023: resources_core.FormatString(data.JsonEnv),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 94, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 94, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *LuceeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state LuceeModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 94, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *LuceeResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan LuceeModel
+ var state LuceeModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.JsonEnv.IsNull() != state.JsonEnv.IsNull() || plan.JsonEnv.IsUnknown() != state.JsonEnv.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.JsonEnv.IsNull() && !plan.JsonEnv.IsUnknown() && !state.JsonEnv.IsNull() && !state.JsonEnv.IsUnknown() {
+ if !strings.EqualFold(plan.JsonEnv.ValueString(), state.JsonEnv.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // Проверяем изменился ли git_revision.
+ // Если да — вызываем redeploy вместо modify.
+ // git_revision = "" (не задано) → обычный modify.
+ redeployRequested := false
+ if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() {
+ if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() {
+ redeployRequested = true
+ }
+ }
+
+ if !hasServiceParamChanges && !redeployRequested {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ if redeployRequested {
+ // Вызываем redeploy — пустой params (операция не требует CFS-параметров)
+ if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ } else {
+ params := map[int]string{
+ 1024: resources_core.BuildJSON(map[string]string{"replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("\"%s\"", plan.ClusterConfiguration.Cpu.ValueString()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64())}),
+ 1025: resources_core.BuildJSON(map[string]string{"gitPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.HealthPath.ValueString()), "version": fmt.Sprintf("\"%s\"", plan.AppConfiguration.Version.ValueString())}),
+ 1026: resources_core.FormatString(plan.JsonEnv),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 94, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *LuceeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state LuceeModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *LuceeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *LuceeResource) 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
+}
diff --git a/generated/prod/go/95_nodejs_resource.go b/generated/prod/go/95_nodejs_resource.go
new file mode 100644
index 0000000..b9f5f92
--- /dev/null
+++ b/generated/prod/go/95_nodejs_resource.go
@@ -0,0 +1,449 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: nodejs
+// Service ID: 95
+
+var _ resource.Resource = &NodejsResource{}
+var _ resource.ResourceWithModifyPlan = &NodejsResource{}
+var _ resource.ResourceWithImportState = &NodejsResource{}
+
+type NodejsResource struct {
+ client *core.UniversalClient
+}
+
+// NodejsAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type NodejsAccessConfigurationModel struct {
+ Domain types.String `tfsdk:"domain" json:"domain"`
+}
+
+// NodejsAppConfigurationModel — вложенная модель для map-fixed параметра appConfiguration.
+type NodejsAppConfigurationModel struct {
+ Version types.String `tfsdk:"version" json:"version"`
+ GitPath types.String `tfsdk:"git_path" json:"gitPath"`
+ HealthPath types.String `tfsdk:"health_path" json:"healthPath"`
+}
+
+// NodejsClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type NodejsClusterConfigurationModel struct {
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+}
+
+// NodejsStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type NodejsStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+}
+
+type NodejsModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // git_revision: при изменении вызывает redeploy вместо modify.
+ // Опциональное поле — если не задано, modify работает как обычно.
+ GitRevision types.String `tfsdk:"git_revision"`
+ AccessConfiguration *NodejsAccessConfigurationModel `tfsdk:"access_configuration"`
+ AppConfiguration *NodejsAppConfigurationModel `tfsdk:"app_configuration"`
+ ClusterConfiguration *NodejsClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ JsonEnv types.String `tfsdk:"json_env"`
+ StartupConfiguration *NodejsStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewNodejsResource() resource.Resource {
+ return &NodejsResource{}
+}
+
+func (r *NodejsResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_nodejs"
+}
+
+func (r *NodejsResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("node-mt4rik7o"), MarkdownDescription: "Если указан FQDN в формате: `myapp.example.com`, предварительно будет проверяться DNS-запись (А-запись **должна смотреть на IP Ingress кластера ресурсной платформы**)
Если указан в формате `myapp`, DNS-запись будет автоматически создана в служебной DNS-зоне"},
+ }},
+ "app_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("23")},
+ "git_path": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("https://github.com/xahys/testnode.git"), MarkdownDescription: "Путь до репозитория приложения
Обязательно должен быть передан как URL: `https://....`
Должен быть сетевой доступ до URL для скачивания исходников"},
+ "health_path": schema.StringAttribute{Optional: true, MarkdownDescription: "Путь до healtcheck для запуска пода в Kubernetes, который отдаст 200 код
Пустое значение означает, что будет проверяться исключительно доступность приложения по порту"},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(200), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(256), MarkdownDescription: "Указывается в Megabytes"},
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ }},
+ "json_env": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("{}"), MarkdownDescription: "Передаётся в формате КЛЮЧ:ЗНАЧЕНИЕ"},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"},
+ }},
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // При изменении git_revision вызывается redeploy вместо modify.
+ // Если не задано — modify работает без изменений.
+ "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *NodejsResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *NodejsModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *NodejsModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan NodejsModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 1028: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1029: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64())}),
+ 1030: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Domain.ValueString())}),
+ 1031: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.HealthPath.ValueString())}),
+ 1032: resources_core.FormatString(config.JsonEnv),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 95, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *NodejsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data NodejsModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 95, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 1028: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1029: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64())}),
+ 1030: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Domain.ValueString())}),
+ 1031: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.HealthPath.ValueString())}),
+ 1032: resources_core.FormatString(data.JsonEnv),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 95, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 95, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *NodejsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state NodejsModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 95, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *NodejsResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan NodejsModel
+ var state NodejsModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.JsonEnv.IsNull() != state.JsonEnv.IsNull() || plan.JsonEnv.IsUnknown() != state.JsonEnv.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.JsonEnv.IsNull() && !plan.JsonEnv.IsUnknown() && !state.JsonEnv.IsNull() && !state.JsonEnv.IsUnknown() {
+ if !strings.EqualFold(plan.JsonEnv.ValueString(), state.JsonEnv.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // Проверяем изменился ли git_revision.
+ // Если да — вызываем redeploy вместо modify.
+ // git_revision = "" (не задано) → обычный modify.
+ redeployRequested := false
+ if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() {
+ if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() {
+ redeployRequested = true
+ }
+ }
+
+ if !hasServiceParamChanges && !redeployRequested {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ if redeployRequested {
+ // Вызываем redeploy — пустой params (операция не требует CFS-параметров)
+ if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ } else {
+ params := map[int]string{
+ 1033: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64())}),
+ 1034: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.HealthPath.ValueString())}),
+ 1035: resources_core.FormatString(plan.JsonEnv),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 95, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *NodejsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state NodejsModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *NodejsResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *NodejsResource) 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
+}
diff --git a/generated/prod/go/96_pgadmin_resource.go b/generated/prod/go/96_pgadmin_resource.go
new file mode 100644
index 0000000..7d7568f
--- /dev/null
+++ b/generated/prod/go/96_pgadmin_resource.go
@@ -0,0 +1,504 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: pgadmin
+// Service ID: 96
+
+var _ resource.Resource = &PgadminResource{}
+var _ resource.ResourceWithModifyPlan = &PgadminResource{}
+var _ resource.ResourceWithImportState = &PgadminResource{}
+
+type PgadminResource struct {
+ client *core.UniversalClient
+}
+
+type PgadminModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // git_revision: при изменении вызывает redeploy вместо modify.
+ // Опциональное поле — если не задано, modify работает как обычно.
+ GitRevision types.String `tfsdk:"git_revision"`
+ Domain types.String `tfsdk:"domain"`
+ Login types.String `tfsdk:"login"`
+ Password types.String `tfsdk:"password"`
+ ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"`
+ ResourceDisk types.Int64 `tfsdk:"resource_disk"`
+ ResourceMemory types.Int64 `tfsdk:"resource_memory"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewPgadminResource() resource.Resource {
+ return &PgadminResource{}
+}
+
+func (r *PgadminResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_pgadmin"
+}
+
+func (r *PgadminResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("pgadmin-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."},
+ "login": schema.StringAttribute{Required: true, MarkdownDescription: "Логин для подключения к pgAdmin, в формате email (login@email.net) Не изменяется после создания."},
+ "password": schema.StringAttribute{Required: true, MarkdownDescription: "Пароль для подключения к pgAdmin Не изменяется после создания.", Sensitive: true},
+ "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Квота ядра пода (milicores). 1000Mili = 1 Ядро"},
+ "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Квота диска (Указывается в GIGAbytes)"},
+ "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Квота памяти пода (MBytes)"},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // При изменении git_revision вызывается redeploy вместо modify.
+ // Если не задано — modify работает без изменений.
+ "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *PgadminResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *PgadminModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *PgadminModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan PgadminModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.Login.IsNull() && !plan.Login.IsUnknown() && !state.Login.IsNull() && !state.Login.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Login.ValueString(), state.Login.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить login", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.Password.IsNull() && !plan.Password.IsUnknown() && !state.Password.IsNull() && !state.Password.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Password.ValueString(), state.Password.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить password", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.Login.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "login is required.")
+ return
+ }
+ if config.Login.IsUnknown() {
+ return
+ }
+ if config.Password.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "password is required.")
+ return
+ }
+ if config.Password.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 164: resources_core.FormatString(config.Domain),
+ 165: resources_core.FormatInt64(config.ResourceCPU),
+ 166: resources_core.FormatInt64(config.ResourceMemory),
+ 167: resources_core.FormatInt64(config.ResourceDisk),
+ 169: resources_core.FormatString(config.ResourceRealm),
+ 170: resources_core.FormatString(config.Login),
+ 171: resources_core.FormatString(config.Password),
+ }
+ desiredDomain := ""
+ if !config.Domain.IsNull() && !config.Domain.IsUnknown() {
+ desiredDomain = config.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 96, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *PgadminResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data PgadminModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.Login.IsNull() || data.Login.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "login is required.")
+ return
+ }
+ if data.Password.IsNull() || data.Password.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "password is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ if !data.Domain.IsNull() && !data.Domain.IsUnknown() {
+ desiredDomain = data.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 96, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 164: resources_core.FormatString(data.Domain),
+ 165: resources_core.FormatInt64(data.ResourceCPU),
+ 166: resources_core.FormatInt64(data.ResourceMemory),
+ 167: resources_core.FormatInt64(data.ResourceDisk),
+ 169: resources_core.FormatString(data.ResourceRealm),
+ 170: resources_core.FormatString(data.Login),
+ 171: resources_core.FormatString(data.Password),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 96, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 96, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "login", Field: "Login", Type: "string"},
+ {Code: "password", Field: "Password", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *PgadminResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state PgadminModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 96, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "login", Field: "Login", Type: "string"},
+ {Code: "password", Field: "Password", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *PgadminResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan PgadminModel
+ var state PgadminModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() {
+ if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() {
+ if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceDisk.IsNull() != state.ResourceDisk.IsNull() || plan.ResourceDisk.IsUnknown() != state.ResourceDisk.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() {
+ if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // Проверяем изменился ли git_revision.
+ // Если да — вызываем redeploy вместо modify.
+ // git_revision = "" (не задано) → обычный modify.
+ redeployRequested := false
+ if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() {
+ if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() {
+ redeployRequested = true
+ }
+ }
+
+ if !hasServiceParamChanges && !redeployRequested {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ if redeployRequested {
+ // Вызываем redeploy — пустой params (операция не требует CFS-параметров)
+ if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ } else {
+ params := map[int]string{
+ 172: resources_core.FormatInt64(plan.ResourceCPU),
+ 173: resources_core.FormatInt64(plan.ResourceMemory),
+ 174: resources_core.FormatInt64(plan.ResourceDisk),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 96, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "login", Field: "Login", Type: "string"},
+ {Code: "password", Field: "Password", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *PgadminResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state PgadminModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *PgadminResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *PgadminResource) 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
+}
diff --git a/generated/prod/go/97_nodered_resource.go b/generated/prod/go/97_nodered_resource.go
new file mode 100644
index 0000000..147bc59
--- /dev/null
+++ b/generated/prod/go/97_nodered_resource.go
@@ -0,0 +1,420 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: nodered
+// Service ID: 97
+
+var _ resource.Resource = &NoderedResource{}
+var _ resource.ResourceWithModifyPlan = &NoderedResource{}
+var _ resource.ResourceWithImportState = &NoderedResource{}
+
+type NoderedResource struct {
+ client *core.UniversalClient
+}
+
+type NoderedModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ Domain types.String `tfsdk:"domain"`
+ ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"`
+ ResourceDisk types.Int64 `tfsdk:"resource_disk"`
+ ResourceInstances types.Int64 `tfsdk:"resource_instances"`
+ ResourceMemory types.Int64 `tfsdk:"resource_memory"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewNoderedResource() resource.Resource {
+ return &NoderedResource{}
+}
+
+func (r *NoderedResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_nodered"
+}
+
+func (r *NoderedResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("nodered-1"), MarkdownDescription: "Наименование поддомена, будет добавлено в k8stemplate.dev.nubes.ru Не изменяется после создания."},
+ "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Квота ядра пода (Указывается в mili). 1000Mili = 1 Ядро Не изменяется после создания."},
+ "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Квота диска (Указывается в GIGAbytes) Не изменяется после создания."},
+ "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов Не изменяется после создания."},
+ "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Квота памяти пода (Указывается в Megabytes) Не изменяется после создания."},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *NoderedResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *NoderedModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *NoderedModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan NoderedModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() {
+ if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_c_p_u", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() {
+ if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_disk", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() {
+ if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_instances", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() {
+ if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_memory", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 193: resources_core.FormatInt64(config.ResourceCPU),
+ 194: resources_core.FormatInt64(config.ResourceMemory),
+ 195: resources_core.FormatInt64(config.ResourceDisk),
+ 196: resources_core.FormatString(config.ResourceRealm),
+ 197: resources_core.FormatString(config.Domain),
+ 323: resources_core.FormatInt64(config.ResourceInstances),
+ }
+ desiredDomain := ""
+ if !config.Domain.IsNull() && !config.Domain.IsUnknown() {
+ desiredDomain = config.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 97, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *NoderedResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data NoderedModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ if !data.Domain.IsNull() && !data.Domain.IsUnknown() {
+ desiredDomain = data.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 97, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 193: resources_core.FormatInt64(data.ResourceCPU),
+ 194: resources_core.FormatInt64(data.ResourceMemory),
+ 195: resources_core.FormatInt64(data.ResourceDisk),
+ 196: resources_core.FormatString(data.ResourceRealm),
+ 197: resources_core.FormatString(data.Domain),
+ 323: resources_core.FormatInt64(data.ResourceInstances),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 97, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 97, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *NoderedResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state NoderedModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 97, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *NoderedResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan NoderedModel
+ var state NoderedModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 97, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *NoderedResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state NoderedModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *NoderedResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *NoderedResource) 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
+}
diff --git a/generated/prod/go/98_http_resource.go b/generated/prod/go/98_http_resource.go
new file mode 100644
index 0000000..1965e6d
--- /dev/null
+++ b/generated/prod/go/98_http_resource.go
@@ -0,0 +1,498 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: http
+// Service ID: 98
+
+var _ resource.Resource = &HttpResource{}
+var _ resource.ResourceWithModifyPlan = &HttpResource{}
+var _ resource.ResourceWithImportState = &HttpResource{}
+
+type HttpResource struct {
+ client *core.UniversalClient
+}
+
+type HttpModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // git_revision: при изменении вызывает redeploy вместо modify.
+ // Опциональное поле — если не задано, modify работает как обычно.
+ GitRevision types.String `tfsdk:"git_revision"`
+ Domain types.String `tfsdk:"domain"`
+ IngressAnnotations types.String `tfsdk:"ingress_annotations"`
+ JsonEnv types.String `tfsdk:"json_env"`
+ RegistryPath types.String `tfsdk:"registry_path"`
+ ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"`
+ ResourceInstances types.Int64 `tfsdk:"resource_instances"`
+ ResourceMemory types.Int64 `tfsdk:"resource_memory"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewHttpResource() resource.Resource {
+ return &HttpResource{}
+}
+
+func (r *HttpResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_http"
+}
+
+func (r *HttpResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("http-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."},
+ "ingress_annotations": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("{}"), MarkdownDescription: "Формат ключ/значение
Поддерживает пока простые аннотации Не изменяется после создания."},
+ "json_env": schema.StringAttribute{Optional: true, MarkdownDescription: "Массив переменных, будут переданы в env приложения Не изменяется после создания."},
+ "registry_path": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("nexus-sa.tst.nubes.ru/docker-nubes/jolt"), MarkdownDescription: "Путь до image, который будет установлен как deployment Не изменяется после создания."},
+ "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100), MarkdownDescription: "Квота ядра пода (Указывается в mili)"},
+ "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов"},
+ "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(200), MarkdownDescription: "Квота памяти пода (Указывается в Megabytes)"},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // При изменении git_revision вызывается redeploy вместо modify.
+ // Если не задано — modify работает без изменений.
+ "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *HttpResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *HttpModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *HttpModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan HttpModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.IngressAnnotations.IsNull() && !plan.IngressAnnotations.IsUnknown() && !state.IngressAnnotations.IsNull() && !state.IngressAnnotations.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.IngressAnnotations.ValueString(), state.IngressAnnotations.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить ingress_annotations", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.JsonEnv.IsNull() && !plan.JsonEnv.IsUnknown() && !state.JsonEnv.IsNull() && !state.JsonEnv.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.JsonEnv.ValueString(), state.JsonEnv.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить json_env", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.RegistryPath.IsNull() && !plan.RegistryPath.IsUnknown() && !state.RegistryPath.IsNull() && !state.RegistryPath.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.RegistryPath.ValueString(), state.RegistryPath.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить registry_path", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 212: resources_core.FormatString(config.Domain),
+ 213: resources_core.FormatInt64(config.ResourceCPU),
+ 214: resources_core.FormatInt64(config.ResourceMemory),
+ 215: resources_core.FormatInt64(config.ResourceInstances),
+ 216: resources_core.FormatString(config.ResourceRealm),
+ 217: resources_core.FormatString(config.RegistryPath),
+ 241: resources_core.FormatString(config.JsonEnv),
+ 782: resources_core.FormatString(config.IngressAnnotations),
+ }
+ desiredDomain := ""
+ if !config.Domain.IsNull() && !config.Domain.IsUnknown() {
+ desiredDomain = config.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 98, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *HttpResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data HttpModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ if !data.Domain.IsNull() && !data.Domain.IsUnknown() {
+ desiredDomain = data.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 98, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 212: resources_core.FormatString(data.Domain),
+ 213: resources_core.FormatInt64(data.ResourceCPU),
+ 214: resources_core.FormatInt64(data.ResourceMemory),
+ 215: resources_core.FormatInt64(data.ResourceInstances),
+ 216: resources_core.FormatString(data.ResourceRealm),
+ 217: resources_core.FormatString(data.RegistryPath),
+ 241: resources_core.FormatString(data.JsonEnv),
+ 782: resources_core.FormatString(data.IngressAnnotations),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 98, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 98, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "ingressAnnotations", Field: "IngressAnnotations", Type: "string"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "registryPath", Field: "RegistryPath", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *HttpResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state HttpModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 98, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "ingressAnnotations", Field: "IngressAnnotations", Type: "string"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "registryPath", Field: "RegistryPath", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *HttpResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan HttpModel
+ var state HttpModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() {
+ if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() {
+ if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceInstances.IsNull() != state.ResourceInstances.IsNull() || plan.ResourceInstances.IsUnknown() != state.ResourceInstances.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() {
+ if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // Проверяем изменился ли git_revision.
+ // Если да — вызываем redeploy вместо modify.
+ // git_revision = "" (не задано) → обычный modify.
+ redeployRequested := false
+ if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() {
+ if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() {
+ redeployRequested = true
+ }
+ }
+
+ if !hasServiceParamChanges && !redeployRequested {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ if redeployRequested {
+ // Вызываем redeploy — пустой params (операция не требует CFS-параметров)
+ if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ } else {
+ params := map[int]string{
+ 218: resources_core.FormatInt64(plan.ResourceCPU),
+ 219: resources_core.FormatInt64(plan.ResourceMemory),
+ 220: resources_core.FormatInt64(plan.ResourceInstances),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 98, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "ingressAnnotations", Field: "IngressAnnotations", Type: "string"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "registryPath", Field: "RegistryPath", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *HttpResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state HttpModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *HttpResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *HttpResource) 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
+}
diff --git a/generated/prod/go/99_gitea_resource.go b/generated/prod/go/99_gitea_resource.go
new file mode 100644
index 0000000..ca019ef
--- /dev/null
+++ b/generated/prod/go/99_gitea_resource.go
@@ -0,0 +1,579 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: gitea
+// Service ID: 99
+
+var _ resource.Resource = &GiteaResource{}
+var _ resource.ResourceWithModifyPlan = &GiteaResource{}
+var _ resource.ResourceWithImportState = &GiteaResource{}
+
+type GiteaResource struct {
+ client *core.UniversalClient
+}
+
+type GiteaModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ Domain types.String `tfsdk:"domain"`
+ IngressExtraAnnotations types.String `tfsdk:"ingress_extra_annotations"`
+ IniServerParams types.String `tfsdk:"ini_server_params"`
+ PsqlUid types.String `tfsdk:"psql_uid"`
+ ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"`
+ ResourceDisk types.Int64 `tfsdk:"resource_disk"`
+ ResourceInstances types.Int64 `tfsdk:"resource_instances"`
+ ResourceMemory types.Int64 `tfsdk:"resource_memory"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewGiteaResource() resource.Resource {
+ return &GiteaResource{}
+}
+
+func (r *GiteaResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_gitea"
+}
+
+func (r *GiteaResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("gitea-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."},
+ "ingress_extra_annotations": schema.StringAttribute{Optional: true},
+ "ini_server_params": schema.StringAttribute{Optional: true},
+ "psql_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID Услуги k8sPgsql. Пользователя techGitea, базу данных gitea создаёт автоматически Не изменяется после создания."},
+ "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Квота ядра пода (Указывается в mili). 1000Mili = 1 Ядро"},
+ "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Квота диска (Указывается в GIGAbytes)"},
+ "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов"},
+ "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Квота памяти пода (Указывается в Megabytes)"},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *GiteaResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *GiteaModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *GiteaModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan GiteaModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.PsqlUid.IsNull() && !plan.PsqlUid.IsUnknown() && !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.PsqlUid.ValueString(), state.PsqlUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить psql_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.PsqlUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "psql_uid is required.")
+ return
+ }
+ if config.PsqlUid.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.PsqlUid.IsNull() && !config.PsqlUid.IsUnknown() {
+ resolvedPsqlUid, err := r.client.ResolveRefSvcParamValue(ctx, 90, config.PsqlUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve psql_uid", err.Error())
+ } else if resolvedPsqlUid != "" && resolvedPsqlUid != config.PsqlUid.ValueString() {
+ config.PsqlUid = types.StringValue(resolvedPsqlUid)
+ }
+ }
+ params := map[int]string{
+ 232: resources_core.FormatInt64(config.ResourceCPU),
+ 233: resources_core.FormatInt64(config.ResourceMemory),
+ 234: resources_core.FormatInt64(config.ResourceDisk),
+ 235: resources_core.FormatInt64(config.ResourceInstances),
+ 237: resources_core.FormatString(config.Domain),
+ 238: resources_core.FormatString(config.PsqlUid),
+ 818: resources_core.FormatString(config.IngressExtraAnnotations),
+ 821: resources_core.FormatString(config.IniServerParams),
+ }
+ desiredDomain := ""
+ if !config.Domain.IsNull() && !config.Domain.IsUnknown() {
+ desiredDomain = config.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 99, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *GiteaResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data GiteaModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.PsqlUid.IsNull() || data.PsqlUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "psql_uid is required.")
+ return
+ }
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalPsqlUid := data.PsqlUid
+ if !data.PsqlUid.IsNull() && !data.PsqlUid.IsUnknown() {
+ resolvedPsqlUid, err := r.client.ResolveRefSvcParamValue(ctx, 90, data.PsqlUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedPsqlUid != "" && resolvedPsqlUid != data.PsqlUid.ValueString() {
+ data.PsqlUid = types.StringValue(resolvedPsqlUid)
+ }
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ if !data.Domain.IsNull() && !data.Domain.IsUnknown() {
+ desiredDomain = data.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 99, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 232: resources_core.FormatInt64(data.ResourceCPU),
+ 233: resources_core.FormatInt64(data.ResourceMemory),
+ 234: resources_core.FormatInt64(data.ResourceDisk),
+ 235: resources_core.FormatInt64(data.ResourceInstances),
+ 237: resources_core.FormatString(data.Domain),
+ 238: resources_core.FormatString(data.PsqlUid),
+ 818: resources_core.FormatString(data.IngressExtraAnnotations),
+ 821: resources_core.FormatString(data.IniServerParams),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 99, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 99, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "ingressExtraAnnotations", Field: "IngressExtraAnnotations", Type: "string"},
+ {Code: "iniServerParams", Field: "IniServerParams", Type: "string"},
+ {Code: "psqlUid", Field: "PsqlUid", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() {
+ resolvedPsqlUid, err := r.client.ResolveRefSvcParamValue(ctx, 90, state.PsqlUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve psql_uid", err.Error())
+ } else if resolvedPsqlUid != "" && resolvedPsqlUid != state.PsqlUid.ValueString() {
+ state.PsqlUid = types.StringValue(resolvedPsqlUid)
+ }
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+ if !originalPsqlUid.IsNull() && !originalPsqlUid.IsUnknown() && !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() {
+ if strings.EqualFold(state.PsqlUid.ValueString(), originalPsqlUid.ValueString()) {
+ state.PsqlUid = originalPsqlUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *GiteaResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state GiteaModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 99, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "ingressExtraAnnotations", Field: "IngressExtraAnnotations", Type: "string"},
+ {Code: "iniServerParams", Field: "IniServerParams", Type: "string"},
+ {Code: "psqlUid", Field: "PsqlUid", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !newState.PsqlUid.IsNull() && !newState.PsqlUid.IsUnknown() {
+ resolvedPsqlUid, err := r.client.ResolveRefSvcParamValue(ctx, 90, newState.PsqlUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve psql_uid", err.Error())
+ } else if resolvedPsqlUid != "" && resolvedPsqlUid != newState.PsqlUid.ValueString() {
+ newState.PsqlUid = types.StringValue(resolvedPsqlUid)
+ }
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+ if !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() && !newState.PsqlUid.IsNull() && !newState.PsqlUid.IsUnknown() {
+ if strings.EqualFold(newState.PsqlUid.ValueString(), state.PsqlUid.ValueString()) {
+ newState.PsqlUid = state.PsqlUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *GiteaResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan GiteaModel
+ var state GiteaModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() {
+ if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() {
+ if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceDisk.IsNull() != state.ResourceDisk.IsNull() || plan.ResourceDisk.IsUnknown() != state.ResourceDisk.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() {
+ if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceInstances.IsNull() != state.ResourceInstances.IsNull() || plan.ResourceInstances.IsUnknown() != state.ResourceInstances.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() {
+ if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.IngressExtraAnnotations.IsNull() != state.IngressExtraAnnotations.IsNull() || plan.IngressExtraAnnotations.IsUnknown() != state.IngressExtraAnnotations.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.IngressExtraAnnotations.IsNull() && !plan.IngressExtraAnnotations.IsUnknown() && !state.IngressExtraAnnotations.IsNull() && !state.IngressExtraAnnotations.IsUnknown() {
+ if !strings.EqualFold(plan.IngressExtraAnnotations.ValueString(), state.IngressExtraAnnotations.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.IniServerParams.IsNull() != state.IniServerParams.IsNull() || plan.IniServerParams.IsUnknown() != state.IniServerParams.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.IniServerParams.IsNull() && !plan.IniServerParams.IsUnknown() && !state.IniServerParams.IsNull() && !state.IniServerParams.IsUnknown() {
+ if !strings.EqualFold(plan.IniServerParams.ValueString(), state.IniServerParams.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 259: resources_core.FormatInt64(plan.ResourceCPU),
+ 260: resources_core.FormatInt64(plan.ResourceMemory),
+ 261: resources_core.FormatInt64(plan.ResourceDisk),
+ 262: resources_core.FormatInt64(plan.ResourceInstances),
+ 819: resources_core.FormatString(plan.IngressExtraAnnotations),
+ 820: resources_core.FormatString(plan.IniServerParams),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 99, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "ingressExtraAnnotations", Field: "IngressExtraAnnotations", Type: "string"},
+ {Code: "iniServerParams", Field: "IniServerParams", Type: "string"},
+ {Code: "psqlUid", Field: "PsqlUid", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() {
+ resolvedPsqlUid, err := r.client.ResolveRefSvcParamValue(ctx, 90, state.PsqlUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve psql_uid", err.Error())
+ } else if resolvedPsqlUid != "" && resolvedPsqlUid != state.PsqlUid.ValueString() {
+ state.PsqlUid = types.StringValue(resolvedPsqlUid)
+ }
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+ if !plan.PsqlUid.IsNull() && !plan.PsqlUid.IsUnknown() && !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() {
+ if strings.EqualFold(state.PsqlUid.ValueString(), plan.PsqlUid.ValueString()) {
+ state.PsqlUid = plan.PsqlUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *GiteaResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state GiteaModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *GiteaResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *GiteaResource) 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
+}
diff --git a/generated/prod/go/registry.go b/generated/prod/go/registry.go
new file mode 100644
index 0000000..de4c11b
--- /dev/null
+++ b/generated/prod/go/registry.go
@@ -0,0 +1,73 @@
+package resources_gen
+
+import "github.com/hashicorp/terraform-plugin-framework/resource"
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+func AllResources() []func() resource.Resource {
+ return []func() resource.Resource{
+ NewAkhqResource,
+ NewClickhouseResource,
+ NewDnsrecordResource,
+ NewDnszoneResource,
+ NewDummyResource,
+ NewFlaskResource,
+ NewGiteaResource,
+ NewGiteaComplexResource,
+ NewHarborResource,
+ NewHttpResource,
+ NewK8sSthutrvalClusterResource,
+ NewK8sVeleroResource,
+ NewK8sZitiControllerResource,
+ NewKafkaResource,
+ NewLlmAiResource,
+ NewLuceeResource,
+ NewMariadbResource,
+ NewMongodbResource,
+ NewNextcloudResource,
+ NewNifiResource,
+ NewNodejsResource,
+ NewNoderedResource,
+ NewOpenwhiskResource,
+ NewPgadminResource,
+ NewPostgresResource,
+ NewRabbitmqResource,
+ NewRedisResource,
+ NewS3Resource,
+ NewS3bucketResource,
+ NewSupersetResource,
+ NewTemplateResource,
+ NewTenantResource,
+ NewValoTenantResource,
+ NewVappResource,
+ NewVcComplexResource,
+ NewVcMgmtSthutrvalClusterResource,
+ NewVcNsxtResource,
+ NewVcOrgResource,
+ NewVcOrgSaasResource,
+ NewVcVdcResource,
+ NewVcVdcGroupResource,
+ NewVcVmResource,
+ NewVcVmV2Resource,
+ NewVcVmV3Resource,
+ NewVcexternalipResource,
+ NewZonesV2Resource,
+ NewClickhouseDatabaseResource,
+ NewClickhouseUserResource,
+ NewK8sSthutrvalClusterUserResource,
+ NewKafkaTopicResource,
+ NewKafkaUserResource,
+ NewLlmAiSecretsResource,
+ NewLlmAiTeamResource,
+ NewMariadbDatabaseResource,
+ NewMariadbUserResource,
+ NewMongodbUserResource,
+ NewNextcloudBackupResource,
+ NewPostgresBackupResource,
+ NewPostgresDatabaseResource,
+ NewPostgresUserResource,
+ NewS3SubUserResource,
+ NewVcOrgUserResource,
+ NewVcVdcGroupVdcResource,
+ NewZonesV2RecordResource,
+ }
+}
diff --git a/generated/test/go/100_openwhisk_resource.go b/generated/test/go/100_openwhisk_resource.go
new file mode 100644
index 0000000..3c65789
--- /dev/null
+++ b/generated/test/go/100_openwhisk_resource.go
@@ -0,0 +1,322 @@
+package resources_gen
+
+import (
+ "context"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: openwhisk
+// Service ID: 100
+
+var _ resource.Resource = &OpenwhiskResource{}
+var _ resource.ResourceWithModifyPlan = &OpenwhiskResource{}
+var _ resource.ResourceWithImportState = &OpenwhiskResource{}
+
+type OpenwhiskResource struct {
+ client *core.UniversalClient
+}
+
+type OpenwhiskModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewOpenwhiskResource() resource.Resource {
+ return &OpenwhiskResource{}
+}
+
+func (r *OpenwhiskResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_openwhisk"
+}
+
+func (r *OpenwhiskResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *OpenwhiskResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *OpenwhiskModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *OpenwhiskModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan OpenwhiskModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{}
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 100, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *OpenwhiskResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data OpenwhiskModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 100, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{}
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 100, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 100, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{})
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *OpenwhiskResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state OpenwhiskModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 100, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{})
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *OpenwhiskResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan OpenwhiskModel
+ var state OpenwhiskModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 100, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{})
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *OpenwhiskResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state OpenwhiskModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *OpenwhiskResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *OpenwhiskResource) 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
+}
diff --git a/generated/test/go/109_zones_v2_record_resource.go b/generated/test/go/109_zones_v2_record_resource.go
new file mode 100644
index 0000000..b875e09
--- /dev/null
+++ b/generated/test/go/109_zones_v2_record_resource.go
@@ -0,0 +1,366 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: zones_v2
+// Subresource: record
+
+var _ resource.Resource = &ZonesV2RecordResource{}
+
+// ZonesV2RecordResource manages a subresource via operations.
+type ZonesV2RecordResource struct {
+ client *core.UniversalClient
+}
+
+type ZonesV2RecordModel struct {
+ ID types.String `tfsdk:"id"`
+ ZonesV2ID types.String `tfsdk:"zones_v2_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"`
+ RecordInput types.String `tfsdk:"record_input"`
+ RecordName types.String `tfsdk:"record_name"`
+ RecordTTL types.Int64 `tfsdk:"record_t_t_l"`
+ RecordType types.String `tfsdk:"record_type"`
+}
+
+func NewZonesV2RecordResource() resource.Resource {
+ return &ZonesV2RecordResource{}
+}
+
+func (r *ZonesV2RecordResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_zones_v2_record"
+}
+
+func (r *ZonesV2RecordResource) 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()}},
+ "zones_v2_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."},
+ "record_input": schema.StringAttribute{Required: true, MarkdownDescription: "Валидация производится только после запуска операции. Если при указанном типе будет указано некорректно значение, это будет описано в логе ошибки операции"},
+ "record_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя зоны в конце проставляется автоматически Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "record_t_t_l": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(3600), MarkdownDescription: "Time-to-Leave записи. Раз в какое время запись будет обновлена"},
+ "record_type": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("A")},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *ZonesV2RecordResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan ZonesV2RecordModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if plan.RecordInput.IsNull() || plan.RecordInput.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "record_input is required.")
+ return
+ }
+ if plan.RecordName.IsNull() || plan.RecordName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "record_name is required.")
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.ZonesV2ID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "recordName": resources_core.FormatString(plan.RecordName),
+ }
+ identityCodes := []string{
+ "recordName",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("record", 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, "record", 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{
+ "recordName": resources_core.FormatString(plan.RecordName),
+ "recordTTL": resources_core.FormatInt64(plan.RecordTTL),
+ "recordType": resources_core.FormatString(plan.RecordType),
+ "recordInput": resources_core.FormatString(plan.RecordInput),
+ })
+
+ 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, "create_record", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_record", 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, "record", 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, "record", 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, "record", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *ZonesV2RecordResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state ZonesV2RecordModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *ZonesV2RecordResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan ZonesV2RecordModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "modify_record" == "" {
+ var state ZonesV2RecordModel
+ 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.ZonesV2ID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{
+ "recordName": resources_core.FormatString(plan.RecordName),
+ "recordTTL": resources_core.FormatInt64(plan.RecordTTL),
+ "recordType": resources_core.FormatString(plan.RecordType),
+ "recordInput": resources_core.FormatString(plan.RecordInput),
+ })
+
+ idParams := map[string]string{
+ "recordName": resources_core.FormatString(plan.RecordName),
+ }
+
+ 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, "modify_record", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "record", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *ZonesV2RecordResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state ZonesV2RecordModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_record" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.ZonesV2ID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "recordName": resources_core.FormatString(state.RecordName),
+ }
+ identityCodes := []string{
+ "recordName",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("record", 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{
+ "recordName": resources_core.FormatString(state.RecordName),
+ })
+
+ 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, "delete_record", 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 *ZonesV2RecordResource) 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
+}
diff --git a/generated/test/go/109_zones_v2_resource.go b/generated/test/go/109_zones_v2_resource.go
new file mode 100644
index 0000000..52dbc10
--- /dev/null
+++ b/generated/test/go/109_zones_v2_resource.go
@@ -0,0 +1,484 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: zones_v2
+// Service ID: 109
+
+var _ resource.Resource = &ZonesV2Resource{}
+var _ resource.ResourceWithModifyPlan = &ZonesV2Resource{}
+var _ resource.ResourceWithImportState = &ZonesV2Resource{}
+
+type ZonesV2Resource struct {
+ client *core.UniversalClient
+}
+
+type ZonesV2Model struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ Records types.String `tfsdk:"records"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ ZoneExpire types.Int64 `tfsdk:"zone_expire"`
+ ZoneKind types.String `tfsdk:"zone_kind"`
+ ZoneMastersForSlave types.String `tfsdk:"zone_masters_for_slave"`
+ ZoneName types.String `tfsdk:"zone_name"`
+ ZoneRefresh types.Int64 `tfsdk:"zone_refresh"`
+ ZoneRetry types.Int64 `tfsdk:"zone_retry"`
+ ZoneTTL types.Int64 `tfsdk:"zone_t_t_l"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewZonesV2Resource() resource.Resource {
+ return &ZonesV2Resource{}
+}
+
+func (r *ZonesV2Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_zones_v2"
+}
+
+func (r *ZonesV2Resource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "records": schema.StringAttribute{Required: true},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("powerdns"), MarkdownDescription: "Не изменяется после создания."},
+ "zone_expire": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(3600000), MarkdownDescription: "Время в течение которого вторичный DNS будет пытаться завершить синхронизацию зоны с первичным Не изменяется после создания."},
+ "zone_kind": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("NATIVE"), MarkdownDescription: "Тип зоны Не изменяется после создания."},
+ "zone_masters_for_slave": schema.StringAttribute{Optional: true, MarkdownDescription: "Использовать если выбран тип SLAVE. IP адреса в формате через пробел для настройки Master-серверов Не изменяется после создания."},
+ "zone_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя зоны Не изменяется после создания."},
+ "zone_refresh": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(86400), MarkdownDescription: "Время ожидания ответа вторичного DNS перед запросом SOA-записи с первичных серверов Не изменяется после создания."},
+ "zone_retry": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(7200), MarkdownDescription: "Время в течение которого сервер повторно попробует синхронизировать информацию с первичных DNS-серверов Не изменяется после создания."},
+ "zone_t_t_l": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(86400), MarkdownDescription: "Время, в течение которого информация будет кешироваться другими DNS-серверами Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *ZonesV2Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *ZonesV2Model
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *ZonesV2Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan ZonesV2Model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneExpire.IsNull() && !plan.ZoneExpire.IsUnknown() && !state.ZoneExpire.IsNull() && !state.ZoneExpire.IsUnknown() {
+ if plan.ZoneExpire.ValueInt64() != state.ZoneExpire.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить zone_expire", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneKind.IsNull() && !plan.ZoneKind.IsUnknown() && !state.ZoneKind.IsNull() && !state.ZoneKind.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneKind.ValueString(), state.ZoneKind.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_kind", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneMastersForSlave.IsNull() && !plan.ZoneMastersForSlave.IsUnknown() && !state.ZoneMastersForSlave.IsNull() && !state.ZoneMastersForSlave.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneMastersForSlave.ValueString(), state.ZoneMastersForSlave.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_masters_for_slave", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneName.IsNull() && !plan.ZoneName.IsUnknown() && !state.ZoneName.IsNull() && !state.ZoneName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneName.ValueString(), state.ZoneName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneRefresh.IsNull() && !plan.ZoneRefresh.IsUnknown() && !state.ZoneRefresh.IsNull() && !state.ZoneRefresh.IsUnknown() {
+ if plan.ZoneRefresh.ValueInt64() != state.ZoneRefresh.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить zone_refresh", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneRetry.IsNull() && !plan.ZoneRetry.IsUnknown() && !state.ZoneRetry.IsNull() && !state.ZoneRetry.IsUnknown() {
+ if plan.ZoneRetry.ValueInt64() != state.ZoneRetry.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить zone_retry", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneTTL.IsNull() && !plan.ZoneTTL.IsUnknown() && !state.ZoneTTL.IsNull() && !state.ZoneTTL.IsUnknown() {
+ if plan.ZoneTTL.ValueInt64() != state.ZoneTTL.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить zone_t_t_l", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.Records.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "records is required.")
+ return
+ }
+ if config.Records.IsUnknown() {
+ return
+ }
+ if config.ZoneName.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "zone_name is required.")
+ return
+ }
+ if config.ZoneName.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 761: resources_core.FormatString(config.ZoneName),
+ 762: resources_core.FormatString(config.ZoneKind),
+ 763: resources_core.FormatString(config.ZoneMastersForSlave),
+ 764: resources_core.FormatInt64(config.ZoneRefresh),
+ 765: resources_core.FormatInt64(config.ZoneRetry),
+ 766: resources_core.FormatInt64(config.ZoneExpire),
+ 767: resources_core.FormatInt64(config.ZoneTTL),
+ 768: resources_core.FormatString(config.ResourceRealm),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 109, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *ZonesV2Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data ZonesV2Model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.Records.IsNull() || data.Records.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "records is required.")
+ return
+ }
+ if data.ZoneName.IsNull() || data.ZoneName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "zone_name is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 109, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 761: resources_core.FormatString(data.ZoneName),
+ 762: resources_core.FormatString(data.ZoneKind),
+ 763: resources_core.FormatString(data.ZoneMastersForSlave),
+ 764: resources_core.FormatInt64(data.ZoneRefresh),
+ 765: resources_core.FormatInt64(data.ZoneRetry),
+ 766: resources_core.FormatInt64(data.ZoneExpire),
+ 767: resources_core.FormatInt64(data.ZoneTTL),
+ 768: resources_core.FormatString(data.ResourceRealm),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 109, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 109, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "records", Field: "Records", Type: "array-map-fixed"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "zoneExpire", Field: "ZoneExpire", Type: "int64"},
+ {Code: "zoneKind", Field: "ZoneKind", Type: "string"},
+ {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"},
+ {Code: "zoneName", Field: "ZoneName", Type: "string"},
+ {Code: "zoneRefresh", Field: "ZoneRefresh", Type: "int64"},
+ {Code: "zoneRetry", Field: "ZoneRetry", Type: "int64"},
+ {Code: "zoneTTL", Field: "ZoneTTL", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *ZonesV2Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state ZonesV2Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 109, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "records", Field: "Records", Type: "array-map-fixed"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "zoneExpire", Field: "ZoneExpire", Type: "int64"},
+ {Code: "zoneKind", Field: "ZoneKind", Type: "string"},
+ {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"},
+ {Code: "zoneName", Field: "ZoneName", Type: "string"},
+ {Code: "zoneRefresh", Field: "ZoneRefresh", Type: "int64"},
+ {Code: "zoneRetry", Field: "ZoneRetry", Type: "int64"},
+ {Code: "zoneTTL", Field: "ZoneTTL", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *ZonesV2Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan ZonesV2Model
+ var state ZonesV2Model
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.Records.IsNull() != state.Records.IsNull() || plan.Records.IsUnknown() != state.Records.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.Records.IsNull() && !plan.Records.IsUnknown() && !state.Records.IsNull() && !state.Records.IsUnknown() {
+ if !strings.EqualFold(plan.Records.ValueString(), state.Records.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 769: resources_core.FormatString(plan.Records),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 109, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "records", Field: "Records", Type: "array-map-fixed"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "zoneExpire", Field: "ZoneExpire", Type: "int64"},
+ {Code: "zoneKind", Field: "ZoneKind", Type: "string"},
+ {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"},
+ {Code: "zoneName", Field: "ZoneName", Type: "string"},
+ {Code: "zoneRefresh", Field: "ZoneRefresh", Type: "int64"},
+ {Code: "zoneRetry", Field: "ZoneRetry", Type: "int64"},
+ {Code: "zoneTTL", Field: "ZoneTTL", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *ZonesV2Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state ZonesV2Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *ZonesV2Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *ZonesV2Resource) 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
+}
diff --git a/generated/test/go/110_dnszone_resource.go b/generated/test/go/110_dnszone_resource.go
new file mode 100644
index 0000000..b6842a5
--- /dev/null
+++ b/generated/test/go/110_dnszone_resource.go
@@ -0,0 +1,495 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: dnszone
+// Service ID: 110
+
+var _ resource.Resource = &DnszoneResource{}
+var _ resource.ResourceWithModifyPlan = &DnszoneResource{}
+var _ resource.ResourceWithImportState = &DnszoneResource{}
+
+type DnszoneResource struct {
+ client *core.UniversalClient
+}
+
+type DnszoneModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ ZoneEmail types.String `tfsdk:"zone_email"`
+ ZoneKind types.String `tfsdk:"zone_kind"`
+ ZoneMastersForSlave types.String `tfsdk:"zone_masters_for_slave"`
+ ZoneName types.String `tfsdk:"zone_name"`
+ ZoneTimeToExpire types.String `tfsdk:"zone_time_to_expire"`
+ ZoneTimeToLive types.String `tfsdk:"zone_time_to_live"`
+ ZoneTimeToRefresh types.String `tfsdk:"zone_time_to_refresh"`
+ ZoneTimeToRetry types.String `tfsdk:"zone_time_to_retry"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewDnszoneResource() resource.Resource {
+ return &DnszoneResource{}
+}
+
+func (r *DnszoneResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_dnszone"
+}
+
+func (r *DnszoneResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("powerdns"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ "zone_email": schema.StringAttribute{Required: true, MarkdownDescription: "Контактный e-mail Не изменяется после создания."},
+ "zone_kind": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("NATIVE"), MarkdownDescription: "Тип зоны Не изменяется после создания."},
+ "zone_masters_for_slave": schema.StringAttribute{Optional: true, MarkdownDescription: "Использовать если выбран тип SLAVE. IP адреса в формате через пробел для настройки Master-серверов Не изменяется после создания."},
+ "zone_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя зоны Не изменяется после создания."},
+ "zone_time_to_expire": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("3600000"), MarkdownDescription: "Время в течение которого вторичный DNS будет пытаться завершить синхронизацию зоны с первичным Не изменяется после создания."},
+ "zone_time_to_live": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("86400"), MarkdownDescription: "Время, в течение которого информация будет кешироваться другими DNS-серверами Не изменяется после создания."},
+ "zone_time_to_refresh": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("86400"), MarkdownDescription: "Время ожидания ответа вторичного DNS перед запросом SOA-записи с первичных серверов Не изменяется после создания."},
+ "zone_time_to_retry": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("7200"), MarkdownDescription: "Время в течение которого сервер повторно попробует синхронизировать информацию с первичных DNS-серверов Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *DnszoneResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *DnszoneModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *DnszoneModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan DnszoneModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneEmail.IsNull() && !plan.ZoneEmail.IsUnknown() && !state.ZoneEmail.IsNull() && !state.ZoneEmail.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneEmail.ValueString(), state.ZoneEmail.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_email", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneKind.IsNull() && !plan.ZoneKind.IsUnknown() && !state.ZoneKind.IsNull() && !state.ZoneKind.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneKind.ValueString(), state.ZoneKind.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_kind", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneMastersForSlave.IsNull() && !plan.ZoneMastersForSlave.IsUnknown() && !state.ZoneMastersForSlave.IsNull() && !state.ZoneMastersForSlave.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneMastersForSlave.ValueString(), state.ZoneMastersForSlave.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_masters_for_slave", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneName.IsNull() && !plan.ZoneName.IsUnknown() && !state.ZoneName.IsNull() && !state.ZoneName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneName.ValueString(), state.ZoneName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneTimeToExpire.IsNull() && !plan.ZoneTimeToExpire.IsUnknown() && !state.ZoneTimeToExpire.IsNull() && !state.ZoneTimeToExpire.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneTimeToExpire.ValueString(), state.ZoneTimeToExpire.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_time_to_expire", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneTimeToLive.IsNull() && !plan.ZoneTimeToLive.IsUnknown() && !state.ZoneTimeToLive.IsNull() && !state.ZoneTimeToLive.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneTimeToLive.ValueString(), state.ZoneTimeToLive.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_time_to_live", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneTimeToRefresh.IsNull() && !plan.ZoneTimeToRefresh.IsUnknown() && !state.ZoneTimeToRefresh.IsNull() && !state.ZoneTimeToRefresh.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneTimeToRefresh.ValueString(), state.ZoneTimeToRefresh.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_time_to_refresh", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneTimeToRetry.IsNull() && !plan.ZoneTimeToRetry.IsUnknown() && !state.ZoneTimeToRetry.IsNull() && !state.ZoneTimeToRetry.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneTimeToRetry.ValueString(), state.ZoneTimeToRetry.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_time_to_retry", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.ZoneEmail.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "zone_email is required.")
+ return
+ }
+ if config.ZoneEmail.IsUnknown() {
+ return
+ }
+ if config.ZoneName.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "zone_name is required.")
+ return
+ }
+ if config.ZoneName.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 66: resources_core.FormatString(config.ZoneEmail),
+ 67: resources_core.FormatString(config.ZoneName),
+ 68: resources_core.FormatString(config.ZoneKind),
+ 69: resources_core.FormatString(config.ZoneMastersForSlave),
+ 70: resources_core.FormatString(config.ZoneTimeToRefresh),
+ 71: resources_core.FormatString(config.ZoneTimeToRetry),
+ 72: resources_core.FormatString(config.ZoneTimeToExpire),
+ 73: resources_core.FormatString(config.ZoneTimeToLive),
+ 284: resources_core.FormatString(config.ResourceRealm),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 110, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *DnszoneResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data DnszoneModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.ZoneEmail.IsNull() || data.ZoneEmail.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "zone_email is required.")
+ return
+ }
+ if data.ZoneName.IsNull() || data.ZoneName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "zone_name is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 110, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 66: resources_core.FormatString(data.ZoneEmail),
+ 67: resources_core.FormatString(data.ZoneName),
+ 68: resources_core.FormatString(data.ZoneKind),
+ 69: resources_core.FormatString(data.ZoneMastersForSlave),
+ 70: resources_core.FormatString(data.ZoneTimeToRefresh),
+ 71: resources_core.FormatString(data.ZoneTimeToRetry),
+ 72: resources_core.FormatString(data.ZoneTimeToExpire),
+ 73: resources_core.FormatString(data.ZoneTimeToLive),
+ 284: resources_core.FormatString(data.ResourceRealm),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 110, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 110, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "zoneEmail", Field: "ZoneEmail", Type: "string"},
+ {Code: "zoneKind", Field: "ZoneKind", Type: "string"},
+ {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"},
+ {Code: "zoneName", Field: "ZoneName", Type: "string"},
+ {Code: "zoneTimeToExpire", Field: "ZoneTimeToExpire", Type: "string"},
+ {Code: "zoneTimeToLive", Field: "ZoneTimeToLive", Type: "string"},
+ {Code: "zoneTimeToRefresh", Field: "ZoneTimeToRefresh", Type: "string"},
+ {Code: "zoneTimeToRetry", Field: "ZoneTimeToRetry", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *DnszoneResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state DnszoneModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 110, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "zoneEmail", Field: "ZoneEmail", Type: "string"},
+ {Code: "zoneKind", Field: "ZoneKind", Type: "string"},
+ {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"},
+ {Code: "zoneName", Field: "ZoneName", Type: "string"},
+ {Code: "zoneTimeToExpire", Field: "ZoneTimeToExpire", Type: "string"},
+ {Code: "zoneTimeToLive", Field: "ZoneTimeToLive", Type: "string"},
+ {Code: "zoneTimeToRefresh", Field: "ZoneTimeToRefresh", Type: "string"},
+ {Code: "zoneTimeToRetry", Field: "ZoneTimeToRetry", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *DnszoneResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan DnszoneModel
+ var state DnszoneModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 110, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "zoneEmail", Field: "ZoneEmail", Type: "string"},
+ {Code: "zoneKind", Field: "ZoneKind", Type: "string"},
+ {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"},
+ {Code: "zoneName", Field: "ZoneName", Type: "string"},
+ {Code: "zoneTimeToExpire", Field: "ZoneTimeToExpire", Type: "string"},
+ {Code: "zoneTimeToLive", Field: "ZoneTimeToLive", Type: "string"},
+ {Code: "zoneTimeToRefresh", Field: "ZoneTimeToRefresh", Type: "string"},
+ {Code: "zoneTimeToRetry", Field: "ZoneTimeToRetry", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *DnszoneResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state DnszoneModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *DnszoneResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *DnszoneResource) 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
+}
diff --git a/generated/test/go/111_dnsrecord_resource.go b/generated/test/go/111_dnsrecord_resource.go
new file mode 100644
index 0000000..4b988a5
--- /dev/null
+++ b/generated/test/go/111_dnsrecord_resource.go
@@ -0,0 +1,546 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: dnsrecord
+// Service ID: 111
+
+var _ resource.Resource = &DnsrecordResource{}
+var _ resource.ResourceWithModifyPlan = &DnsrecordResource{}
+var _ resource.ResourceWithImportState = &DnsrecordResource{}
+
+type DnsrecordResource struct {
+ client *core.UniversalClient
+}
+
+type DnsrecordModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ RecordInput types.String `tfsdk:"record_input"`
+ RecordName types.String `tfsdk:"record_name"`
+ RecordTTL types.Int64 `tfsdk:"record_t_t_l"`
+ RecordType types.String `tfsdk:"record_type"`
+ ZoneUid types.String `tfsdk:"zone_uid"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewDnsrecordResource() resource.Resource {
+ return &DnsrecordResource{}
+}
+
+func (r *DnsrecordResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_dnsrecord"
+}
+
+func (r *DnsrecordResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "record_input": schema.StringAttribute{Required: true, MarkdownDescription: "Значение. Необходима валидация, в зависимости от типа записи. Должно быть заменено на UUID-внешнийIP"},
+ "record_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя записи. Имя зоны в конце проставляется автоматически Не изменяется после создания."},
+ "record_t_t_l": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(3600), MarkdownDescription: "Time-to-Leave записи. Раз в какое время запись будет обновлена Не изменяется после создания."},
+ "record_type": schema.StringAttribute{Required: true, MarkdownDescription: "Тип DNS-записи Не изменяется после создания."},
+ "zone_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID экземпляра услуги DNS-зоны Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *DnsrecordResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *DnsrecordModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *DnsrecordModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan DnsrecordModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.RecordName.IsNull() && !plan.RecordName.IsUnknown() && !state.RecordName.IsNull() && !state.RecordName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.RecordName.ValueString(), state.RecordName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить record_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.RecordTTL.IsNull() && !plan.RecordTTL.IsUnknown() && !state.RecordTTL.IsNull() && !state.RecordTTL.IsUnknown() {
+ if plan.RecordTTL.ValueInt64() != state.RecordTTL.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить record_t_t_l", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.RecordType.IsNull() && !plan.RecordType.IsUnknown() && !state.RecordType.IsNull() && !state.RecordType.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.RecordType.ValueString(), state.RecordType.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить record_type", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ZoneUid.IsNull() && !plan.ZoneUid.IsUnknown() && !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ZoneUid.ValueString(), state.ZoneUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить zone_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.RecordInput.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "record_input is required.")
+ return
+ }
+ if config.RecordInput.IsUnknown() {
+ return
+ }
+ if config.RecordName.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "record_name is required.")
+ return
+ }
+ if config.RecordName.IsUnknown() {
+ return
+ }
+ if config.RecordType.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "record_type is required.")
+ return
+ }
+ if config.RecordType.IsUnknown() {
+ return
+ }
+ if config.ZoneUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "zone_uid is required.")
+ return
+ }
+ if config.ZoneUid.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.ZoneUid.IsNull() && !config.ZoneUid.IsUnknown() {
+ resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 109, config.ZoneUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve zone_uid", err.Error())
+ } else if resolvedZoneUid != "" && resolvedZoneUid != config.ZoneUid.ValueString() {
+ config.ZoneUid = types.StringValue(resolvedZoneUid)
+ }
+ }
+ params := map[int]string{
+ 75: resources_core.FormatString(config.ZoneUid),
+ 76: resources_core.FormatString(config.RecordType),
+ 77: resources_core.FormatString(config.RecordName),
+ 78: resources_core.FormatString(config.RecordInput),
+ 450: resources_core.FormatInt64(config.RecordTTL),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 111, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *DnsrecordResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data DnsrecordModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.RecordInput.IsNull() || data.RecordInput.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "record_input is required.")
+ return
+ }
+ if data.RecordName.IsNull() || data.RecordName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "record_name is required.")
+ return
+ }
+ if data.RecordType.IsNull() || data.RecordType.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "record_type is required.")
+ return
+ }
+ if data.ZoneUid.IsNull() || data.ZoneUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "zone_uid is required.")
+ return
+ }
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalZoneUid := data.ZoneUid
+ if !data.ZoneUid.IsNull() && !data.ZoneUid.IsUnknown() {
+ resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 109, data.ZoneUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedZoneUid != "" && resolvedZoneUid != data.ZoneUid.ValueString() {
+ data.ZoneUid = types.StringValue(resolvedZoneUid)
+ }
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 111, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 75: resources_core.FormatString(data.ZoneUid),
+ 76: resources_core.FormatString(data.RecordType),
+ 77: resources_core.FormatString(data.RecordName),
+ 78: resources_core.FormatString(data.RecordInput),
+ 450: resources_core.FormatInt64(data.RecordTTL),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 111, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 111, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "recordInput", Field: "RecordInput", Type: "string"},
+ {Code: "recordName", Field: "RecordName", Type: "string"},
+ {Code: "recordTTL", Field: "RecordTTL", Type: "int64"},
+ {Code: "recordType", Field: "RecordType", Type: "string"},
+ {Code: "zoneUid", Field: "ZoneUid", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() {
+ resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 109, state.ZoneUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve zone_uid", err.Error())
+ } else if resolvedZoneUid != "" && resolvedZoneUid != state.ZoneUid.ValueString() {
+ state.ZoneUid = types.StringValue(resolvedZoneUid)
+ }
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+ if !originalZoneUid.IsNull() && !originalZoneUid.IsUnknown() && !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() {
+ if strings.EqualFold(state.ZoneUid.ValueString(), originalZoneUid.ValueString()) {
+ state.ZoneUid = originalZoneUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *DnsrecordResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state DnsrecordModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 111, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "recordInput", Field: "RecordInput", Type: "string"},
+ {Code: "recordName", Field: "RecordName", Type: "string"},
+ {Code: "recordTTL", Field: "RecordTTL", Type: "int64"},
+ {Code: "recordType", Field: "RecordType", Type: "string"},
+ {Code: "zoneUid", Field: "ZoneUid", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !newState.ZoneUid.IsNull() && !newState.ZoneUid.IsUnknown() {
+ resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 109, newState.ZoneUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve zone_uid", err.Error())
+ } else if resolvedZoneUid != "" && resolvedZoneUid != newState.ZoneUid.ValueString() {
+ newState.ZoneUid = types.StringValue(resolvedZoneUid)
+ }
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+ if !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() && !newState.ZoneUid.IsNull() && !newState.ZoneUid.IsUnknown() {
+ if strings.EqualFold(newState.ZoneUid.ValueString(), state.ZoneUid.ValueString()) {
+ newState.ZoneUid = state.ZoneUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *DnsrecordResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan DnsrecordModel
+ var state DnsrecordModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.RecordInput.IsNull() != state.RecordInput.IsNull() || plan.RecordInput.IsUnknown() != state.RecordInput.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.RecordInput.IsNull() && !plan.RecordInput.IsUnknown() && !state.RecordInput.IsNull() && !state.RecordInput.IsUnknown() {
+ if !strings.EqualFold(plan.RecordInput.ValueString(), state.RecordInput.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 256: resources_core.FormatString(plan.RecordInput),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 111, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "recordInput", Field: "RecordInput", Type: "string"},
+ {Code: "recordName", Field: "RecordName", Type: "string"},
+ {Code: "recordTTL", Field: "RecordTTL", Type: "int64"},
+ {Code: "recordType", Field: "RecordType", Type: "string"},
+ {Code: "zoneUid", Field: "ZoneUid", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() {
+ resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 109, state.ZoneUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve zone_uid", err.Error())
+ } else if resolvedZoneUid != "" && resolvedZoneUid != state.ZoneUid.ValueString() {
+ state.ZoneUid = types.StringValue(resolvedZoneUid)
+ }
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+ if !plan.ZoneUid.IsNull() && !plan.ZoneUid.IsUnknown() && !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() {
+ if strings.EqualFold(state.ZoneUid.ValueString(), plan.ZoneUid.ValueString()) {
+ state.ZoneUid = plan.ZoneUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *DnsrecordResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state DnsrecordModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *DnsrecordResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *DnsrecordResource) 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
+}
diff --git a/generated/test/go/112_tenant_resource.go b/generated/test/go/112_tenant_resource.go
new file mode 100644
index 0000000..dff6bb7
--- /dev/null
+++ b/generated/test/go/112_tenant_resource.go
@@ -0,0 +1,372 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: tenant
+// Service ID: 112
+
+var _ resource.Resource = &TenantResource{}
+var _ resource.ResourceWithModifyPlan = &TenantResource{}
+var _ resource.ResourceWithImportState = &TenantResource{}
+
+type TenantResource struct {
+ client *core.UniversalClient
+}
+
+type TenantModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ GrafanaOrgName types.String `tfsdk:"grafana_org_name"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewTenantResource() resource.Resource {
+ return &TenantResource{}
+}
+
+func (r *TenantResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_tenant"
+}
+
+func (r *TenantResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "grafana_org_name": schema.StringAttribute{Required: true, MarkdownDescription: "WZ-Код клиента Не изменяется после создания."},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("grafana.ngcloud.ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *TenantResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *TenantModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *TenantModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan TenantModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.GrafanaOrgName.IsNull() && !plan.GrafanaOrgName.IsUnknown() && !state.GrafanaOrgName.IsNull() && !state.GrafanaOrgName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.GrafanaOrgName.ValueString(), state.GrafanaOrgName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить grafana_org_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.GrafanaOrgName.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "grafana_org_name is required.")
+ return
+ }
+ if config.GrafanaOrgName.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 239: resources_core.FormatString(config.GrafanaOrgName),
+ 240: resources_core.FormatString(config.ResourceRealm),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 112, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *TenantResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data TenantModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.GrafanaOrgName.IsNull() || data.GrafanaOrgName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "grafana_org_name is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 112, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 239: resources_core.FormatString(data.GrafanaOrgName),
+ 240: resources_core.FormatString(data.ResourceRealm),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 112, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 112, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "grafanaOrgName", Field: "GrafanaOrgName", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *TenantResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state TenantModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 112, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "grafanaOrgName", Field: "GrafanaOrgName", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *TenantResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan TenantModel
+ var state TenantModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 112, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "grafanaOrgName", Field: "GrafanaOrgName", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *TenantResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state TenantModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *TenantResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *TenantResource) 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
+}
diff --git a/generated/test/go/113_vc_complex_resource.go b/generated/test/go/113_vc_complex_resource.go
new file mode 100644
index 0000000..8f06254
--- /dev/null
+++ b/generated/test/go/113_vc_complex_resource.go
@@ -0,0 +1,563 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_complex
+// Service ID: 113
+
+var _ resource.Resource = &VcComplexResource{}
+var _ resource.ResourceWithModifyPlan = &VcComplexResource{}
+var _ resource.ResourceWithImportState = &VcComplexResource{}
+
+type VcComplexResource struct {
+ client *core.UniversalClient
+}
+
+type VcComplexModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ CpuAllocated types.String `tfsdk:"cpu_allocated"`
+ CpuGuaranteed types.Int64 `tfsdk:"cpu_guaranteed"`
+ IpSpaceName types.String `tfsdk:"ip_space_name"`
+ MemAllocated types.Int64 `tfsdk:"mem_allocated"`
+ MemGuaranteed types.Int64 `tfsdk:"mem_guaranteed"`
+ NeedEnableAVI types.Bool `tfsdk:"need_enable_a_v_i"`
+ NeedExternalAddressSNAT types.Bool `tfsdk:"need_external_address_s_n_a_t"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SegroupName types.String `tfsdk:"segroup_name"`
+ StorageConfig types.String `tfsdk:"storage_config"`
+ VIPCount types.Int64 `tfsdk:"v_i_p_count"`
+ VappName types.String `tfsdk:"vapp_name"`
+ VdcNetworkPool types.String `tfsdk:"vdc_network_pool"`
+ VdcProviderGateway types.String `tfsdk:"vdc_provider_gateway"`
+ VirtualServicesCount types.String `tfsdk:"virtual_services_count"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVcComplexResource() resource.Resource {
+ return &VcComplexResource{}
+}
+
+func (r *VcComplexResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_complex"
+}
+
+func (r *VcComplexResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "cpu_allocated": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("1"), MarkdownDescription: "шт Не изменяется после создания."},
+ "cpu_guaranteed": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(20), MarkdownDescription: "Не изменяется после создания."},
+ "ip_space_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("internet-ipv4-v1"), MarkdownDescription: "Имя ip Space для внешнего IP. Целесообразнее брать самый дешёвый, так как это vip для исходящего трафика. Необходимо задавать если включен needExternalAddressSNAT Не изменяется после создания."},
+ "mem_allocated": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "gb Не изменяется после создания."},
+ "mem_guaranteed": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(20), MarkdownDescription: "Не изменяется после создания."},
+ "need_enable_a_v_i": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает AVI в Cloud Director Не изменяется после создания."},
+ "need_external_address_s_n_a_t": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Выделяет внешний IP на Edge. Используется для SNAT всего трафика Не изменяется после создания."},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("sandbox.nubes.ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ "segroup_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("SEGROUP-SANDBOX-CL1-SHARED-01"), MarkdownDescription: "Наименование segroup, в котором будут выделяться пулы на AVI Не изменяется после создания."},
+ "storage_config": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{\"name\": \"\", \"size\": 1024}]"), MarkdownDescription: "Описывается как массив объектов, где описывается имя политики и её размер Не изменяется после создания.", PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()}},
+ "v_i_p_count": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "ш Не изменяется после создания."},
+ "vapp_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя vApp Не изменяется после создания."},
+ "vdc_network_pool": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("nsxt-sandbox-geneve-np"), MarkdownDescription: "Не изменяется после создания."},
+ "vdc_provider_gateway": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("sandbox-v1cl1-pvdc"), MarkdownDescription: "Взять из свойств ресурсной платформы.
Путь .vcd.providerVdc - взять KEY
providerGateway Cloud Director. Влияет ресурсы будут предоставлены. Является полем (List<String>) в свойствах рес. платформы ЛК, но сам параметр передаётся только как один String Не изменяется после создания."},
+ "virtual_services_count": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("1"), MarkdownDescription: "Кол-во виртуальных сервисов, которые выделяются на AVI. Во избежании коллапса пока выделяется до 4 Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcComplexResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VcComplexModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VcComplexModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VcComplexModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.CpuAllocated.IsNull() && !plan.CpuAllocated.IsUnknown() && !state.CpuAllocated.IsNull() && !state.CpuAllocated.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.CpuAllocated.ValueString(), state.CpuAllocated.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить cpu_allocated", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.CpuGuaranteed.IsNull() && !plan.CpuGuaranteed.IsUnknown() && !state.CpuGuaranteed.IsNull() && !state.CpuGuaranteed.IsUnknown() {
+ if plan.CpuGuaranteed.ValueInt64() != state.CpuGuaranteed.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить cpu_guaranteed", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.IpSpaceName.IsNull() && !plan.IpSpaceName.IsUnknown() && !state.IpSpaceName.IsNull() && !state.IpSpaceName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.IpSpaceName.ValueString(), state.IpSpaceName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить ip_space_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.MemAllocated.IsNull() && !plan.MemAllocated.IsUnknown() && !state.MemAllocated.IsNull() && !state.MemAllocated.IsUnknown() {
+ if plan.MemAllocated.ValueInt64() != state.MemAllocated.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить mem_allocated", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.MemGuaranteed.IsNull() && !plan.MemGuaranteed.IsUnknown() && !state.MemGuaranteed.IsNull() && !state.MemGuaranteed.IsUnknown() {
+ if plan.MemGuaranteed.ValueInt64() != state.MemGuaranteed.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить mem_guaranteed", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.NeedEnableAVI.IsNull() && !plan.NeedEnableAVI.IsUnknown() && !state.NeedEnableAVI.IsNull() && !state.NeedEnableAVI.IsUnknown() {
+ if plan.NeedEnableAVI.ValueBool() != state.NeedEnableAVI.ValueBool() {
+ resp.Diagnostics.AddError("Нельзя изменить need_enable_a_v_i", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.NeedExternalAddressSNAT.IsNull() && !plan.NeedExternalAddressSNAT.IsUnknown() && !state.NeedExternalAddressSNAT.IsNull() && !state.NeedExternalAddressSNAT.IsUnknown() {
+ if plan.NeedExternalAddressSNAT.ValueBool() != state.NeedExternalAddressSNAT.ValueBool() {
+ resp.Diagnostics.AddError("Нельзя изменить need_external_address_s_n_a_t", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.SegroupName.IsNull() && !plan.SegroupName.IsUnknown() && !state.SegroupName.IsNull() && !state.SegroupName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.SegroupName.ValueString(), state.SegroupName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить segroup_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.StorageConfig.IsNull() && !plan.StorageConfig.IsUnknown() && !state.StorageConfig.IsNull() && !state.StorageConfig.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.StorageConfig.ValueString(), state.StorageConfig.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить storage_config", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VIPCount.IsNull() && !plan.VIPCount.IsUnknown() && !state.VIPCount.IsNull() && !state.VIPCount.IsUnknown() {
+ if plan.VIPCount.ValueInt64() != state.VIPCount.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить v_i_p_count", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VappName.IsNull() && !plan.VappName.IsUnknown() && !state.VappName.IsNull() && !state.VappName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VappName.ValueString(), state.VappName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vapp_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VdcNetworkPool.IsNull() && !plan.VdcNetworkPool.IsUnknown() && !state.VdcNetworkPool.IsNull() && !state.VdcNetworkPool.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VdcNetworkPool.ValueString(), state.VdcNetworkPool.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vdc_network_pool", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VdcProviderGateway.IsNull() && !plan.VdcProviderGateway.IsUnknown() && !state.VdcProviderGateway.IsNull() && !state.VdcProviderGateway.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VdcProviderGateway.ValueString(), state.VdcProviderGateway.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vdc_provider_gateway", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VirtualServicesCount.IsNull() && !plan.VirtualServicesCount.IsUnknown() && !state.VirtualServicesCount.IsNull() && !state.VirtualServicesCount.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VirtualServicesCount.ValueString(), state.VirtualServicesCount.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить virtual_services_count", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.VappName.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "vapp_name is required.")
+ return
+ }
+ if config.VappName.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 332: resources_core.FormatString(config.ResourceRealm),
+ 334: resources_core.FormatString(config.VappName),
+ 358: resources_core.FormatString(config.VdcProviderGateway),
+ 359: resources_core.FormatString(config.VirtualServicesCount),
+ 360: resources_core.FormatString(config.IpSpaceName),
+ 399: resources_core.FormatString(config.StorageConfig),
+ 400: resources_core.FormatString(config.VdcNetworkPool),
+ 401: resources_core.FormatInt64(config.CpuGuaranteed),
+ 402: resources_core.FormatInt64(config.MemGuaranteed),
+ 403: resources_core.FormatString(config.SegroupName),
+ 404: resources_core.FormatBool(config.NeedExternalAddressSNAT),
+ 405: resources_core.FormatBool(config.NeedEnableAVI),
+ 566: resources_core.FormatString(config.CpuAllocated),
+ 567: resources_core.FormatInt64(config.MemAllocated),
+ 568: resources_core.FormatInt64(config.VIPCount),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 113, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VcComplexResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VcComplexModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.VappName.IsNull() || data.VappName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "vapp_name is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 113, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 332: resources_core.FormatString(data.ResourceRealm),
+ 334: resources_core.FormatString(data.VappName),
+ 358: resources_core.FormatString(data.VdcProviderGateway),
+ 359: resources_core.FormatString(data.VirtualServicesCount),
+ 360: resources_core.FormatString(data.IpSpaceName),
+ 399: resources_core.FormatString(data.StorageConfig),
+ 400: resources_core.FormatString(data.VdcNetworkPool),
+ 401: resources_core.FormatInt64(data.CpuGuaranteed),
+ 402: resources_core.FormatInt64(data.MemGuaranteed),
+ 403: resources_core.FormatString(data.SegroupName),
+ 404: resources_core.FormatBool(data.NeedExternalAddressSNAT),
+ 405: resources_core.FormatBool(data.NeedEnableAVI),
+ 566: resources_core.FormatString(data.CpuAllocated),
+ 567: resources_core.FormatInt64(data.MemAllocated),
+ 568: resources_core.FormatInt64(data.VIPCount),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 113, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 113, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "cpuAllocated", Field: "CpuAllocated", Type: "string"},
+ {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "memAllocated", Field: "MemAllocated", Type: "int64"},
+ {Code: "memGuaranteed", Field: "MemGuaranteed", Type: "int64"},
+ {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"},
+ {Code: "needExternalAddressSNAT", Field: "NeedExternalAddressSNAT", Type: "bool"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "segroupName", Field: "SegroupName", Type: "string"},
+ {Code: "storageConfig", Field: "StorageConfig", Type: "string"},
+ {Code: "vIPCount", Field: "VIPCount", Type: "int64"},
+ {Code: "vappName", Field: "VappName", Type: "string"},
+ {Code: "vdcNetworkPool", Field: "VdcNetworkPool", Type: "string"},
+ {Code: "vdcProviderGateway", Field: "VdcProviderGateway", Type: "string"},
+ {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcComplexResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcComplexModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 113, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "cpuAllocated", Field: "CpuAllocated", Type: "string"},
+ {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "memAllocated", Field: "MemAllocated", Type: "int64"},
+ {Code: "memGuaranteed", Field: "MemGuaranteed", Type: "int64"},
+ {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"},
+ {Code: "needExternalAddressSNAT", Field: "NeedExternalAddressSNAT", Type: "bool"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "segroupName", Field: "SegroupName", Type: "string"},
+ {Code: "storageConfig", Field: "StorageConfig", Type: "string"},
+ {Code: "vIPCount", Field: "VIPCount", Type: "int64"},
+ {Code: "vappName", Field: "VappName", Type: "string"},
+ {Code: "vdcNetworkPool", Field: "VdcNetworkPool", Type: "string"},
+ {Code: "vdcProviderGateway", Field: "VdcProviderGateway", Type: "string"},
+ {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VcComplexResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcComplexModel
+ var state VcComplexModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 113, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "cpuAllocated", Field: "CpuAllocated", Type: "string"},
+ {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "memAllocated", Field: "MemAllocated", Type: "int64"},
+ {Code: "memGuaranteed", Field: "MemGuaranteed", Type: "int64"},
+ {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"},
+ {Code: "needExternalAddressSNAT", Field: "NeedExternalAddressSNAT", Type: "bool"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "segroupName", Field: "SegroupName", Type: "string"},
+ {Code: "storageConfig", Field: "StorageConfig", Type: "string"},
+ {Code: "vIPCount", Field: "VIPCount", Type: "int64"},
+ {Code: "vappName", Field: "VappName", Type: "string"},
+ {Code: "vdcNetworkPool", Field: "VdcNetworkPool", Type: "string"},
+ {Code: "vdcProviderGateway", Field: "VdcProviderGateway", Type: "string"},
+ {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcComplexResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcComplexModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VcComplexResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VcComplexResource) 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
+}
diff --git a/generated/test/go/114_gitea_complex_resource.go b/generated/test/go/114_gitea_complex_resource.go
new file mode 100644
index 0000000..6fb5f15
--- /dev/null
+++ b/generated/test/go/114_gitea_complex_resource.go
@@ -0,0 +1,322 @@
+package resources_gen
+
+import (
+ "context"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: gitea_complex
+// Service ID: 114
+
+var _ resource.Resource = &GiteaComplexResource{}
+var _ resource.ResourceWithModifyPlan = &GiteaComplexResource{}
+var _ resource.ResourceWithImportState = &GiteaComplexResource{}
+
+type GiteaComplexResource struct {
+ client *core.UniversalClient
+}
+
+type GiteaComplexModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewGiteaComplexResource() resource.Resource {
+ return &GiteaComplexResource{}
+}
+
+func (r *GiteaComplexResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_gitea_complex"
+}
+
+func (r *GiteaComplexResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *GiteaComplexResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *GiteaComplexModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *GiteaComplexModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan GiteaComplexModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{}
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 114, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *GiteaComplexResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data GiteaComplexModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 114, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{}
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 114, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 114, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{})
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *GiteaComplexResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state GiteaComplexModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 114, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{})
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *GiteaComplexResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan GiteaComplexModel
+ var state GiteaComplexModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 114, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{})
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *GiteaComplexResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state GiteaComplexModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *GiteaComplexResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *GiteaComplexResource) 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
+}
diff --git a/generated/test/go/115_mariadb_database_resource.go b/generated/test/go/115_mariadb_database_resource.go
new file mode 100644
index 0000000..ecce971
--- /dev/null
+++ b/generated/test/go/115_mariadb_database_resource.go
@@ -0,0 +1,343 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: mariadb
+// Subresource: database
+
+var _ resource.Resource = &MariadbDatabaseResource{}
+
+// MariadbDatabaseResource manages a subresource via operations.
+type MariadbDatabaseResource struct {
+ client *core.UniversalClient
+}
+
+type MariadbDatabaseModel struct {
+ ID types.String `tfsdk:"id"`
+ MariadbID types.String `tfsdk:"mariadb_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"`
+ DbName types.String `tfsdk:"db_name"`
+}
+
+func NewMariadbDatabaseResource() resource.Resource {
+ return &MariadbDatabaseResource{}
+}
+
+func (r *MariadbDatabaseResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_mariadb_database"
+}
+
+func (r *MariadbDatabaseResource) 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()}},
+ "mariadb_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."},
+ "db_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("db-bdwhrjlz"), MarkdownDescription: "Пример: `db01` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *MariadbDatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan MariadbDatabaseModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.MariadbID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "dbName": resources_core.FormatString(plan.DbName),
+ }
+ identityCodes := []string{
+ "dbName",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", 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, "database", 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{
+ "dbName": resources_core.FormatString(plan.DbName),
+ })
+
+ 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, "create_database", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_database", 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, "database", 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, "database", 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, "database", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *MariadbDatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state MariadbDatabaseModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *MariadbDatabaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan MariadbDatabaseModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state MariadbDatabaseModel
+ 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.MariadbID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "dbName": resources_core.FormatString(plan.DbName),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *MariadbDatabaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state MariadbDatabaseModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_database" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.MariadbID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "dbName": resources_core.FormatString(state.DbName),
+ }
+ identityCodes := []string{
+ "dbName",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", 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{
+ "dbName": resources_core.FormatString(state.DbName),
+ })
+
+ 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, "delete_database", 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 *MariadbDatabaseResource) 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
+}
diff --git a/generated/test/go/115_mariadb_resource.go b/generated/test/go/115_mariadb_resource.go
new file mode 100644
index 0000000..16798a3
--- /dev/null
+++ b/generated/test/go/115_mariadb_resource.go
@@ -0,0 +1,447 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: mariadb
+// Service ID: 115
+
+var _ resource.Resource = &MariadbResource{}
+var _ resource.ResourceWithModifyPlan = &MariadbResource{}
+var _ resource.ResourceWithImportState = &MariadbResource{}
+
+type MariadbResource struct {
+ client *core.UniversalClient
+}
+
+// MariadbAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type MariadbAccessConfigurationModel struct {
+ MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"`
+ MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"`
+}
+
+// MariadbAutoscaleConfigurationModel — вложенная модель для map-fixed параметра autoscaleConfiguration.
+type MariadbAutoscaleConfigurationModel struct {
+ Quota types.Int64 `tfsdk:"quota" json:"quota"`
+ Percent types.Int64 `tfsdk:"percent" json:"percent"`
+ Enabled types.Bool `tfsdk:"enabled" json:"enabled"`
+ Schedule types.Int64 `tfsdk:"schedule" json:"schedule"`
+}
+
+// MariadbBackupConfigurationModel — вложенная модель для map-fixed параметра backupConfiguration.
+type MariadbBackupConfigurationModel struct {
+ S3Uid types.String `tfsdk:"s3_uid" json:"s3Uid"`
+ Retain types.Int64 `tfsdk:"retain" json:"retain"`
+ Schedule types.String `tfsdk:"schedule" json:"schedule"`
+}
+
+// MariadbClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type MariadbClusterConfigurationModel struct {
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+ Disk types.Int64 `tfsdk:"disk" json:"disk"`
+}
+
+// MariadbMariadbConfigurationModel — вложенная модель для map-fixed параметра mariadbConfiguration.
+type MariadbMariadbConfigurationModel struct {
+ Version types.String `tfsdk:"version" json:"version"`
+}
+
+// MariadbStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type MariadbStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+}
+
+type MariadbModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AccessConfiguration *MariadbAccessConfigurationModel `tfsdk:"access_configuration"`
+ AutoscaleConfiguration *MariadbAutoscaleConfigurationModel `tfsdk:"autoscale_configuration"`
+ BackupConfiguration *MariadbBackupConfigurationModel `tfsdk:"backup_configuration"`
+ ClusterConfiguration *MariadbClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ MariadbConfiguration *MariadbMariadbConfigurationModel `tfsdk:"mariadb_configuration"`
+ StartupConfiguration *MariadbStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewMariadbResource() resource.Resource {
+ return &MariadbResource{}
+}
+
+func (r *MariadbResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_mariadb"
+}
+
+func (r *MariadbResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"},
+ "master_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"},
+ }},
+ "autoscale_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "quota": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100), MarkdownDescription: "До какого размера возможно увеличивать диск, когда включен Autoscale"},
+ "percent": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Требует включения autoScale. Позволяет указать в процентах расширение от текущего макс объема
Минимальный размер на который расширяется - **1Gb**"},
+ "enabled": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает автоскейлинг PV. При включении параметра необходимо настроить autoScalePercentage и autoScaleTechWindow. Алерт при срабатывании вызывает modify у текущей услуги"},
+ "schedule": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "Требует включения autoScale. Окно обновления (пока не реализовано). Указывается час в виде Integer (0 / 5 / 12 / 23)"},
+ }},
+ "backup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "s3_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Экземпляр S3 обязателен"},
+ "retain": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(720)},
+ "schedule": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("0 0 * * *")},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Указывается в Megabytes"},
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Пока `Galera` выключена
Как будет запрос на включение и тестирование, реплики можно будет увеличивать"},
+ "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Указывается в GIGAbytes"},
+ }},
+ "mariadb_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("9.4.0")},
+ }},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"},
+ }},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *MariadbResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *MariadbModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *MariadbModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan MariadbModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 877: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}),
+ 878: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64())}),
+ 879: resources_core.BuildJSON(map[string]string{"masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString())}),
+ 880: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.MariadbConfiguration.Version.ValueString())}),
+ 881: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", config.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", config.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", config.BackupConfiguration.Schedule.ValueString())}),
+ 882: resources_core.BuildJSON(map[string]string{"quota": fmt.Sprintf("%d", config.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", config.AutoscaleConfiguration.Percent.ValueInt64()), "enabled": fmt.Sprintf("%v", config.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", config.AutoscaleConfiguration.Schedule.ValueInt64())}),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 115, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *MariadbResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data MariadbModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 115, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 877: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}),
+ 878: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64())}),
+ 879: resources_core.BuildJSON(map[string]string{"masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString())}),
+ 880: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.MariadbConfiguration.Version.ValueString())}),
+ 881: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", data.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", data.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", data.BackupConfiguration.Schedule.ValueString())}),
+ 882: resources_core.BuildJSON(map[string]string{"quota": fmt.Sprintf("%d", data.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", data.AutoscaleConfiguration.Percent.ValueInt64()), "enabled": fmt.Sprintf("%v", data.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", data.AutoscaleConfiguration.Schedule.ValueInt64())}),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 115, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 115, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"},
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "mariadbConfiguration", Field: "MariadbConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *MariadbResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state MariadbModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 115, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"},
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "mariadbConfiguration", Field: "MariadbConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *MariadbResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan MariadbModel
+ var state MariadbModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 883: resources_core.BuildJSON(map[string]string{"replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64())}),
+ 884: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString())}),
+ 885: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.MariadbConfiguration.Version.ValueString())}),
+ 886: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", plan.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.Schedule.ValueString())}),
+ 887: resources_core.BuildJSON(map[string]string{"percent": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Percent.ValueInt64()), "enabled": fmt.Sprintf("%v", plan.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Quota.ValueInt64())}),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 115, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"},
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "mariadbConfiguration", Field: "MariadbConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *MariadbResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state MariadbModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *MariadbResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *MariadbResource) 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
+}
diff --git a/generated/test/go/115_mariadb_user_resource.go b/generated/test/go/115_mariadb_user_resource.go
new file mode 100644
index 0000000..5f74ba8
--- /dev/null
+++ b/generated/test/go/115_mariadb_user_resource.go
@@ -0,0 +1,367 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: mariadb
+// Subresource: user
+
+var _ resource.Resource = &MariadbUserResource{}
+
+// MariadbUserResource manages a subresource via operations.
+type MariadbUserResource struct {
+ client *core.UniversalClient
+}
+
+type MariadbUserModel struct {
+ ID types.String `tfsdk:"id"`
+ MariadbID types.String `tfsdk:"mariadb_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"`
+ AccessHosts types.String `tfsdk:"access_hosts"`
+ DbName types.String `tfsdk:"db_name"`
+ Password types.String `tfsdk:"password"`
+ Username types.String `tfsdk:"username"`
+}
+
+func NewMariadbUserResource() resource.Resource {
+ return &MariadbUserResource{}
+}
+
+func (r *MariadbUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_mariadb_user"
+}
+
+func (r *MariadbUserResource) 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()}},
+ "mariadb_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."},
+ "access_hosts": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("%"), MarkdownDescription: "Пример: `%` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "db_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Необходимо указать массив существующих баз Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "password": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("dz8vtf8o"), MarkdownDescription: "Секрет будет записан в хранилище секретов и не будет отображаться публично Не изменяется после создания. Изменение требует пересоздания.", Sensitive: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "username": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("user2ktdtvd8"), MarkdownDescription: "Пример: `myusername` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *MariadbUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan MariadbUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.MariadbID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "accessHosts": resources_core.FormatString(plan.AccessHosts),
+ "dbName": resources_core.FormatString(plan.DbName),
+ "password": resources_core.FormatString(plan.Password),
+ "username": resources_core.FormatString(plan.Username),
+ }
+ identityCodes := []string{
+ "accessHosts",
+ "dbName",
+ "password",
+ "username",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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, "user", 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{
+ "username": resources_core.FormatString(plan.Username),
+ "password": resources_core.FormatString(plan.Password),
+ "dbName": resources_core.FormatString(plan.DbName),
+ "accessHosts": resources_core.FormatString(plan.AccessHosts),
+ })
+
+ 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, "create_user", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", 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, "user", 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, "user", 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, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *MariadbUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state MariadbUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *MariadbUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan MariadbUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state MariadbUserModel
+ 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.MariadbID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "accessHosts": resources_core.FormatString(plan.AccessHosts),
+ "dbName": resources_core.FormatString(plan.DbName),
+ "password": resources_core.FormatString(plan.Password),
+ "username": resources_core.FormatString(plan.Username),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *MariadbUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state MariadbUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_user" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.MariadbID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "accessHosts": resources_core.FormatString(state.AccessHosts),
+ "dbName": resources_core.FormatString(state.DbName),
+ "password": resources_core.FormatString(state.Password),
+ "username": resources_core.FormatString(state.Username),
+ }
+ identityCodes := []string{
+ "accessHosts",
+ "dbName",
+ "password",
+ "username",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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{
+ "username": resources_core.FormatString(state.Username),
+ })
+
+ 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, "delete_user", 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 *MariadbUserResource) 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
+}
diff --git a/generated/test/go/116_kafka_resource.go b/generated/test/go/116_kafka_resource.go
new file mode 100644
index 0000000..746405d
--- /dev/null
+++ b/generated/test/go/116_kafka_resource.go
@@ -0,0 +1,389 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: kafka
+// Service ID: 116
+
+var _ resource.Resource = &KafkaResource{}
+var _ resource.ResourceWithModifyPlan = &KafkaResource{}
+var _ resource.ResourceWithImportState = &KafkaResource{}
+
+type KafkaResource struct {
+ client *core.UniversalClient
+}
+
+// KafkaAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type KafkaAccessConfigurationModel struct {
+ MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"`
+ AllocationExternal types.Bool `tfsdk:"allocation_external" json:"allocationExternal"`
+}
+
+// KafkaClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type KafkaClusterConfigurationModel struct {
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+ Disk types.Int64 `tfsdk:"disk" json:"disk"`
+}
+
+// KafkaStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type KafkaStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+}
+
+type KafkaModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AccessConfiguration *KafkaAccessConfigurationModel `tfsdk:"access_configuration"`
+ ClusterConfiguration *KafkaClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ StartupConfiguration *KafkaStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewKafkaResource() resource.Resource {
+ return &KafkaResource{}
+}
+
+func (r *KafkaResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_kafka"
+}
+
+func (r *KafkaResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"},
+ "allocation_external": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2000), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2048)},
+ "replicas": schema.Int64Attribute{Required: true},
+ "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ }},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"},
+ }},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *KafkaResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *KafkaModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *KafkaModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan KafkaModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 1084: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1085: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64())}),
+ 1086: resources_core.BuildJSON(map[string]string{"masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString()), "allocationExternal": fmt.Sprintf("%v", config.AccessConfiguration.AllocationExternal.ValueBool())}),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 116, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *KafkaResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data KafkaModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 116, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 1084: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1085: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64())}),
+ 1086: resources_core.BuildJSON(map[string]string{"masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString()), "allocationExternal": fmt.Sprintf("%v", data.AccessConfiguration.AllocationExternal.ValueBool())}),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 116, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 116, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *KafkaResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state KafkaModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 116, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *KafkaResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan KafkaModel
+ var state KafkaModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 1087: resources_core.BuildJSON(map[string]string{"replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64())}),
+ 1088: resources_core.BuildJSON(map[string]string{"allocationExternal": fmt.Sprintf("%v", plan.AccessConfiguration.AllocationExternal.ValueBool()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString())}),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 116, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *KafkaResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state KafkaModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *KafkaResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *KafkaResource) 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
+}
diff --git a/generated/test/go/116_kafka_topic_resource.go b/generated/test/go/116_kafka_topic_resource.go
new file mode 100644
index 0000000..3f289b3
--- /dev/null
+++ b/generated/test/go/116_kafka_topic_resource.go
@@ -0,0 +1,364 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: kafka
+// Subresource: topic
+
+var _ resource.Resource = &KafkaTopicResource{}
+
+// KafkaTopicResource manages a subresource via operations.
+type KafkaTopicResource struct {
+ client *core.UniversalClient
+}
+
+type KafkaTopicModel struct {
+ ID types.String `tfsdk:"id"`
+ KafkaID types.String `tfsdk:"kafka_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"`
+ NameTopic types.String `tfsdk:"name_topic"`
+ Partitions types.Int64 `tfsdk:"partitions"`
+ Replicas types.Int64 `tfsdk:"replicas"`
+}
+
+func NewKafkaTopicResource() resource.Resource {
+ return &KafkaTopicResource{}
+}
+
+func (r *KafkaTopicResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_kafka_topic"
+}
+
+func (r *KafkaTopicResource) 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()}},
+ "kafka_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."},
+ "name_topic": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `orders-events` Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "partitions": schema.Int64Attribute{Required: true, MarkdownDescription: "Число партиций можно увеличить, но уменьшить их нельзя."},
+ "replicas": schema.Int64Attribute{Required: true, MarkdownDescription: "Количество реплик не может привышать количество реплик брокера"},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *KafkaTopicResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan KafkaTopicModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if plan.NameTopic.IsNull() || plan.NameTopic.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "name_topic is required.")
+ return
+ }
+ if plan.Partitions.IsNull() || plan.Partitions.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "partitions is required.")
+ return
+ }
+ if plan.Replicas.IsNull() || plan.Replicas.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "replicas is required.")
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.KafkaID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "nameTopic": resources_core.FormatString(plan.NameTopic),
+ }
+ identityCodes := []string{
+ "nameTopic",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("topic", 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, "topic", 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{
+ "nameTopic": resources_core.FormatString(plan.NameTopic),
+ "partitions": resources_core.FormatInt64(plan.Partitions),
+ "replicas": resources_core.FormatInt64(plan.Replicas),
+ })
+
+ 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, "create_topic", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_topic", 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, "topic", 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, "topic", 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, "topic", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *KafkaTopicResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state KafkaTopicModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *KafkaTopicResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan KafkaTopicModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "modify_topic" == "" {
+ var state KafkaTopicModel
+ 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.KafkaID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{
+ "nameTopic": resources_core.FormatString(plan.NameTopic),
+ "partitions": resources_core.FormatInt64(plan.Partitions),
+ "replicas": resources_core.FormatInt64(plan.Replicas),
+ })
+
+ idParams := map[string]string{
+ "nameTopic": resources_core.FormatString(plan.NameTopic),
+ }
+
+ 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, "modify_topic", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "topic", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *KafkaTopicResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state KafkaTopicModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_topic" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.KafkaID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "nameTopic": resources_core.FormatString(state.NameTopic),
+ }
+ identityCodes := []string{
+ "nameTopic",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("topic", 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{
+ "nameTopic": resources_core.FormatString(state.NameTopic),
+ })
+
+ 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, "delete_topic", 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 *KafkaTopicResource) 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
+}
diff --git a/generated/test/go/116_kafka_user_resource.go b/generated/test/go/116_kafka_user_resource.go
new file mode 100644
index 0000000..52ea73c
--- /dev/null
+++ b/generated/test/go/116_kafka_user_resource.go
@@ -0,0 +1,373 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: kafka
+// Subresource: user
+
+var _ resource.Resource = &KafkaUserResource{}
+
+// KafkaUserResource manages a subresource via operations.
+type KafkaUserResource struct {
+ client *core.UniversalClient
+}
+
+type KafkaUserModel struct {
+ ID types.String `tfsdk:"id"`
+ KafkaID types.String `tfsdk:"kafka_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"`
+ AccessHosts types.String `tfsdk:"access_hosts"`
+ Group types.String `tfsdk:"group"`
+ NameTopic types.String `tfsdk:"name_topic"`
+ Operations types.String `tfsdk:"operations"`
+ Username types.String `tfsdk:"username"`
+}
+
+func NewKafkaUserResource() resource.Resource {
+ return &KafkaUserResource{}
+}
+
+func (r *KafkaUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_kafka_user"
+}
+
+func (r *KafkaUserResource) 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()}},
+ "kafka_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."},
+ "access_hosts": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("*"), MarkdownDescription: "Пример: `192.168.1.0/24` или `*`"},
+ "group": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("*"), MarkdownDescription: "Пример: `order-consumers` или `*`"},
+ "name_topic": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `orders-events`. Использовать маску нельзя(`orders*`), только полное название или `*`"},
+ "operations": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `Create, Describe, Read, Write`"},
+ "username": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `username-admin` Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *KafkaUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan KafkaUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if plan.NameTopic.IsNull() || plan.NameTopic.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "name_topic is required.")
+ return
+ }
+ if plan.Operations.IsNull() || plan.Operations.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "operations is required.")
+ return
+ }
+ if plan.Username.IsNull() || plan.Username.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "username is required.")
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.KafkaID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "username": resources_core.FormatString(plan.Username),
+ }
+ identityCodes := []string{
+ "username",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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, "user", 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{
+ "username": resources_core.FormatString(plan.Username),
+ "nameTopic": resources_core.FormatString(plan.NameTopic),
+ "operations": resources_core.FormatString(plan.Operations),
+ "group": resources_core.FormatString(plan.Group),
+ "accessHosts": resources_core.FormatString(plan.AccessHosts),
+ })
+
+ 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, "create_user", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", 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, "user", 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, "user", 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, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *KafkaUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state KafkaUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *KafkaUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan KafkaUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "modify_user" == "" {
+ var state KafkaUserModel
+ 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.KafkaID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{
+ "nameTopic": resources_core.FormatString(plan.NameTopic),
+ "operations": resources_core.FormatString(plan.Operations),
+ "group": resources_core.FormatString(plan.Group),
+ "accessHosts": resources_core.FormatString(plan.AccessHosts),
+ "username": resources_core.FormatString(plan.Username),
+ })
+
+ idParams := map[string]string{
+ "username": resources_core.FormatString(plan.Username),
+ }
+
+ 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, "modify_user", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *KafkaUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state KafkaUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_user" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.KafkaID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "username": resources_core.FormatString(state.Username),
+ }
+ identityCodes := []string{
+ "username",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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{
+ "username": resources_core.FormatString(state.Username),
+ })
+
+ 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, "delete_user", 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 *KafkaUserResource) 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
+}
diff --git a/generated/test/go/117_nifi_resource.go b/generated/test/go/117_nifi_resource.go
new file mode 100644
index 0000000..c2886ce
--- /dev/null
+++ b/generated/test/go/117_nifi_resource.go
@@ -0,0 +1,533 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: nifi
+// Service ID: 117
+
+var _ resource.Resource = &NifiResource{}
+var _ resource.ResourceWithModifyPlan = &NifiResource{}
+var _ resource.ResourceWithImportState = &NifiResource{}
+
+type NifiResource struct {
+ client *core.UniversalClient
+}
+
+type NifiModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ KafkaUid types.String `tfsdk:"kafka_uid"`
+ NameTopic types.String `tfsdk:"name_topic"`
+ Partitions types.Int64 `tfsdk:"partitions"`
+ Replicas types.Int64 `tfsdk:"replicas"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewNifiResource() resource.Resource {
+ return &NifiResource{}
+}
+
+func (r *NifiResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_nifi"
+}
+
+func (r *NifiResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "kafka_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Выбрать кластер из списка Не изменяется после создания."},
+ "name_topic": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `orders-events` Не изменяется после создания."},
+ "partitions": schema.Int64Attribute{Required: true, MarkdownDescription: "Число партиций на кластер ≤ 100 × (число брокеров) × (фактор репликации). Пример `50`"},
+ "replicas": schema.Int64Attribute{Required: true, MarkdownDescription: "Количество партиций ≤ (число брокеров). Пример `2`"},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *NifiResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *NifiModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *NifiModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan NifiModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.KafkaUid.IsNull() && !plan.KafkaUid.IsUnknown() && !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.KafkaUid.ValueString(), state.KafkaUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить kafka_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.NameTopic.IsNull() && !plan.NameTopic.IsUnknown() && !state.NameTopic.IsNull() && !state.NameTopic.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.NameTopic.ValueString(), state.NameTopic.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить name_topic", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.KafkaUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "kafka_uid is required.")
+ return
+ }
+ if config.KafkaUid.IsUnknown() {
+ return
+ }
+ if config.NameTopic.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "name_topic is required.")
+ return
+ }
+ if config.NameTopic.IsUnknown() {
+ return
+ }
+ if config.Partitions.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "partitions is required.")
+ return
+ }
+ if config.Partitions.IsUnknown() {
+ return
+ }
+ if config.Replicas.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "replicas is required.")
+ return
+ }
+ if config.Replicas.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.KafkaUid.IsNull() && !config.KafkaUid.IsUnknown() {
+ resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, config.KafkaUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error())
+ } else if resolvedKafkaUid != "" && resolvedKafkaUid != config.KafkaUid.ValueString() {
+ config.KafkaUid = types.StringValue(resolvedKafkaUid)
+ }
+ }
+ params := map[int]string{
+ 471: resources_core.FormatString(config.KafkaUid),
+ 472: resources_core.FormatInt64(config.Partitions),
+ 473: resources_core.FormatInt64(config.Replicas),
+ 474: resources_core.FormatString(config.NameTopic),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 117, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *NifiResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data NifiModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.KafkaUid.IsNull() || data.KafkaUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "kafka_uid is required.")
+ return
+ }
+ if data.NameTopic.IsNull() || data.NameTopic.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "name_topic is required.")
+ return
+ }
+ if data.Partitions.IsNull() || data.Partitions.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "partitions is required.")
+ return
+ }
+ if data.Replicas.IsNull() || data.Replicas.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "replicas is required.")
+ return
+ }
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalKafkaUid := data.KafkaUid
+ if !data.KafkaUid.IsNull() && !data.KafkaUid.IsUnknown() {
+ resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, data.KafkaUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedKafkaUid != "" && resolvedKafkaUid != data.KafkaUid.ValueString() {
+ data.KafkaUid = types.StringValue(resolvedKafkaUid)
+ }
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 117, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 471: resources_core.FormatString(data.KafkaUid),
+ 472: resources_core.FormatInt64(data.Partitions),
+ 473: resources_core.FormatInt64(data.Replicas),
+ 474: resources_core.FormatString(data.NameTopic),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 117, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 117, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "kafkaUid", Field: "KafkaUid", Type: "string"},
+ {Code: "nameTopic", Field: "NameTopic", Type: "string"},
+ {Code: "partitions", Field: "Partitions", Type: "int64"},
+ {Code: "replicas", Field: "Replicas", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() {
+ resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, state.KafkaUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error())
+ } else if resolvedKafkaUid != "" && resolvedKafkaUid != state.KafkaUid.ValueString() {
+ state.KafkaUid = types.StringValue(resolvedKafkaUid)
+ }
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+ if !originalKafkaUid.IsNull() && !originalKafkaUid.IsUnknown() && !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() {
+ if strings.EqualFold(state.KafkaUid.ValueString(), originalKafkaUid.ValueString()) {
+ state.KafkaUid = originalKafkaUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *NifiResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state NifiModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 117, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "kafkaUid", Field: "KafkaUid", Type: "string"},
+ {Code: "nameTopic", Field: "NameTopic", Type: "string"},
+ {Code: "partitions", Field: "Partitions", Type: "int64"},
+ {Code: "replicas", Field: "Replicas", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !newState.KafkaUid.IsNull() && !newState.KafkaUid.IsUnknown() {
+ resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, newState.KafkaUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error())
+ } else if resolvedKafkaUid != "" && resolvedKafkaUid != newState.KafkaUid.ValueString() {
+ newState.KafkaUid = types.StringValue(resolvedKafkaUid)
+ }
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+ if !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() && !newState.KafkaUid.IsNull() && !newState.KafkaUid.IsUnknown() {
+ if strings.EqualFold(newState.KafkaUid.ValueString(), state.KafkaUid.ValueString()) {
+ newState.KafkaUid = state.KafkaUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *NifiResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan NifiModel
+ var state NifiModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.Partitions.IsNull() != state.Partitions.IsNull() || plan.Partitions.IsUnknown() != state.Partitions.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.Partitions.IsNull() && !plan.Partitions.IsUnknown() && !state.Partitions.IsNull() && !state.Partitions.IsUnknown() {
+ if plan.Partitions.ValueInt64() != state.Partitions.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.Replicas.IsNull() != state.Replicas.IsNull() || plan.Replicas.IsUnknown() != state.Replicas.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.Replicas.IsNull() && !plan.Replicas.IsUnknown() && !state.Replicas.IsNull() && !state.Replicas.IsUnknown() {
+ if plan.Replicas.ValueInt64() != state.Replicas.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 477: resources_core.FormatInt64(plan.Partitions),
+ 478: resources_core.FormatInt64(plan.Replicas),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 117, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "kafkaUid", Field: "KafkaUid", Type: "string"},
+ {Code: "nameTopic", Field: "NameTopic", Type: "string"},
+ {Code: "partitions", Field: "Partitions", Type: "int64"},
+ {Code: "replicas", Field: "Replicas", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() {
+ resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, state.KafkaUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error())
+ } else if resolvedKafkaUid != "" && resolvedKafkaUid != state.KafkaUid.ValueString() {
+ state.KafkaUid = types.StringValue(resolvedKafkaUid)
+ }
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+ if !plan.KafkaUid.IsNull() && !plan.KafkaUid.IsUnknown() && !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() {
+ if strings.EqualFold(state.KafkaUid.ValueString(), plan.KafkaUid.ValueString()) {
+ state.KafkaUid = plan.KafkaUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *NifiResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state NifiModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *NifiResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *NifiResource) 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
+}
diff --git a/generated/test/go/119_akhq_resource.go b/generated/test/go/119_akhq_resource.go
new file mode 100644
index 0000000..8ce242b
--- /dev/null
+++ b/generated/test/go/119_akhq_resource.go
@@ -0,0 +1,554 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: akhq
+// Service ID: 119
+
+var _ resource.Resource = &AkhqResource{}
+var _ resource.ResourceWithModifyPlan = &AkhqResource{}
+var _ resource.ResourceWithImportState = &AkhqResource{}
+
+type AkhqResource struct {
+ client *core.UniversalClient
+}
+
+type AkhqModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ Domain types.String `tfsdk:"domain"`
+ KafkaUid types.String `tfsdk:"kafka_uid"`
+ ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"`
+ ResourceInstances types.Int64 `tfsdk:"resource_instances"`
+ ResourceMemory types.Int64 `tfsdk:"resource_memory"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewAkhqResource() resource.Resource {
+ return &AkhqResource{}
+}
+
+func (r *AkhqResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_akhq"
+}
+
+func (r *AkhqResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "domain": schema.StringAttribute{Required: true, MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."},
+ "kafka_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Выбрать кластер из списка"},
+ "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Пример: `1000` Не изменяется после создания."},
+ "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Не изменяется после создания."},
+ "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Пример: `1024` Не изменяется после создания."},
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Выбрать платформу из списка Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *AkhqResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *AkhqModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *AkhqModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan AkhqModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() {
+ if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_c_p_u", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() {
+ if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_instances", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() {
+ if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_memory", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.Domain.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "domain is required.")
+ return
+ }
+ if config.Domain.IsUnknown() {
+ return
+ }
+ if config.KafkaUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "kafka_uid is required.")
+ return
+ }
+ if config.KafkaUid.IsUnknown() {
+ return
+ }
+ if config.ResourceRealm.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+ if config.ResourceRealm.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.KafkaUid.IsNull() && !config.KafkaUid.IsUnknown() {
+ resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, config.KafkaUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error())
+ } else if resolvedKafkaUid != "" && resolvedKafkaUid != config.KafkaUid.ValueString() {
+ config.KafkaUid = types.StringValue(resolvedKafkaUid)
+ }
+ }
+ params := map[int]string{
+ 550: resources_core.FormatString(config.ResourceRealm),
+ 551: resources_core.FormatInt64(config.ResourceMemory),
+ 552: resources_core.FormatInt64(config.ResourceCPU),
+ 553: resources_core.FormatString(config.Domain),
+ 554: resources_core.FormatInt64(config.ResourceInstances),
+ 555: resources_core.FormatString(config.KafkaUid),
+ }
+ desiredDomain := ""
+ if !config.Domain.IsNull() && !config.Domain.IsUnknown() {
+ desiredDomain = config.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 119, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *AkhqResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data AkhqModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.Domain.IsNull() || data.Domain.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "domain is required.")
+ return
+ }
+ if data.KafkaUid.IsNull() || data.KafkaUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "kafka_uid is required.")
+ return
+ }
+ if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalKafkaUid := data.KafkaUid
+ if !data.KafkaUid.IsNull() && !data.KafkaUid.IsUnknown() {
+ resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, data.KafkaUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedKafkaUid != "" && resolvedKafkaUid != data.KafkaUid.ValueString() {
+ data.KafkaUid = types.StringValue(resolvedKafkaUid)
+ }
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ if !data.Domain.IsNull() && !data.Domain.IsUnknown() {
+ desiredDomain = data.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 119, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 550: resources_core.FormatString(data.ResourceRealm),
+ 551: resources_core.FormatInt64(data.ResourceMemory),
+ 552: resources_core.FormatInt64(data.ResourceCPU),
+ 553: resources_core.FormatString(data.Domain),
+ 554: resources_core.FormatInt64(data.ResourceInstances),
+ 555: resources_core.FormatString(data.KafkaUid),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 119, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 119, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "kafkaUid", Field: "KafkaUid", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() {
+ resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, state.KafkaUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error())
+ } else if resolvedKafkaUid != "" && resolvedKafkaUid != state.KafkaUid.ValueString() {
+ state.KafkaUid = types.StringValue(resolvedKafkaUid)
+ }
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+ if !originalKafkaUid.IsNull() && !originalKafkaUid.IsUnknown() && !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() {
+ if strings.EqualFold(state.KafkaUid.ValueString(), originalKafkaUid.ValueString()) {
+ state.KafkaUid = originalKafkaUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *AkhqResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state AkhqModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 119, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "kafkaUid", Field: "KafkaUid", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !newState.KafkaUid.IsNull() && !newState.KafkaUid.IsUnknown() {
+ resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, newState.KafkaUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error())
+ } else if resolvedKafkaUid != "" && resolvedKafkaUid != newState.KafkaUid.ValueString() {
+ newState.KafkaUid = types.StringValue(resolvedKafkaUid)
+ }
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+ if !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() && !newState.KafkaUid.IsNull() && !newState.KafkaUid.IsUnknown() {
+ if strings.EqualFold(newState.KafkaUid.ValueString(), state.KafkaUid.ValueString()) {
+ newState.KafkaUid = state.KafkaUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *AkhqResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan AkhqModel
+ var state AkhqModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.KafkaUid.IsNull() != state.KafkaUid.IsNull() || plan.KafkaUid.IsUnknown() != state.KafkaUid.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.KafkaUid.IsNull() && !plan.KafkaUid.IsUnknown() && !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() {
+ if !strings.EqualFold(plan.KafkaUid.ValueString(), state.KafkaUid.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 565: resources_core.FormatString(plan.KafkaUid),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 119, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "kafkaUid", Field: "KafkaUid", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() {
+ resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, state.KafkaUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error())
+ } else if resolvedKafkaUid != "" && resolvedKafkaUid != state.KafkaUid.ValueString() {
+ state.KafkaUid = types.StringValue(resolvedKafkaUid)
+ }
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+ if !plan.KafkaUid.IsNull() && !plan.KafkaUid.IsUnknown() && !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() {
+ if strings.EqualFold(state.KafkaUid.ValueString(), plan.KafkaUid.ValueString()) {
+ state.KafkaUid = plan.KafkaUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *AkhqResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state AkhqModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *AkhqResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *AkhqResource) 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
+}
diff --git a/generated/test/go/120_clickhouse_database_resource.go b/generated/test/go/120_clickhouse_database_resource.go
new file mode 100644
index 0000000..cc5c42e
--- /dev/null
+++ b/generated/test/go/120_clickhouse_database_resource.go
@@ -0,0 +1,343 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: clickhouse
+// Subresource: database
+
+var _ resource.Resource = &ClickhouseDatabaseResource{}
+
+// ClickhouseDatabaseResource manages a subresource via operations.
+type ClickhouseDatabaseResource struct {
+ client *core.UniversalClient
+}
+
+type ClickhouseDatabaseModel struct {
+ ID types.String `tfsdk:"id"`
+ ClickhouseID types.String `tfsdk:"clickhouse_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"`
+ DbName types.String `tfsdk:"db_name"`
+}
+
+func NewClickhouseDatabaseResource() resource.Resource {
+ return &ClickhouseDatabaseResource{}
+}
+
+func (r *ClickhouseDatabaseResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_clickhouse_database"
+}
+
+func (r *ClickhouseDatabaseResource) 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()}},
+ "clickhouse_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."},
+ "db_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("db01"), MarkdownDescription: "Пример: `db01` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *ClickhouseDatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan ClickhouseDatabaseModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.ClickhouseID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "dbName": resources_core.FormatString(plan.DbName),
+ }
+ identityCodes := []string{
+ "dbName",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", 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, "database", 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{
+ "dbName": resources_core.FormatString(plan.DbName),
+ })
+
+ 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, "create_database", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_database", 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, "database", 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, "database", 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, "database", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *ClickhouseDatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state ClickhouseDatabaseModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *ClickhouseDatabaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan ClickhouseDatabaseModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state ClickhouseDatabaseModel
+ 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.ClickhouseID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "dbName": resources_core.FormatString(plan.DbName),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *ClickhouseDatabaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state ClickhouseDatabaseModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_database" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.ClickhouseID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "dbName": resources_core.FormatString(state.DbName),
+ }
+ identityCodes := []string{
+ "dbName",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", 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{
+ "dbName": resources_core.FormatString(state.DbName),
+ })
+
+ 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, "delete_database", 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 *ClickhouseDatabaseResource) 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
+}
diff --git a/generated/test/go/120_clickhouse_resource.go b/generated/test/go/120_clickhouse_resource.go
new file mode 100644
index 0000000..b5c560d
--- /dev/null
+++ b/generated/test/go/120_clickhouse_resource.go
@@ -0,0 +1,487 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: clickhouse
+// Service ID: 120
+
+var _ resource.Resource = &ClickhouseResource{}
+var _ resource.ResourceWithModifyPlan = &ClickhouseResource{}
+var _ resource.ResourceWithImportState = &ClickhouseResource{}
+
+type ClickhouseResource struct {
+ client *core.UniversalClient
+}
+
+// ClickhouseAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type ClickhouseAccessConfigurationModel struct {
+ MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"`
+ MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"`
+}
+
+// ClickhouseAutoscaleConfigurationModel — вложенная модель для map-fixed параметра autoscaleConfiguration.
+type ClickhouseAutoscaleConfigurationModel struct {
+ Enabled types.Bool `tfsdk:"enabled" json:"enabled"`
+ Schedule types.Int64 `tfsdk:"schedule" json:"schedule"`
+ Quota types.Int64 `tfsdk:"quota" json:"quota"`
+ Percent types.Int64 `tfsdk:"percent" json:"percent"`
+}
+
+// ClickhouseBackupConfigurationModel — вложенная модель для map-fixed параметра backupConfiguration.
+type ClickhouseBackupConfigurationModel struct {
+ Retain types.Int64 `tfsdk:"retain" json:"retain"`
+ Schedule types.String `tfsdk:"schedule" json:"schedule"`
+ S3Uid types.String `tfsdk:"s3_uid" json:"s3Uid"`
+}
+
+// ClickhouseClickhouseConfigurationModel — вложенная модель для map-fixed параметра clickhouseConfiguration.
+type ClickhouseClickhouseConfigurationModel struct {
+ Version types.String `tfsdk:"version" json:"version"`
+}
+
+// ClickhouseClickhouseKeeperConfigurationModel — вложенная модель для map-fixed параметра clickhouseKeeperConfiguration.
+type ClickhouseClickhouseKeeperConfigurationModel struct {
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+}
+
+// ClickhouseClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type ClickhouseClusterConfigurationModel struct {
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+ Shards types.Int64 `tfsdk:"shards" json:"shards"`
+ Disk types.Int64 `tfsdk:"disk" json:"disk"`
+}
+
+// ClickhouseStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type ClickhouseStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+}
+
+type ClickhouseModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AccessConfiguration *ClickhouseAccessConfigurationModel `tfsdk:"access_configuration"`
+ AutoscaleConfiguration *ClickhouseAutoscaleConfigurationModel `tfsdk:"autoscale_configuration"`
+ BackupConfiguration *ClickhouseBackupConfigurationModel `tfsdk:"backup_configuration"`
+ ClickhouseConfiguration *ClickhouseClickhouseConfigurationModel `tfsdk:"clickhouse_configuration"`
+ ClickhouseFilesConfiguration types.String `tfsdk:"clickhouse_files_configuration"`
+ ClickhouseKeeperConfiguration *ClickhouseClickhouseKeeperConfigurationModel `tfsdk:"clickhouse_keeper_configuration"`
+ ClusterConfiguration *ClickhouseClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ StartupConfiguration *ClickhouseStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewClickhouseResource() resource.Resource {
+ return &ClickhouseResource{}
+}
+
+func (r *ClickhouseResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_clickhouse"
+}
+
+func (r *ClickhouseResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "master_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"},
+ "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"},
+ }},
+ "autoscale_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "enabled": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает автоскейлинг PV. При включении параметра необходимо настроить autoScalePercentage и autoScaleTechWindow. Алерт при срабатывании вызывает modify у текущей услуги"},
+ "schedule": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "Требует включения autoScale. Окно обновления (пока не реализовано). Указывается час в виде Integer (0 / 5 / 12 / 23)"},
+ "quota": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100), MarkdownDescription: "До какого размера возможно увеличивать диск, когда включен Autoscale"},
+ "percent": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Требует включения autoScale. Позволяет указать в процентах расширение от текущего макс объема
Минимальный размер на который расширяется - **1Gb**"},
+ }},
+ "backup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "retain": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(7)},
+ "schedule": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("0 0 * * *")},
+ "s3_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Экземпляр S3 обязателен"},
+ }},
+ "clickhouse_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("25.10")},
+ }},
+ "clickhouse_files_configuration": schema.StringAttribute{Optional: true, MarkdownDescription: "Формат:
- path: somethings3.xml
xmlConfig: '<clickhouse>......</clickhouse>'"},
+ "clickhouse_keeper_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Указывается в Megabytes"},
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "Если указан 0, то инстансы ClickHouse Keeper развёрнуты не будут"},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1000), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2048), MarkdownDescription: "Указывается в Megabytes"},
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ "shards": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "**Шард** - это часть данных, разделенная на разные узлы (серверы) для горизонтального масштабирования и обработки больших объемов информации.
**Реплика** - это точная копия данных внутри одного шарда"},
+ "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Указывается в GIGAbytes"},
+ }},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"},
+ }},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *ClickhouseResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *ClickhouseModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *ClickhouseModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan ClickhouseModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 837: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}),
+ 838: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "shards": fmt.Sprintf("%d", config.ClusterConfiguration.Shards.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64())}),
+ 839: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString())}),
+ 840: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClickhouseKeeperConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClickhouseKeeperConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClickhouseKeeperConfiguration.Replicas.ValueInt64())}),
+ 841: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.ClickhouseConfiguration.Version.ValueString())}),
+ 842: resources_core.BuildJSON(map[string]string{"retain": fmt.Sprintf("%d", config.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", config.BackupConfiguration.Schedule.ValueString()), "s3Uid": fmt.Sprintf("\"%s\"", config.BackupConfiguration.S3Uid.ValueString())}),
+ 843: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", config.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", config.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", config.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", config.AutoscaleConfiguration.Percent.ValueInt64())}),
+ 851: resources_core.FormatString(config.ClickhouseFilesConfiguration),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 120, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *ClickhouseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data ClickhouseModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 120, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 837: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}),
+ 838: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "shards": fmt.Sprintf("%d", data.ClusterConfiguration.Shards.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64())}),
+ 839: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString())}),
+ 840: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClickhouseKeeperConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClickhouseKeeperConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClickhouseKeeperConfiguration.Replicas.ValueInt64())}),
+ 841: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.ClickhouseConfiguration.Version.ValueString())}),
+ 842: resources_core.BuildJSON(map[string]string{"retain": fmt.Sprintf("%d", data.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", data.BackupConfiguration.Schedule.ValueString()), "s3Uid": fmt.Sprintf("\"%s\"", data.BackupConfiguration.S3Uid.ValueString())}),
+ 843: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", data.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", data.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", data.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", data.AutoscaleConfiguration.Percent.ValueInt64())}),
+ 851: resources_core.FormatString(data.ClickhouseFilesConfiguration),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 120, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 120, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"},
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clickhouseConfiguration", Field: "ClickhouseConfiguration", Type: "map-fixed"},
+ {Code: "clickhouseFilesConfiguration", Field: "ClickhouseFilesConfiguration", Type: "array-map-fixed"},
+ {Code: "clickhouseKeeperConfiguration", Field: "ClickhouseKeeperConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *ClickhouseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state ClickhouseModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 120, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"},
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clickhouseConfiguration", Field: "ClickhouseConfiguration", Type: "map-fixed"},
+ {Code: "clickhouseFilesConfiguration", Field: "ClickhouseFilesConfiguration", Type: "array-map-fixed"},
+ {Code: "clickhouseKeeperConfiguration", Field: "ClickhouseKeeperConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *ClickhouseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan ClickhouseModel
+ var state ClickhouseModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.ClickhouseFilesConfiguration.IsNull() != state.ClickhouseFilesConfiguration.IsNull() || plan.ClickhouseFilesConfiguration.IsUnknown() != state.ClickhouseFilesConfiguration.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ClickhouseFilesConfiguration.IsNull() && !plan.ClickhouseFilesConfiguration.IsUnknown() && !state.ClickhouseFilesConfiguration.IsNull() && !state.ClickhouseFilesConfiguration.IsUnknown() {
+ if !strings.EqualFold(plan.ClickhouseFilesConfiguration.ValueString(), state.ClickhouseFilesConfiguration.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 844: resources_core.BuildJSON(map[string]string{"shards": fmt.Sprintf("%d", plan.ClusterConfiguration.Shards.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64())}),
+ 845: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClickhouseKeeperConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClickhouseKeeperConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClickhouseKeeperConfiguration.Replicas.ValueInt64())}),
+ 846: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString())}),
+ 847: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.ClickhouseConfiguration.Version.ValueString())}),
+ 848: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", plan.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.Schedule.ValueString())}),
+ 849: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", plan.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Percent.ValueInt64())}),
+ 850: resources_core.FormatString(plan.ClickhouseFilesConfiguration),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 120, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"},
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clickhouseConfiguration", Field: "ClickhouseConfiguration", Type: "map-fixed"},
+ {Code: "clickhouseFilesConfiguration", Field: "ClickhouseFilesConfiguration", Type: "array-map-fixed"},
+ {Code: "clickhouseKeeperConfiguration", Field: "ClickhouseKeeperConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *ClickhouseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state ClickhouseModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *ClickhouseResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *ClickhouseResource) 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
+}
diff --git a/generated/test/go/120_clickhouse_user_resource.go b/generated/test/go/120_clickhouse_user_resource.go
new file mode 100644
index 0000000..8dc28f7
--- /dev/null
+++ b/generated/test/go/120_clickhouse_user_resource.go
@@ -0,0 +1,359 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: clickhouse
+// Subresource: user
+
+var _ resource.Resource = &ClickhouseUserResource{}
+
+// ClickhouseUserResource manages a subresource via operations.
+type ClickhouseUserResource struct {
+ client *core.UniversalClient
+}
+
+type ClickhouseUserModel struct {
+ ID types.String `tfsdk:"id"`
+ ClickhouseID types.String `tfsdk:"clickhouse_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"`
+ AccessHosts types.String `tfsdk:"access_hosts"`
+ DbName types.String `tfsdk:"db_name"`
+ Username types.String `tfsdk:"username"`
+}
+
+func NewClickhouseUserResource() resource.Resource {
+ return &ClickhouseUserResource{}
+}
+
+func (r *ClickhouseUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_clickhouse_user"
+}
+
+func (r *ClickhouseUserResource) 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()}},
+ "clickhouse_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."},
+ "access_hosts": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("::/0"), MarkdownDescription: "Пример: `::/0` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "db_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "username": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("myusername"), MarkdownDescription: "Пример: `myusername` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *ClickhouseUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan ClickhouseUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.ClickhouseID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "accessHosts": resources_core.FormatString(plan.AccessHosts),
+ "dbName": resources_core.FormatString(plan.DbName),
+ "username": resources_core.FormatString(plan.Username),
+ }
+ identityCodes := []string{
+ "accessHosts",
+ "dbName",
+ "username",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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, "user", 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{
+ "username": resources_core.FormatString(plan.Username),
+ "dbName": resources_core.FormatString(plan.DbName),
+ "accessHosts": resources_core.FormatString(plan.AccessHosts),
+ })
+
+ 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, "create_user", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", 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, "user", 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, "user", 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, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *ClickhouseUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state ClickhouseUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *ClickhouseUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan ClickhouseUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state ClickhouseUserModel
+ 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.ClickhouseID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "accessHosts": resources_core.FormatString(plan.AccessHosts),
+ "dbName": resources_core.FormatString(plan.DbName),
+ "username": resources_core.FormatString(plan.Username),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *ClickhouseUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state ClickhouseUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_user" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.ClickhouseID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "accessHosts": resources_core.FormatString(state.AccessHosts),
+ "dbName": resources_core.FormatString(state.DbName),
+ "username": resources_core.FormatString(state.Username),
+ }
+ identityCodes := []string{
+ "accessHosts",
+ "dbName",
+ "username",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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{
+ "username": resources_core.FormatString(state.Username),
+ })
+
+ 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, "delete_user", 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 *ClickhouseUserResource) 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
+}
diff --git a/generated/test/go/12_s3_resource.go b/generated/test/go/12_s3_resource.go
new file mode 100644
index 0000000..2d179e4
--- /dev/null
+++ b/generated/test/go/12_s3_resource.go
@@ -0,0 +1,421 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: s3
+// Service ID: 12
+
+var _ resource.Resource = &S3Resource{}
+var _ resource.ResourceWithModifyPlan = &S3Resource{}
+var _ resource.ResourceWithImportState = &S3Resource{}
+
+type S3Resource struct {
+ client *core.UniversalClient
+}
+
+type S3Model struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ DisplayName types.String `tfsdk:"display_name"`
+ MaxBucketsPerUser types.Int64 `tfsdk:"max_buckets_per_user"`
+ MaxObjectsPerBucket types.Int64 `tfsdk:"max_objects_per_bucket"`
+ MaxSizeGbPerUser types.Int64 `tfsdk:"max_size_gb_per_user"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewS3Resource() resource.Resource {
+ return &S3Resource{}
+}
+
+func (r *S3Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_s3"
+}
+
+func (r *S3Resource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "display_name": schema.StringAttribute{Required: true, MarkdownDescription: "Человеко-читаемое имя (транслит из Elma) - передаётся имя клиента Не изменяется после создания."},
+ "max_buckets_per_user": schema.Int64Attribute{Optional: true, MarkdownDescription: "Квота на пользователя. Количество бакетов пользователя. Передаём руками"},
+ "max_objects_per_bucket": schema.Int64Attribute{Optional: true, MarkdownDescription: "Квота на бакет. Кол-во объектов. Передаём руками"},
+ "max_size_gb_per_user": schema.Int64Attribute{Optional: true, MarkdownDescription: "maxSizeGb. Передаём руками"},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("ceph.tst.nubes.ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *S3Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *S3Model
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *S3Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan S3Model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.DisplayName.IsNull() && !plan.DisplayName.IsUnknown() && !state.DisplayName.IsNull() && !state.DisplayName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.DisplayName.ValueString(), state.DisplayName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить display_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.DisplayName.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "display_name is required.")
+ return
+ }
+ if config.DisplayName.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 163: resources_core.FormatString(config.ResourceRealm),
+ 320: resources_core.FormatString(config.DisplayName),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 12, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *S3Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data S3Model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.DisplayName.IsNull() || data.DisplayName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "display_name is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 12, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 163: resources_core.FormatString(data.ResourceRealm),
+ 320: resources_core.FormatString(data.DisplayName),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 12, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 12, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "displayName", Field: "DisplayName", Type: "string"},
+ {Code: "maxBucketsPerUser", Field: "MaxBucketsPerUser", Type: "int64"},
+ {Code: "maxObjectsPerBucket", Field: "MaxObjectsPerBucket", Type: "int64"},
+ {Code: "maxSizeGbPerUser", Field: "MaxSizeGbPerUser", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *S3Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state S3Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 12, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "displayName", Field: "DisplayName", Type: "string"},
+ {Code: "maxBucketsPerUser", Field: "MaxBucketsPerUser", Type: "int64"},
+ {Code: "maxObjectsPerBucket", Field: "MaxObjectsPerBucket", Type: "int64"},
+ {Code: "maxSizeGbPerUser", Field: "MaxSizeGbPerUser", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *S3Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan S3Model
+ var state S3Model
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.MaxSizeGbPerUser.IsNull() != state.MaxSizeGbPerUser.IsNull() || plan.MaxSizeGbPerUser.IsUnknown() != state.MaxSizeGbPerUser.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.MaxSizeGbPerUser.IsNull() && !plan.MaxSizeGbPerUser.IsUnknown() && !state.MaxSizeGbPerUser.IsNull() && !state.MaxSizeGbPerUser.IsUnknown() {
+ if plan.MaxSizeGbPerUser.ValueInt64() != state.MaxSizeGbPerUser.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.MaxObjectsPerBucket.IsNull() != state.MaxObjectsPerBucket.IsNull() || plan.MaxObjectsPerBucket.IsUnknown() != state.MaxObjectsPerBucket.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.MaxObjectsPerBucket.IsNull() && !plan.MaxObjectsPerBucket.IsUnknown() && !state.MaxObjectsPerBucket.IsNull() && !state.MaxObjectsPerBucket.IsUnknown() {
+ if plan.MaxObjectsPerBucket.ValueInt64() != state.MaxObjectsPerBucket.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.MaxBucketsPerUser.IsNull() != state.MaxBucketsPerUser.IsNull() || plan.MaxBucketsPerUser.IsUnknown() != state.MaxBucketsPerUser.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.MaxBucketsPerUser.IsNull() && !plan.MaxBucketsPerUser.IsUnknown() && !state.MaxBucketsPerUser.IsNull() && !state.MaxBucketsPerUser.IsUnknown() {
+ if plan.MaxBucketsPerUser.ValueInt64() != state.MaxBucketsPerUser.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 50: resources_core.FormatInt64(plan.MaxSizeGbPerUser),
+ 51: resources_core.FormatInt64(plan.MaxObjectsPerBucket),
+ 52: resources_core.FormatInt64(plan.MaxBucketsPerUser),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 12, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "displayName", Field: "DisplayName", Type: "string"},
+ {Code: "maxBucketsPerUser", Field: "MaxBucketsPerUser", Type: "int64"},
+ {Code: "maxObjectsPerBucket", Field: "MaxObjectsPerBucket", Type: "int64"},
+ {Code: "maxSizeGbPerUser", Field: "MaxSizeGbPerUser", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *S3Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state S3Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *S3Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *S3Resource) 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
+}
diff --git a/generated/test/go/12_s3_sub_user_resource.go b/generated/test/go/12_s3_sub_user_resource.go
new file mode 100644
index 0000000..b8e0168
--- /dev/null
+++ b/generated/test/go/12_s3_sub_user_resource.go
@@ -0,0 +1,358 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: s3
+// Subresource: sub_user
+
+var _ resource.Resource = &S3SubUserResource{}
+
+// S3SubUserResource manages a subresource via operations.
+type S3SubUserResource struct {
+ client *core.UniversalClient
+}
+
+type S3SubUserModel struct {
+ ID types.String `tfsdk:"id"`
+ S3ID types.String `tfsdk:"s3_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"`
+ SubUserAccess types.String `tfsdk:"sub_user_access"`
+ SubUserName types.String `tfsdk:"sub_user_name"`
+}
+
+func NewS3SubUserResource() resource.Resource {
+ return &S3SubUserResource{}
+}
+
+func (r *S3SubUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_s3_sub_user"
+}
+
+func (r *S3SubUserResource) 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()}},
+ "s3_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."},
+ "sub_user_access": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "sub_user_name": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *S3SubUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan S3SubUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if plan.SubUserAccess.IsNull() || plan.SubUserAccess.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "sub_user_access is required.")
+ return
+ }
+ if plan.SubUserName.IsNull() || plan.SubUserName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "sub_user_name is required.")
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.S3ID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "subUserAccess": resources_core.FormatString(plan.SubUserAccess),
+ "subUserName": resources_core.FormatString(plan.SubUserName),
+ }
+ identityCodes := []string{
+ "subUserAccess",
+ "subUserName",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("sub_user", 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, "sub_user", 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{
+ "subUserName": resources_core.FormatString(plan.SubUserName),
+ "subUserAccess": resources_core.FormatString(plan.SubUserAccess),
+ })
+
+ 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, "create_sub_user", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_sub_user", 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, "sub_user", 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, "sub_user", 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, "sub_user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *S3SubUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state S3SubUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *S3SubUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan S3SubUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state S3SubUserModel
+ 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.S3ID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "subUserAccess": resources_core.FormatString(plan.SubUserAccess),
+ "subUserName": resources_core.FormatString(plan.SubUserName),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "sub_user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *S3SubUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state S3SubUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_sub_user" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.S3ID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "subUserAccess": resources_core.FormatString(state.SubUserAccess),
+ "subUserName": resources_core.FormatString(state.SubUserName),
+ }
+ identityCodes := []string{
+ "subUserAccess",
+ "subUserName",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("sub_user", 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{
+ "subUserName": resources_core.FormatString(state.SubUserName),
+ })
+
+ 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, "delete_sub_user", 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 *S3SubUserResource) 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
+}
diff --git a/generated/test/go/13_s3bucket_resource.go b/generated/test/go/13_s3bucket_resource.go
new file mode 100644
index 0000000..644ffb7
--- /dev/null
+++ b/generated/test/go/13_s3bucket_resource.go
@@ -0,0 +1,478 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: s3bucket
+// Service ID: 13
+
+var _ resource.Resource = &S3bucketResource{}
+var _ resource.ResourceWithModifyPlan = &S3bucketResource{}
+var _ resource.ResourceWithImportState = &S3bucketResource{}
+
+type S3bucketResource struct {
+ client *core.UniversalClient
+}
+
+type S3bucketModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ BucketName types.String `tfsdk:"bucket_name"`
+ CorsAll types.Bool `tfsdk:"cors_all"`
+ ListAll types.Bool `tfsdk:"list_all"`
+ MaxSize types.String `tfsdk:"max_size"`
+ Placement types.String `tfsdk:"placement"`
+ ReadAll types.Bool `tfsdk:"read_all"`
+ S3UserUid types.String `tfsdk:"s3_user_uid"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewS3bucketResource() resource.Resource {
+ return &S3bucketResource{}
+}
+
+func (r *S3bucketResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_s3bucket"
+}
+
+func (r *S3bucketResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "bucket_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя бакета s3 Не изменяется после создания."},
+ "cors_all": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Публичный доступ к чтению настроек позволяет любому пользователю интернета читать конфигурации CORS Не изменяется после создания."},
+ "list_all": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Публичный доступ к листингу позволяет любому пользователю интернета получить список объектов в бакете. Не изменяется после создания."},
+ "max_size": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("-1"), MarkdownDescription: "Максимальный размер бакета s3. Если поставить -1, будет безлимит. Размер указывается в Bytes Не изменяется после создания."},
+ "placement": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("HOT"), MarkdownDescription: "Класс хранилища, который используется по умолчанию при загрузке объектов. Чем холоднее, тем дешевле место, но дороже обращения Не изменяется после создания."},
+ "read_all": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Публичный доступ на чтение позволяет любому пользователю интернета скачивать объекты из бакета. Не изменяется после создания."},
+ "s3_user_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UID Корневой услуги S3. На основе него будет создан бакет. Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *S3bucketResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *S3bucketModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *S3bucketModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan S3bucketModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.BucketName.IsNull() && !plan.BucketName.IsUnknown() && !state.BucketName.IsNull() && !state.BucketName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.BucketName.ValueString(), state.BucketName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить bucket_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.CorsAll.IsNull() && !plan.CorsAll.IsUnknown() && !state.CorsAll.IsNull() && !state.CorsAll.IsUnknown() {
+ if plan.CorsAll.ValueBool() != state.CorsAll.ValueBool() {
+ resp.Diagnostics.AddError("Нельзя изменить cors_all", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ListAll.IsNull() && !plan.ListAll.IsUnknown() && !state.ListAll.IsNull() && !state.ListAll.IsUnknown() {
+ if plan.ListAll.ValueBool() != state.ListAll.ValueBool() {
+ resp.Diagnostics.AddError("Нельзя изменить list_all", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.MaxSize.IsNull() && !plan.MaxSize.IsUnknown() && !state.MaxSize.IsNull() && !state.MaxSize.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.MaxSize.ValueString(), state.MaxSize.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить max_size", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.Placement.IsNull() && !plan.Placement.IsUnknown() && !state.Placement.IsNull() && !state.Placement.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Placement.ValueString(), state.Placement.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить placement", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ReadAll.IsNull() && !plan.ReadAll.IsUnknown() && !state.ReadAll.IsNull() && !state.ReadAll.IsUnknown() {
+ if plan.ReadAll.ValueBool() != state.ReadAll.ValueBool() {
+ resp.Diagnostics.AddError("Нельзя изменить read_all", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.S3UserUid.IsNull() && !plan.S3UserUid.IsUnknown() && !state.S3UserUid.IsNull() && !state.S3UserUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.S3UserUid.ValueString(), state.S3UserUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить s3_user_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.BucketName.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "bucket_name is required.")
+ return
+ }
+ if config.BucketName.IsUnknown() {
+ return
+ }
+ if config.S3UserUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "s3_user_uid is required.")
+ return
+ }
+ if config.S3UserUid.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.S3UserUid.IsNull() && !config.S3UserUid.IsUnknown() {
+ resolvedS3UserUid, err := r.client.ResolveRefSvcParamValue(ctx, 12, config.S3UserUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve s3_user_uid", err.Error())
+ } else if resolvedS3UserUid != "" && resolvedS3UserUid != config.S3UserUid.ValueString() {
+ config.S3UserUid = types.StringValue(resolvedS3UserUid)
+ }
+ }
+ params := map[int]string{
+ 124: resources_core.FormatString(config.S3UserUid),
+ 125: resources_core.FormatString(config.BucketName),
+ 126: resources_core.FormatString(config.MaxSize),
+ 127: resources_core.FormatBool(config.ReadAll),
+ 128: resources_core.FormatBool(config.ListAll),
+ 129: resources_core.FormatBool(config.CorsAll),
+ 130: resources_core.FormatString(config.Placement),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 13, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *S3bucketResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data S3bucketModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.BucketName.IsNull() || data.BucketName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "bucket_name is required.")
+ return
+ }
+ if data.S3UserUid.IsNull() || data.S3UserUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "s3_user_uid is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 13, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 124: resources_core.FormatString(data.S3UserUid),
+ 125: resources_core.FormatString(data.BucketName),
+ 126: resources_core.FormatString(data.MaxSize),
+ 127: resources_core.FormatBool(data.ReadAll),
+ 128: resources_core.FormatBool(data.ListAll),
+ 129: resources_core.FormatBool(data.CorsAll),
+ 130: resources_core.FormatString(data.Placement),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 13, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 13, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "bucketName", Field: "BucketName", Type: "string"},
+ {Code: "corsAll", Field: "CorsAll", Type: "bool"},
+ {Code: "listAll", Field: "ListAll", Type: "bool"},
+ {Code: "maxSize", Field: "MaxSize", Type: "string"},
+ {Code: "placement", Field: "Placement", Type: "string"},
+ {Code: "readAll", Field: "ReadAll", Type: "bool"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *S3bucketResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state S3bucketModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 13, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "bucketName", Field: "BucketName", Type: "string"},
+ {Code: "corsAll", Field: "CorsAll", Type: "bool"},
+ {Code: "listAll", Field: "ListAll", Type: "bool"},
+ {Code: "maxSize", Field: "MaxSize", Type: "string"},
+ {Code: "placement", Field: "Placement", Type: "string"},
+ {Code: "readAll", Field: "ReadAll", Type: "bool"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *S3bucketResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan S3bucketModel
+ var state S3bucketModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 13, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "bucketName", Field: "BucketName", Type: "string"},
+ {Code: "corsAll", Field: "CorsAll", Type: "bool"},
+ {Code: "listAll", Field: "ListAll", Type: "bool"},
+ {Code: "maxSize", Field: "MaxSize", Type: "string"},
+ {Code: "placement", Field: "Placement", Type: "string"},
+ {Code: "readAll", Field: "ReadAll", Type: "bool"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *S3bucketResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state S3bucketModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *S3bucketResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *S3bucketResource) 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
+}
diff --git a/generated/test/go/148_vc_mgmt_sthutrval_cluster_resource.go b/generated/test/go/148_vc_mgmt_sthutrval_cluster_resource.go
new file mode 100644
index 0000000..cfd5712
--- /dev/null
+++ b/generated/test/go/148_vc_mgmt_sthutrval_cluster_resource.go
@@ -0,0 +1,419 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_mgmt_sthutrval_cluster
+// Service ID: 148
+
+var _ resource.Resource = &VcMgmtSthutrvalClusterResource{}
+var _ resource.ResourceWithModifyPlan = &VcMgmtSthutrvalClusterResource{}
+var _ resource.ResourceWithImportState = &VcMgmtSthutrvalClusterResource{}
+
+type VcMgmtSthutrvalClusterResource struct {
+ client *core.UniversalClient
+}
+
+// VcMgmtSthutrvalClusterAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type VcMgmtSthutrvalClusterAccessConfigurationModel struct {
+ NeedExternalAddressAPI types.Bool `tfsdk:"need_external_address_a_p_i" json:"needExternalAddressAPI"`
+ AccessIpListAPI types.String `tfsdk:"access_ip_list_a_p_i" json:"accessIpListAPI"`
+ NeedExternalAddressIngress types.Bool `tfsdk:"need_external_address_ingress" json:"needExternalAddressIngress"`
+ AccessIpListIngress types.String `tfsdk:"access_ip_list_ingress" json:"accessIpListIngress"`
+}
+
+// VcMgmtSthutrvalClusterClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type VcMgmtSthutrvalClusterClusterConfigurationModel struct {
+ AppVersion types.String `tfsdk:"app_version" json:"appVersion"`
+}
+
+// VcMgmtSthutrvalClusterControlPlaneConfigurationModel — вложенная модель для map-fixed параметра controlPlaneConfiguration.
+type VcMgmtSthutrvalClusterControlPlaneConfigurationModel struct {
+ SizingPolicy types.String `tfsdk:"sizing_policy" json:"sizingPolicy"`
+ SizingDisk types.Int64 `tfsdk:"sizing_disk" json:"sizingDisk"`
+ Count types.Int64 `tfsdk:"count" json:"count"`
+}
+
+// VcMgmtSthutrvalClusterStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type VcMgmtSthutrvalClusterStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+ ClusterName types.String `tfsdk:"cluster_name" json:"clusterName"`
+}
+
+type VcMgmtSthutrvalClusterModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AccessConfiguration *VcMgmtSthutrvalClusterAccessConfigurationModel `tfsdk:"access_configuration"`
+ ClusterConfiguration *VcMgmtSthutrvalClusterClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ ControlPlaneConfiguration *VcMgmtSthutrvalClusterControlPlaneConfigurationModel `tfsdk:"control_plane_configuration"`
+ StartupConfiguration *VcMgmtSthutrvalClusterStartupConfigurationModel `tfsdk:"startup_configuration"`
+ WorkerConfiguration types.String `tfsdk:"worker_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVcMgmtSthutrvalClusterResource() resource.Resource {
+ return &VcMgmtSthutrvalClusterResource{}
+}
+
+func (r *VcMgmtSthutrvalClusterResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_mgmt_sthutrval_cluster"
+}
+
+func (r *VcMgmtSthutrvalClusterResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "need_external_address_a_p_i": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Выделяет внешний IP для доступа к kubernetes API. Пока нельзя выставить false"},
+ "access_ip_list_a_p_i": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Access IP List для подключения к Kubernetes API. Указывается массив. Если передан пустой массив, доступ выделяется всем
Пример: **["0.0.0.0/0"]** или **[]** или **["178.4.13.2", "178.4.14.0/24"]**"},
+ "need_external_address_ingress": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Выделяет внешний IP для доступа к Ingress"},
+ "access_ip_list_ingress": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Access IP List для подключения к Ingress. Указывается массив. Если передан пустой массив, доступ выделяется всем
Пример: **["0.0.0.0/0"]** или **[]** или **["178.4.13.2", "178.4.14.0/24"]**"},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "app_version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("2.13.1")},
+ }},
+ "control_plane_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "sizing_policy": schema.StringAttribute{Required: true, MarkdownDescription: "Доступные sizing Policy представлены исходя из ресурсной платформы, которая была выбрана"},
+ "sizing_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(50)},
+ "count": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ }},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("sandbox.nubes.ru"), MarkdownDescription: "Облако в котором будет развёрнут тенант"},
+ "cluster_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("example-co5wmalx")},
+ }},
+ "worker_configuration": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcMgmtSthutrvalClusterResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VcMgmtSthutrvalClusterModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VcMgmtSthutrvalClusterModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VcMgmtSthutrvalClusterModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.WorkerConfiguration.IsNull() && !plan.WorkerConfiguration.IsUnknown() && !state.WorkerConfiguration.IsNull() && !state.WorkerConfiguration.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.WorkerConfiguration.ValueString(), state.WorkerConfiguration.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить worker_configuration", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 771: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString()), "clusterName": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ClusterName.ValueString())}),
+ 772: resources_core.BuildJSON(map[string]string{"appVersion": fmt.Sprintf("\"%s\"", config.ClusterConfiguration.AppVersion.ValueString())}),
+ 773: resources_core.BuildJSON(map[string]string{"sizingPolicy": fmt.Sprintf("\"%s\"", config.ControlPlaneConfiguration.SizingPolicy.ValueString()), "sizingDisk": fmt.Sprintf("%d", config.ControlPlaneConfiguration.SizingDisk.ValueInt64()), "count": fmt.Sprintf("%d", config.ControlPlaneConfiguration.Count.ValueInt64())}),
+ 774: resources_core.FormatString(config.WorkerConfiguration),
+ 775: resources_core.BuildJSON(map[string]string{"needExternalAddressAPI": fmt.Sprintf("%v", config.AccessConfiguration.NeedExternalAddressAPI.ValueBool()), "accessIpListAPI": fmt.Sprintf("\"%s\"", config.AccessConfiguration.AccessIpListAPI.ValueString()), "needExternalAddressIngress": fmt.Sprintf("%v", config.AccessConfiguration.NeedExternalAddressIngress.ValueBool()), "accessIpListIngress": fmt.Sprintf("\"%s\"", config.AccessConfiguration.AccessIpListIngress.ValueString())}),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 148, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VcMgmtSthutrvalClusterResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VcMgmtSthutrvalClusterModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 148, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 771: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString()), "clusterName": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ClusterName.ValueString())}),
+ 772: resources_core.BuildJSON(map[string]string{"appVersion": fmt.Sprintf("\"%s\"", data.ClusterConfiguration.AppVersion.ValueString())}),
+ 773: resources_core.BuildJSON(map[string]string{"sizingPolicy": fmt.Sprintf("\"%s\"", data.ControlPlaneConfiguration.SizingPolicy.ValueString()), "sizingDisk": fmt.Sprintf("%d", data.ControlPlaneConfiguration.SizingDisk.ValueInt64()), "count": fmt.Sprintf("%d", data.ControlPlaneConfiguration.Count.ValueInt64())}),
+ 774: resources_core.FormatString(data.WorkerConfiguration),
+ 775: resources_core.BuildJSON(map[string]string{"needExternalAddressAPI": fmt.Sprintf("%v", data.AccessConfiguration.NeedExternalAddressAPI.ValueBool()), "accessIpListAPI": fmt.Sprintf("\"%s\"", data.AccessConfiguration.AccessIpListAPI.ValueString()), "needExternalAddressIngress": fmt.Sprintf("%v", data.AccessConfiguration.NeedExternalAddressIngress.ValueBool()), "accessIpListIngress": fmt.Sprintf("\"%s\"", data.AccessConfiguration.AccessIpListIngress.ValueString())}),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 148, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 148, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcMgmtSthutrvalClusterResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcMgmtSthutrvalClusterModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 148, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VcMgmtSthutrvalClusterResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcMgmtSthutrvalClusterModel
+ var state VcMgmtSthutrvalClusterModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 148, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcMgmtSthutrvalClusterResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcMgmtSthutrvalClusterModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VcMgmtSthutrvalClusterResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VcMgmtSthutrvalClusterResource) 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
+}
diff --git a/generated/test/go/149_valo_tenant_resource.go b/generated/test/go/149_valo_tenant_resource.go
new file mode 100644
index 0000000..252f012
--- /dev/null
+++ b/generated/test/go/149_valo_tenant_resource.go
@@ -0,0 +1,431 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: valo_tenant
+// Service ID: 149
+
+var _ resource.Resource = &ValoTenantResource{}
+var _ resource.ResourceWithModifyPlan = &ValoTenantResource{}
+var _ resource.ResourceWithImportState = &ValoTenantResource{}
+
+type ValoTenantResource struct {
+ client *core.UniversalClient
+}
+
+// ValoTenantValoOptionsModel — вложенная модель для map-fixed параметра valoOptions.
+type ValoTenantValoOptionsModel struct {
+ RequestApproveMaxPeriod types.Int64 `tfsdk:"request_approve_max_period" json:"requestApproveMaxPeriod"`
+ FfOnlineEditor types.Bool `tfsdk:"ff_online_editor" json:"ffOnlineEditor"`
+ FfWatermark types.Bool `tfsdk:"ff_watermark" json:"ffWatermark"`
+ FfUserImport types.Bool `tfsdk:"ff_user_import" json:"ffUserImport"`
+ FfThemes types.Bool `tfsdk:"ff_themes" json:"ffThemes"`
+ FfPublicLink types.Bool `tfsdk:"ff_public_link" json:"ffPublicLink"`
+ FfLogging types.Bool `tfsdk:"ff_logging" json:"ffLogging"`
+ UserCount types.Int64 `tfsdk:"user_count" json:"userCount"`
+}
+
+type ValoTenantModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ Domain types.String `tfsdk:"domain"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ S3Storage types.Int64 `tfsdk:"s3_storage"`
+ ValoOptions *ValoTenantValoOptionsModel `tfsdk:"valo_options"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewValoTenantResource() resource.Resource {
+ return &ValoTenantResource{}
+}
+
+func (r *ValoTenantResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_valo_tenant"
+}
+
+func (r *ValoTenantResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "domain": schema.StringAttribute{Required: true, MarkdownDescription: "Будет добавлено в зону valo.ngcloud.ru Не изменяется после создания."},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("valo.ngcloud.ru"), MarkdownDescription: "Не изменяется после создания."},
+ "s3_storage": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Параметр указывается в `Gb`"},
+ "valo_options": schema.SingleNestedAttribute{Optional: true, Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "request_approve_max_period": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2), MarkdownDescription: "Максимальное значение периода согласования (в днях)"},
+ "ff_online_editor": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"},
+ "ff_watermark": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"},
+ "ff_user_import": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"},
+ "ff_themes": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"},
+ "ff_public_link": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"},
+ "ff_logging": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"},
+ "user_count": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10)},
+ }},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *ValoTenantResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *ValoTenantModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *ValoTenantModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan ValoTenantModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.Domain.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "domain is required.")
+ return
+ }
+ if config.Domain.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 655: resources_core.FormatString(config.ResourceRealm),
+ 656: resources_core.FormatString(config.Domain),
+ 657: resources_core.BuildJSON(map[string]string{"requestApproveMaxPeriod": fmt.Sprintf("%d", config.ValoOptions.RequestApproveMaxPeriod.ValueInt64()), "ffOnlineEditor": fmt.Sprintf("%v", config.ValoOptions.FfOnlineEditor.ValueBool()), "ffWatermark": fmt.Sprintf("%v", config.ValoOptions.FfWatermark.ValueBool()), "ffUserImport": fmt.Sprintf("%v", config.ValoOptions.FfUserImport.ValueBool()), "ffThemes": fmt.Sprintf("%v", config.ValoOptions.FfThemes.ValueBool()), "ffPublicLink": fmt.Sprintf("%v", config.ValoOptions.FfPublicLink.ValueBool()), "ffLogging": fmt.Sprintf("%v", config.ValoOptions.FfLogging.ValueBool()), "userCount": fmt.Sprintf("%d", config.ValoOptions.UserCount.ValueInt64())}),
+ 658: resources_core.FormatInt64(config.S3Storage),
+ }
+ desiredDomain := ""
+ if !config.Domain.IsNull() && !config.Domain.IsUnknown() {
+ desiredDomain = config.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 149, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *ValoTenantResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data ValoTenantModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.Domain.IsNull() || data.Domain.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "domain is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ if !data.Domain.IsNull() && !data.Domain.IsUnknown() {
+ desiredDomain = data.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 149, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 655: resources_core.FormatString(data.ResourceRealm),
+ 656: resources_core.FormatString(data.Domain),
+ 657: resources_core.BuildJSON(map[string]string{"requestApproveMaxPeriod": fmt.Sprintf("%d", data.ValoOptions.RequestApproveMaxPeriod.ValueInt64()), "ffOnlineEditor": fmt.Sprintf("%v", data.ValoOptions.FfOnlineEditor.ValueBool()), "ffWatermark": fmt.Sprintf("%v", data.ValoOptions.FfWatermark.ValueBool()), "ffUserImport": fmt.Sprintf("%v", data.ValoOptions.FfUserImport.ValueBool()), "ffThemes": fmt.Sprintf("%v", data.ValoOptions.FfThemes.ValueBool()), "ffPublicLink": fmt.Sprintf("%v", data.ValoOptions.FfPublicLink.ValueBool()), "ffLogging": fmt.Sprintf("%v", data.ValoOptions.FfLogging.ValueBool()), "userCount": fmt.Sprintf("%d", data.ValoOptions.UserCount.ValueInt64())}),
+ 658: resources_core.FormatInt64(data.S3Storage),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 149, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 149, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "s3Storage", Field: "S3Storage", Type: "int64"},
+ {Code: "valoOptions", Field: "ValoOptions", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *ValoTenantResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state ValoTenantModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 149, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "s3Storage", Field: "S3Storage", Type: "int64"},
+ {Code: "valoOptions", Field: "ValoOptions", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *ValoTenantResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan ValoTenantModel
+ var state ValoTenantModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.S3Storage.IsNull() != state.S3Storage.IsNull() || plan.S3Storage.IsUnknown() != state.S3Storage.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.S3Storage.IsNull() && !plan.S3Storage.IsUnknown() && !state.S3Storage.IsNull() && !state.S3Storage.IsUnknown() {
+ if plan.S3Storage.ValueInt64() != state.S3Storage.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 659: resources_core.BuildJSON(map[string]string{"ffWatermark": fmt.Sprintf("%v", plan.ValoOptions.FfWatermark.ValueBool()), "ffUserImport": fmt.Sprintf("%v", plan.ValoOptions.FfUserImport.ValueBool()), "ffThemes": fmt.Sprintf("%v", plan.ValoOptions.FfThemes.ValueBool()), "ffPublicLink": fmt.Sprintf("%v", plan.ValoOptions.FfPublicLink.ValueBool()), "ffLogging": fmt.Sprintf("%v", plan.ValoOptions.FfLogging.ValueBool()), "userCount": fmt.Sprintf("%d", plan.ValoOptions.UserCount.ValueInt64()), "requestApproveMaxPeriod": fmt.Sprintf("%d", plan.ValoOptions.RequestApproveMaxPeriod.ValueInt64()), "ffOnlineEditor": fmt.Sprintf("%v", plan.ValoOptions.FfOnlineEditor.ValueBool())}),
+ 660: resources_core.FormatInt64(plan.S3Storage),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 149, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "s3Storage", Field: "S3Storage", Type: "int64"},
+ {Code: "valoOptions", Field: "ValoOptions", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *ValoTenantResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state ValoTenantModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *ValoTenantResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *ValoTenantResource) 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
+}
diff --git a/generated/test/go/150_k8s_sthutrval_cluster_resource.go b/generated/test/go/150_k8s_sthutrval_cluster_resource.go
new file mode 100644
index 0000000..d77d291
--- /dev/null
+++ b/generated/test/go/150_k8s_sthutrval_cluster_resource.go
@@ -0,0 +1,429 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: k8s_sthutrval_cluster
+// Service ID: 150
+
+var _ resource.Resource = &K8sSthutrvalClusterResource{}
+var _ resource.ResourceWithModifyPlan = &K8sSthutrvalClusterResource{}
+var _ resource.ResourceWithImportState = &K8sSthutrvalClusterResource{}
+
+type K8sSthutrvalClusterResource struct {
+ client *core.UniversalClient
+}
+
+// K8sSthutrvalClusterAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type K8sSthutrvalClusterAccessConfigurationModel struct {
+ NeedExternalAddressAPI types.Bool `tfsdk:"need_external_address_a_p_i" json:"needExternalAddressAPI"`
+ AccessIpListAPI types.String `tfsdk:"access_ip_list_a_p_i" json:"accessIpListAPI"`
+ NeedExternalAddressIngress types.Bool `tfsdk:"need_external_address_ingress" json:"needExternalAddressIngress"`
+ AccessIpListIngress types.String `tfsdk:"access_ip_list_ingress" json:"accessIpListIngress"`
+}
+
+// K8sSthutrvalClusterClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type K8sSthutrvalClusterClusterConfigurationModel struct {
+ AppVersion types.String `tfsdk:"app_version" json:"appVersion"`
+}
+
+// K8sSthutrvalClusterControlPlaneConfigurationModel — вложенная модель для map-fixed параметра controlPlaneConfiguration.
+type K8sSthutrvalClusterControlPlaneConfigurationModel struct {
+ SizingPolicy types.String `tfsdk:"sizing_policy" json:"sizingPolicy"`
+ SizingDisk types.Int64 `tfsdk:"sizing_disk" json:"sizingDisk"`
+ Count types.Int64 `tfsdk:"count" json:"count"`
+}
+
+// K8sSthutrvalClusterStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type K8sSthutrvalClusterStartupConfigurationModel struct {
+ NsxtUid types.String `tfsdk:"nsxt_uid" json:"nsxtUid"`
+ ClusterName types.String `tfsdk:"cluster_name" json:"clusterName"`
+ VdcUid types.String `tfsdk:"vdc_uid" json:"vdcUid"`
+}
+
+type K8sSthutrvalClusterModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AccessConfiguration *K8sSthutrvalClusterAccessConfigurationModel `tfsdk:"access_configuration"`
+ ClusterConfiguration *K8sSthutrvalClusterClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ ControlPlaneConfiguration *K8sSthutrvalClusterControlPlaneConfigurationModel `tfsdk:"control_plane_configuration"`
+ StartupConfiguration *K8sSthutrvalClusterStartupConfigurationModel `tfsdk:"startup_configuration"`
+ WorkerConfiguration types.String `tfsdk:"worker_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewK8sSthutrvalClusterResource() resource.Resource {
+ return &K8sSthutrvalClusterResource{}
+}
+
+func (r *K8sSthutrvalClusterResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_k8s_sthutrval_cluster"
+}
+
+func (r *K8sSthutrvalClusterResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "need_external_address_a_p_i": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Выделяет внешний IP для доступа к kubernetes API. Пока нельзя выставить false"},
+ "access_ip_list_a_p_i": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Access IP List для подключения к Kubernetes API. Указывается массив. Если передан пустой массив, доступ выделяется всем"},
+ "need_external_address_ingress": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Выделяет внешний IP для доступа к Ingress"},
+ "access_ip_list_ingress": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Access IP List для подключения к Ingress. Указывается массив. Если передан пустой массив, доступ выделяется всем"},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "app_version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("2.13.1")},
+ }},
+ "control_plane_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "sizing_policy": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Доступные sizing Policy можно обновлять через услугу `Виртуальный датацентр`"},
+ "sizing_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(50), MarkdownDescription: "Дисковая политика берётся из услуги VDC, первый в списке"},
+ "count": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ }},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "nsxt_uid": schema.StringAttribute{Required: true},
+ "cluster_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("example-biu4ghxt")},
+ "vdc_uid": schema.StringAttribute{Required: true},
+ }},
+ "worker_configuration": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]")},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *K8sSthutrvalClusterResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *K8sSthutrvalClusterModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *K8sSthutrvalClusterModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan K8sSthutrvalClusterModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 750: resources_core.BuildJSON(map[string]string{"nsxtUid": fmt.Sprintf("\"%s\"", config.StartupConfiguration.NsxtUid.ValueString()), "clusterName": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ClusterName.ValueString()), "vdcUid": fmt.Sprintf("\"%s\"", config.StartupConfiguration.VdcUid.ValueString())}),
+ 751: resources_core.BuildJSON(map[string]string{"sizingPolicy": fmt.Sprintf("\"%s\"", config.ControlPlaneConfiguration.SizingPolicy.ValueString()), "sizingDisk": fmt.Sprintf("%d", config.ControlPlaneConfiguration.SizingDisk.ValueInt64()), "count": fmt.Sprintf("%d", config.ControlPlaneConfiguration.Count.ValueInt64())}),
+ 752: resources_core.FormatString(config.WorkerConfiguration),
+ 753: resources_core.BuildJSON(map[string]string{"needExternalAddressAPI": fmt.Sprintf("%v", config.AccessConfiguration.NeedExternalAddressAPI.ValueBool()), "accessIpListAPI": fmt.Sprintf("\"%s\"", config.AccessConfiguration.AccessIpListAPI.ValueString()), "needExternalAddressIngress": fmt.Sprintf("%v", config.AccessConfiguration.NeedExternalAddressIngress.ValueBool()), "accessIpListIngress": fmt.Sprintf("\"%s\"", config.AccessConfiguration.AccessIpListIngress.ValueString())}),
+ 755: resources_core.BuildJSON(map[string]string{"appVersion": fmt.Sprintf("\"%s\"", config.ClusterConfiguration.AppVersion.ValueString())}),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 150, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *K8sSthutrvalClusterResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data K8sSthutrvalClusterModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 150, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 750: resources_core.BuildJSON(map[string]string{"nsxtUid": fmt.Sprintf("\"%s\"", data.StartupConfiguration.NsxtUid.ValueString()), "clusterName": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ClusterName.ValueString()), "vdcUid": fmt.Sprintf("\"%s\"", data.StartupConfiguration.VdcUid.ValueString())}),
+ 751: resources_core.BuildJSON(map[string]string{"sizingPolicy": fmt.Sprintf("\"%s\"", data.ControlPlaneConfiguration.SizingPolicy.ValueString()), "sizingDisk": fmt.Sprintf("%d", data.ControlPlaneConfiguration.SizingDisk.ValueInt64()), "count": fmt.Sprintf("%d", data.ControlPlaneConfiguration.Count.ValueInt64())}),
+ 752: resources_core.FormatString(data.WorkerConfiguration),
+ 753: resources_core.BuildJSON(map[string]string{"needExternalAddressAPI": fmt.Sprintf("%v", data.AccessConfiguration.NeedExternalAddressAPI.ValueBool()), "accessIpListAPI": fmt.Sprintf("\"%s\"", data.AccessConfiguration.AccessIpListAPI.ValueString()), "needExternalAddressIngress": fmt.Sprintf("%v", data.AccessConfiguration.NeedExternalAddressIngress.ValueBool()), "accessIpListIngress": fmt.Sprintf("\"%s\"", data.AccessConfiguration.AccessIpListIngress.ValueString())}),
+ 755: resources_core.BuildJSON(map[string]string{"appVersion": fmt.Sprintf("\"%s\"", data.ClusterConfiguration.AppVersion.ValueString())}),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 150, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 150, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8sSthutrvalClusterResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state K8sSthutrvalClusterModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 150, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *K8sSthutrvalClusterResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan K8sSthutrvalClusterModel
+ var state K8sSthutrvalClusterModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.WorkerConfiguration.IsNull() != state.WorkerConfiguration.IsNull() || plan.WorkerConfiguration.IsUnknown() != state.WorkerConfiguration.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.WorkerConfiguration.IsNull() && !plan.WorkerConfiguration.IsUnknown() && !state.WorkerConfiguration.IsNull() && !state.WorkerConfiguration.IsUnknown() {
+ if !strings.EqualFold(plan.WorkerConfiguration.ValueString(), state.WorkerConfiguration.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 756: resources_core.BuildJSON(map[string]string{"appVersion": fmt.Sprintf("\"%s\"", plan.ClusterConfiguration.AppVersion.ValueString())}),
+ 757: resources_core.BuildJSON(map[string]string{"sizingPolicy": fmt.Sprintf("\"%s\"", plan.ControlPlaneConfiguration.SizingPolicy.ValueString()), "sizingDisk": fmt.Sprintf("%d", plan.ControlPlaneConfiguration.SizingDisk.ValueInt64()), "count": fmt.Sprintf("%d", plan.ControlPlaneConfiguration.Count.ValueInt64())}),
+ 758: resources_core.FormatString(plan.WorkerConfiguration),
+ 759: resources_core.BuildJSON(map[string]string{"accessIpListIngress": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.AccessIpListIngress.ValueString()), "needExternalAddressAPI": fmt.Sprintf("%v", plan.AccessConfiguration.NeedExternalAddressAPI.ValueBool()), "accessIpListAPI": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.AccessIpListAPI.ValueString()), "needExternalAddressIngress": fmt.Sprintf("%v", plan.AccessConfiguration.NeedExternalAddressIngress.ValueBool())}),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 150, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8sSthutrvalClusterResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state K8sSthutrvalClusterModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *K8sSthutrvalClusterResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *K8sSthutrvalClusterResource) 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
+}
diff --git a/generated/test/go/150_k8s_sthutrval_cluster_user_resource.go b/generated/test/go/150_k8s_sthutrval_cluster_user_resource.go
new file mode 100644
index 0000000..55faf0e
--- /dev/null
+++ b/generated/test/go/150_k8s_sthutrval_cluster_user_resource.go
@@ -0,0 +1,343 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: k8s_sthutrval_cluster
+// Subresource: user
+
+var _ resource.Resource = &K8sSthutrvalClusterUserResource{}
+
+// K8sSthutrvalClusterUserResource manages a subresource via operations.
+type K8sSthutrvalClusterUserResource struct {
+ client *core.UniversalClient
+}
+
+type K8sSthutrvalClusterUserModel struct {
+ ID types.String `tfsdk:"id"`
+ K8sSthutrvalClusterID types.String `tfsdk:"k8s_sthutrval_cluster_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"`
+ Admins types.String `tfsdk:"admins"`
+}
+
+func NewK8sSthutrvalClusterUserResource() resource.Resource {
+ return &K8sSthutrvalClusterUserResource{}
+}
+
+func (r *K8sSthutrvalClusterUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_k8s_sthutrval_cluster_user"
+}
+
+func (r *K8sSthutrvalClusterUserResource) 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()}},
+ "k8s_sthutrval_cluster_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."},
+ "admins": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *K8sSthutrvalClusterUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan K8sSthutrvalClusterUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.K8sSthutrvalClusterID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "admins": resources_core.FormatString(plan.Admins),
+ }
+ identityCodes := []string{
+ "admins",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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, "user", 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{
+ "admins": resources_core.FormatString(plan.Admins),
+ })
+
+ 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, "create_user", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", 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, "user", 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, "user", 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, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *K8sSthutrvalClusterUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state K8sSthutrvalClusterUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8sSthutrvalClusterUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan K8sSthutrvalClusterUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state K8sSthutrvalClusterUserModel
+ 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.K8sSthutrvalClusterID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "admins": resources_core.FormatString(plan.Admins),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *K8sSthutrvalClusterUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state K8sSthutrvalClusterUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_user" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.K8sSthutrvalClusterID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "admins": resources_core.FormatString(state.Admins),
+ }
+ identityCodes := []string{
+ "admins",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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{
+ "admins": resources_core.FormatString(state.Admins),
+ })
+
+ 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, "delete_user", 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 *K8sSthutrvalClusterUserResource) 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
+}
diff --git a/generated/test/go/151_k8s_openbao_kv_resource.go b/generated/test/go/151_k8s_openbao_kv_resource.go
new file mode 100644
index 0000000..df5550e
--- /dev/null
+++ b/generated/test/go/151_k8s_openbao_kv_resource.go
@@ -0,0 +1,343 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: k8s_openbao
+// Subresource: kv
+
+var _ resource.Resource = &K8sOpenbaoKvResource{}
+
+// K8sOpenbaoKvResource manages a subresource via operations.
+type K8sOpenbaoKvResource struct {
+ client *core.UniversalClient
+}
+
+type K8sOpenbaoKvModel struct {
+ ID types.String `tfsdk:"id"`
+ K8sOpenbaoID types.String `tfsdk:"k8s_openbao_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"`
+ KvName types.String `tfsdk:"kv_name"`
+}
+
+func NewK8sOpenbaoKvResource() resource.Resource {
+ return &K8sOpenbaoKvResource{}
+}
+
+func (r *K8sOpenbaoKvResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_k8s_openbao_kv"
+}
+
+func (r *K8sOpenbaoKvResource) 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()}},
+ "k8s_openbao_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."},
+ "kv_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("kvname-yyz2zm23"), MarkdownDescription: "Будет создан `v2 тип движка`
Уникальное в рамках услуги Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *K8sOpenbaoKvResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan K8sOpenbaoKvModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.K8sOpenbaoID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "kvName": resources_core.FormatString(plan.KvName),
+ }
+ identityCodes := []string{
+ "kvName",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("kv", 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, "kv", 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{
+ "kvName": resources_core.FormatString(plan.KvName),
+ })
+
+ 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, "create_kv", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_kv", 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, "kv", 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, "kv", 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, "kv", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *K8sOpenbaoKvResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state K8sOpenbaoKvModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8sOpenbaoKvResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan K8sOpenbaoKvModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state K8sOpenbaoKvModel
+ 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.K8sOpenbaoID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "kvName": resources_core.FormatString(plan.KvName),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "kv", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *K8sOpenbaoKvResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state K8sOpenbaoKvModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_kv" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.K8sOpenbaoID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "kvName": resources_core.FormatString(state.KvName),
+ }
+ identityCodes := []string{
+ "kvName",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("kv", 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{
+ "kvName": resources_core.FormatString(state.KvName),
+ })
+
+ 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, "delete_kv", 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 *K8sOpenbaoKvResource) 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
+}
diff --git a/generated/test/go/151_k8s_openbao_policy_resource.go b/generated/test/go/151_k8s_openbao_policy_resource.go
new file mode 100644
index 0000000..ef23e6d
--- /dev/null
+++ b/generated/test/go/151_k8s_openbao_policy_resource.go
@@ -0,0 +1,355 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: k8s_openbao
+// Subresource: policy
+
+var _ resource.Resource = &K8sOpenbaoPolicyResource{}
+
+// K8sOpenbaoPolicyResource manages a subresource via operations.
+type K8sOpenbaoPolicyResource struct {
+ client *core.UniversalClient
+}
+
+type K8sOpenbaoPolicyModel struct {
+ ID types.String `tfsdk:"id"`
+ K8sOpenbaoID types.String `tfsdk:"k8s_openbao_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"`
+ PolicyName types.String `tfsdk:"policy_name"`
+ PolicySettings types.String `tfsdk:"policy_settings"`
+}
+
+func NewK8sOpenbaoPolicyResource() resource.Resource {
+ return &K8sOpenbaoPolicyResource{}
+}
+
+func (r *K8sOpenbaoPolicyResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_k8s_openbao_policy"
+}
+
+func (r *K8sOpenbaoPolicyResource) 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()}},
+ "k8s_openbao_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."},
+ "policy_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("policy-r2ksc6sl"), MarkdownDescription: "Сделано для дальнейшего удобного использования Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "policy_settings": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *K8sOpenbaoPolicyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan K8sOpenbaoPolicyModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if plan.PolicySettings.IsNull() || plan.PolicySettings.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "policy_settings is required.")
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.K8sOpenbaoID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "policyName": resources_core.FormatString(plan.PolicyName),
+ "policySettings": resources_core.FormatString(plan.PolicySettings),
+ }
+ identityCodes := []string{
+ "policyName",
+ "policySettings",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("policy", 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, "policy", 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{
+ "policySettings": resources_core.FormatString(plan.PolicySettings),
+ "policyName": resources_core.FormatString(plan.PolicyName),
+ })
+
+ 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, "create_policy", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_policy", 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, "policy", 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, "policy", 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, "policy", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *K8sOpenbaoPolicyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state K8sOpenbaoPolicyModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8sOpenbaoPolicyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan K8sOpenbaoPolicyModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state K8sOpenbaoPolicyModel
+ 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.K8sOpenbaoID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "policyName": resources_core.FormatString(plan.PolicyName),
+ "policySettings": resources_core.FormatString(plan.PolicySettings),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "policy", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *K8sOpenbaoPolicyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state K8sOpenbaoPolicyModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_policy" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.K8sOpenbaoID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "policyName": resources_core.FormatString(state.PolicyName),
+ "policySettings": resources_core.FormatString(state.PolicySettings),
+ }
+ identityCodes := []string{
+ "policyName",
+ "policySettings",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("policy", 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{
+ "policyName": resources_core.FormatString(state.PolicyName),
+ })
+
+ 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, "delete_policy", 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 *K8sOpenbaoPolicyResource) 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
+}
diff --git a/generated/test/go/151_k8s_openbao_resource.go b/generated/test/go/151_k8s_openbao_resource.go
new file mode 100644
index 0000000..92dcaf2
--- /dev/null
+++ b/generated/test/go/151_k8s_openbao_resource.go
@@ -0,0 +1,468 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: k8s_openbao
+// Service ID: 151
+
+var _ resource.Resource = &K8sOpenbaoResource{}
+var _ resource.ResourceWithModifyPlan = &K8sOpenbaoResource{}
+var _ resource.ResourceWithImportState = &K8sOpenbaoResource{}
+
+type K8sOpenbaoResource struct {
+ client *core.UniversalClient
+}
+
+type K8sOpenbaoModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AppVersion types.String `tfsdk:"app_version"`
+ Domain types.String `tfsdk:"domain"`
+ ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"`
+ ResourceDisk types.Int64 `tfsdk:"resource_disk"`
+ ResourceInstances types.Int64 `tfsdk:"resource_instances"`
+ ResourceMemory types.Int64 `tfsdk:"resource_memory"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewK8sOpenbaoResource() resource.Resource {
+ return &K8sOpenbaoResource{}
+}
+
+func (r *K8sOpenbaoResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_k8s_openbao"
+}
+
+func (r *K8sOpenbaoResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "app_version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("2.5.2"), MarkdownDescription: "Выбрать из списка"},
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("bao-uqvjbvor"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."},
+ "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Пример: `500`"},
+ "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Пример: `10`"},
+ "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(3), MarkdownDescription: "Пример: `3`"},
+ "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Пример: `2048`"},
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes в котором будет развёрнуто приложение Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *K8sOpenbaoResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *K8sOpenbaoModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *K8sOpenbaoModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan K8sOpenbaoModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.ResourceRealm.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+ if config.ResourceRealm.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 663: resources_core.FormatString(config.ResourceRealm),
+ 664: resources_core.FormatInt64(config.ResourceCPU),
+ 665: resources_core.FormatInt64(config.ResourceInstances),
+ 666: resources_core.FormatInt64(config.ResourceMemory),
+ 667: resources_core.FormatInt64(config.ResourceDisk),
+ 668: resources_core.FormatString(config.AppVersion),
+ 669: resources_core.FormatString(config.Domain),
+ }
+ desiredDomain := ""
+ if !config.Domain.IsNull() && !config.Domain.IsUnknown() {
+ desiredDomain = config.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 151, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *K8sOpenbaoResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data K8sOpenbaoModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ if !data.Domain.IsNull() && !data.Domain.IsUnknown() {
+ desiredDomain = data.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 151, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 663: resources_core.FormatString(data.ResourceRealm),
+ 664: resources_core.FormatInt64(data.ResourceCPU),
+ 665: resources_core.FormatInt64(data.ResourceInstances),
+ 666: resources_core.FormatInt64(data.ResourceMemory),
+ 667: resources_core.FormatInt64(data.ResourceDisk),
+ 668: resources_core.FormatString(data.AppVersion),
+ 669: resources_core.FormatString(data.Domain),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 151, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 151, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "appVersion", Field: "AppVersion", Type: "string"},
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8sOpenbaoResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state K8sOpenbaoModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 151, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "appVersion", Field: "AppVersion", Type: "string"},
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *K8sOpenbaoResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan K8sOpenbaoModel
+ var state K8sOpenbaoModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.ResourceInstances.IsNull() != state.ResourceInstances.IsNull() || plan.ResourceInstances.IsUnknown() != state.ResourceInstances.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() {
+ if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() {
+ if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() {
+ if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceDisk.IsNull() != state.ResourceDisk.IsNull() || plan.ResourceDisk.IsUnknown() != state.ResourceDisk.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() {
+ if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.AppVersion.IsNull() != state.AppVersion.IsNull() || plan.AppVersion.IsUnknown() != state.AppVersion.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.AppVersion.IsNull() && !plan.AppVersion.IsUnknown() && !state.AppVersion.IsNull() && !state.AppVersion.IsUnknown() {
+ if !strings.EqualFold(plan.AppVersion.ValueString(), state.AppVersion.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 670: resources_core.FormatInt64(plan.ResourceInstances),
+ 671: resources_core.FormatInt64(plan.ResourceCPU),
+ 672: resources_core.FormatInt64(plan.ResourceMemory),
+ 673: resources_core.FormatInt64(plan.ResourceDisk),
+ 674: resources_core.FormatString(plan.AppVersion),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 151, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "appVersion", Field: "AppVersion", Type: "string"},
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8sOpenbaoResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state K8sOpenbaoModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *K8sOpenbaoResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *K8sOpenbaoResource) 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
+}
diff --git a/generated/test/go/151_k8s_openbao_secrets_resource.go b/generated/test/go/151_k8s_openbao_secrets_resource.go
new file mode 100644
index 0000000..161164e
--- /dev/null
+++ b/generated/test/go/151_k8s_openbao_secrets_resource.go
@@ -0,0 +1,368 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: k8s_openbao
+// Subresource: secrets
+
+var _ resource.Resource = &K8sOpenbaoSecretsResource{}
+
+// K8sOpenbaoSecretsResource manages a subresource via operations.
+type K8sOpenbaoSecretsResource struct {
+ client *core.UniversalClient
+}
+
+type K8sOpenbaoSecretsModel struct {
+ ID types.String `tfsdk:"id"`
+ K8sOpenbaoID types.String `tfsdk:"k8s_openbao_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"`
+ KvName types.String `tfsdk:"kv_name"`
+ SecretData types.String `tfsdk:"secret_data"`
+ SecretPath types.String `tfsdk:"secret_path"`
+}
+
+func NewK8sOpenbaoSecretsResource() resource.Resource {
+ return &K8sOpenbaoSecretsResource{}
+}
+
+func (r *K8sOpenbaoSecretsResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_k8s_openbao_secrets"
+}
+
+func (r *K8sOpenbaoSecretsResource) 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()}},
+ "k8s_openbao_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."},
+ "kv_name": schema.StringAttribute{Required: true, MarkdownDescription: "Выберите из списка Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "secret_data": schema.StringAttribute{Required: true, MarkdownDescription: "Хотя бы один секрет необходимо добавить Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "secret_path": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("path/to/mysecret"), MarkdownDescription: "Пример: `path` или `road/to/path` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *K8sOpenbaoSecretsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan K8sOpenbaoSecretsModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if plan.KvName.IsNull() || plan.KvName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "kv_name is required.")
+ return
+ }
+ if plan.SecretData.IsNull() || plan.SecretData.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "secret_data is required.")
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.K8sOpenbaoID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "kvName": resources_core.FormatString(plan.KvName),
+ "secretData": resources_core.FormatString(plan.SecretData),
+ "secretPath": resources_core.FormatString(plan.SecretPath),
+ }
+ identityCodes := []string{
+ "kvName",
+ "secretData",
+ "secretPath",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("secrets", 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, "secrets", 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{
+ "kvName": resources_core.FormatString(plan.KvName),
+ "secretPath": resources_core.FormatString(plan.SecretPath),
+ "secretData": resources_core.FormatString(plan.SecretData),
+ })
+
+ 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, "create_secrets", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_secrets", 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, "secrets", 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, "secrets", 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, "secrets", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *K8sOpenbaoSecretsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state K8sOpenbaoSecretsModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8sOpenbaoSecretsResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan K8sOpenbaoSecretsModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state K8sOpenbaoSecretsModel
+ 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.K8sOpenbaoID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "kvName": resources_core.FormatString(plan.KvName),
+ "secretData": resources_core.FormatString(plan.SecretData),
+ "secretPath": resources_core.FormatString(plan.SecretPath),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "secrets", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *K8sOpenbaoSecretsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state K8sOpenbaoSecretsModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_secrets" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.K8sOpenbaoID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "kvName": resources_core.FormatString(state.KvName),
+ "secretData": resources_core.FormatString(state.SecretData),
+ "secretPath": resources_core.FormatString(state.SecretPath),
+ }
+ identityCodes := []string{
+ "kvName",
+ "secretData",
+ "secretPath",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("secrets", 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{
+ "kvName": resources_core.FormatString(state.KvName),
+ "secretPath": resources_core.FormatString(state.SecretPath),
+ })
+
+ 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, "delete_secrets", 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 *K8sOpenbaoSecretsResource) 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
+}
diff --git a/generated/test/go/163_llm_ai_resource.go b/generated/test/go/163_llm_ai_resource.go
new file mode 100644
index 0000000..1037328
--- /dev/null
+++ b/generated/test/go/163_llm_ai_resource.go
@@ -0,0 +1,345 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: llm_ai
+// Service ID: 163
+
+var _ resource.Resource = &LlmAiResource{}
+var _ resource.ResourceWithModifyPlan = &LlmAiResource{}
+var _ resource.ResourceWithImportState = &LlmAiResource{}
+
+type LlmAiResource struct {
+ client *core.UniversalClient
+}
+
+type LlmAiModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewLlmAiResource() resource.Resource {
+ return &LlmAiResource{}
+}
+
+func (r *LlmAiResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_llm_ai"
+}
+
+func (r *LlmAiResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("llmAi.aillm.ru"), MarkdownDescription: "Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *LlmAiResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *LlmAiModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *LlmAiModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan LlmAiModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 826: resources_core.FormatString(config.ResourceRealm),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 163, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *LlmAiResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data LlmAiModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 163, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 826: resources_core.FormatString(data.ResourceRealm),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 163, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 163, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *LlmAiResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state LlmAiModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 163, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *LlmAiResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan LlmAiModel
+ var state LlmAiModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 163, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *LlmAiResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state LlmAiModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *LlmAiResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *LlmAiResource) 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
+}
diff --git a/generated/test/go/163_llm_ai_secrets_resource.go b/generated/test/go/163_llm_ai_secrets_resource.go
new file mode 100644
index 0000000..063f1fb
--- /dev/null
+++ b/generated/test/go/163_llm_ai_secrets_resource.go
@@ -0,0 +1,389 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: llm_ai
+// Subresource: secrets
+
+var _ resource.Resource = &LlmAiSecretsResource{}
+
+// LlmAiSecretsResource manages a subresource via operations.
+type LlmAiSecretsResource struct {
+ client *core.UniversalClient
+}
+
+type LlmAiSecretsModel struct {
+ ID types.String `tfsdk:"id"`
+ LlmAiID types.String `tfsdk:"llm_ai_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"`
+ KeyName types.String `tfsdk:"key_name"`
+ Models types.String `tfsdk:"models"`
+ RpmLimit types.Int64 `tfsdk:"rpm_limit"`
+ TeamName types.String `tfsdk:"team_name"`
+ TpmLimit types.Int64 `tfsdk:"tpm_limit"`
+}
+
+func NewLlmAiSecretsResource() resource.Resource {
+ return &LlmAiSecretsResource{}
+}
+
+func (r *LlmAiSecretsResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_llm_ai_secrets"
+}
+
+func (r *LlmAiSecretsResource) 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()}},
+ "llm_ai_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."},
+ "key_name": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "models": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "rpm_limit": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(60), MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.Int64{int64planmodifier.RequiresReplace()}},
+ "team_name": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "tpm_limit": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100000), MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.Int64{int64planmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *LlmAiSecretsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan LlmAiSecretsModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if plan.KeyName.IsNull() || plan.KeyName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "key_name is required.")
+ return
+ }
+ if plan.Models.IsNull() || plan.Models.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "models is required.")
+ return
+ }
+ if plan.TeamName.IsNull() || plan.TeamName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "team_name is required.")
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.LlmAiID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "keyName": resources_core.FormatString(plan.KeyName),
+ "models": resources_core.FormatString(plan.Models),
+ "rpmLimit": resources_core.FormatInt64(plan.RpmLimit),
+ "teamName": resources_core.FormatString(plan.TeamName),
+ "tpmLimit": resources_core.FormatInt64(plan.TpmLimit),
+ }
+ identityCodes := []string{
+ "keyName",
+ "models",
+ "rpmLimit",
+ "teamName",
+ "tpmLimit",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("secrets", 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, "secrets", 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{
+ "models": resources_core.FormatString(plan.Models),
+ "rpmLimit": resources_core.FormatInt64(plan.RpmLimit),
+ "tpmLimit": resources_core.FormatInt64(plan.TpmLimit),
+ "keyName": resources_core.FormatString(plan.KeyName),
+ "teamName": resources_core.FormatString(plan.TeamName),
+ })
+
+ 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, "create_secrets", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_secrets", 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, "secrets", 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, "secrets", 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, "secrets", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *LlmAiSecretsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state LlmAiSecretsModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *LlmAiSecretsResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan LlmAiSecretsModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state LlmAiSecretsModel
+ 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.LlmAiID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "keyName": resources_core.FormatString(plan.KeyName),
+ "models": resources_core.FormatString(plan.Models),
+ "rpmLimit": resources_core.FormatInt64(plan.RpmLimit),
+ "teamName": resources_core.FormatString(plan.TeamName),
+ "tpmLimit": resources_core.FormatInt64(plan.TpmLimit),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "secrets", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *LlmAiSecretsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state LlmAiSecretsModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_secrets" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.LlmAiID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "keyName": resources_core.FormatString(state.KeyName),
+ "models": resources_core.FormatString(state.Models),
+ "rpmLimit": resources_core.FormatInt64(state.RpmLimit),
+ "teamName": resources_core.FormatString(state.TeamName),
+ "tpmLimit": resources_core.FormatInt64(state.TpmLimit),
+ }
+ identityCodes := []string{
+ "keyName",
+ "models",
+ "rpmLimit",
+ "teamName",
+ "tpmLimit",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("secrets", 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{
+ "keyName": resources_core.FormatString(state.KeyName),
+ "teamName": resources_core.FormatString(state.TeamName),
+ })
+
+ 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, "delete_secrets", 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 *LlmAiSecretsResource) 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
+}
diff --git a/generated/test/go/163_llm_ai_team_resource.go b/generated/test/go/163_llm_ai_team_resource.go
new file mode 100644
index 0000000..e411362
--- /dev/null
+++ b/generated/test/go/163_llm_ai_team_resource.go
@@ -0,0 +1,365 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: llm_ai
+// Subresource: team
+
+var _ resource.Resource = &LlmAiTeamResource{}
+
+// LlmAiTeamResource manages a subresource via operations.
+type LlmAiTeamResource struct {
+ client *core.UniversalClient
+}
+
+type LlmAiTeamModel struct {
+ ID types.String `tfsdk:"id"`
+ LlmAiID types.String `tfsdk:"llm_ai_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"`
+ Models types.String `tfsdk:"models"`
+ RpmLimit types.Int64 `tfsdk:"rpm_limit"`
+ TeamName types.String `tfsdk:"team_name"`
+ TpmLimit types.Int64 `tfsdk:"tpm_limit"`
+}
+
+func NewLlmAiTeamResource() resource.Resource {
+ return &LlmAiTeamResource{}
+}
+
+func (r *LlmAiTeamResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_llm_ai_team"
+}
+
+func (r *LlmAiTeamResource) 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()}},
+ "llm_ai_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."},
+ "models": schema.StringAttribute{Required: true},
+ "rpm_limit": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(60)},
+ "team_name": schema.StringAttribute{Required: true, MarkdownDescription: "Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "tpm_limit": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100000)},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *LlmAiTeamResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan LlmAiTeamModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if plan.Models.IsNull() || plan.Models.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "models is required.")
+ return
+ }
+ if plan.TeamName.IsNull() || plan.TeamName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "team_name is required.")
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.LlmAiID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "teamName": resources_core.FormatString(plan.TeamName),
+ }
+ identityCodes := []string{
+ "teamName",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("team", 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, "team", 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{
+ "teamName": resources_core.FormatString(plan.TeamName),
+ "models": resources_core.FormatString(plan.Models),
+ "rpmLimit": resources_core.FormatInt64(plan.RpmLimit),
+ "tpmLimit": resources_core.FormatInt64(plan.TpmLimit),
+ })
+
+ 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, "create_team", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_team", 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, "team", 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, "team", 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, "team", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *LlmAiTeamResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state LlmAiTeamModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *LlmAiTeamResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan LlmAiTeamModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "modify_team" == "" {
+ var state LlmAiTeamModel
+ 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.LlmAiID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{
+ "teamName": resources_core.FormatString(plan.TeamName),
+ "models": resources_core.FormatString(plan.Models),
+ "rpmLimit": resources_core.FormatInt64(plan.RpmLimit),
+ "tpmLimit": resources_core.FormatInt64(plan.TpmLimit),
+ })
+
+ idParams := map[string]string{
+ "teamName": resources_core.FormatString(plan.TeamName),
+ }
+
+ 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, "modify_team", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "team", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *LlmAiTeamResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state LlmAiTeamModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_team" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.LlmAiID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "teamName": resources_core.FormatString(state.TeamName),
+ }
+ identityCodes := []string{
+ "teamName",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("team", 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{
+ "teamName": resources_core.FormatString(state.TeamName),
+ })
+
+ 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, "delete_team", 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 *LlmAiTeamResource) 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
+}
diff --git a/generated/test/go/19_vc_org_resource.go b/generated/test/go/19_vc_org_resource.go
new file mode 100644
index 0000000..90d238a
--- /dev/null
+++ b/generated/test/go/19_vc_org_resource.go
@@ -0,0 +1,391 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_org
+// Service ID: 19
+
+var _ resource.Resource = &VcOrgResource{}
+var _ resource.ResourceWithModifyPlan = &VcOrgResource{}
+var _ resource.ResourceWithImportState = &VcOrgResource{}
+
+type VcOrgResource struct {
+ client *core.UniversalClient
+}
+
+type VcOrgModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ OrganizationType types.String `tfsdk:"organization_type"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ VIPConfigure types.String `tfsdk:"v_i_p_configure"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVcOrgResource() resource.Resource {
+ return &VcOrgResource{}
+}
+
+func (r *VcOrgResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_org"
+}
+
+func (r *VcOrgResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "organization_type": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("iaas"), MarkdownDescription: "Доступ к организации предоставляется только при выборе `iaas` Не изменяется после создания."},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("sandbox.nubes.ru"), MarkdownDescription: "Облако в котором будет развёрнут тенант Не изменяется после создания."},
+ "v_i_p_configure": schema.StringAttribute{Required: true},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcOrgResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VcOrgModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VcOrgModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VcOrgModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.OrganizationType.IsNull() && !plan.OrganizationType.IsUnknown() && !state.OrganizationType.IsNull() && !state.OrganizationType.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.OrganizationType.ValueString(), state.OrganizationType.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить organization_type", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.VIPConfigure.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "v_i_p_configure is required.")
+ return
+ }
+ if config.VIPConfigure.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 418: resources_core.FormatString(config.ResourceRealm),
+ 556: resources_core.FormatString(config.OrganizationType),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 19, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VcOrgResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VcOrgModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.VIPConfigure.IsNull() || data.VIPConfigure.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "v_i_p_configure is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 19, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 418: resources_core.FormatString(data.ResourceRealm),
+ 556: resources_core.FormatString(data.OrganizationType),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 19, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 19, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "organizationType", Field: "OrganizationType", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "vIPConfigure", Field: "VIPConfigure", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcOrgResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcOrgModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 19, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "organizationType", Field: "OrganizationType", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "vIPConfigure", Field: "VIPConfigure", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VcOrgResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcOrgModel
+ var state VcOrgModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.VIPConfigure.IsNull() != state.VIPConfigure.IsNull() || plan.VIPConfigure.IsUnknown() != state.VIPConfigure.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.VIPConfigure.IsNull() && !plan.VIPConfigure.IsUnknown() && !state.VIPConfigure.IsNull() && !state.VIPConfigure.IsUnknown() {
+ if !strings.EqualFold(plan.VIPConfigure.ValueString(), state.VIPConfigure.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 662: resources_core.FormatString(plan.VIPConfigure),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 19, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "organizationType", Field: "OrganizationType", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "vIPConfigure", Field: "VIPConfigure", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcOrgResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcOrgModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VcOrgResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VcOrgResource) 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
+}
diff --git a/generated/test/go/19_vc_org_user_resource.go b/generated/test/go/19_vc_org_user_resource.go
new file mode 100644
index 0000000..b18fcdd
--- /dev/null
+++ b/generated/test/go/19_vc_org_user_resource.go
@@ -0,0 +1,343 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_org
+// Subresource: user
+
+var _ resource.Resource = &VcOrgUserResource{}
+
+// VcOrgUserResource manages a subresource via operations.
+type VcOrgUserResource struct {
+ client *core.UniversalClient
+}
+
+type VcOrgUserModel struct {
+ ID types.String `tfsdk:"id"`
+ VcOrgID types.String `tfsdk:"vc_org_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"`
+ Admins types.String `tfsdk:"admins"`
+}
+
+func NewVcOrgUserResource() resource.Resource {
+ return &VcOrgUserResource{}
+}
+
+func (r *VcOrgUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_org_user"
+}
+
+func (r *VcOrgUserResource) 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()}},
+ "vc_org_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."},
+ "admins": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcOrgUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan VcOrgUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.VcOrgID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "admins": resources_core.FormatString(plan.Admins),
+ }
+ identityCodes := []string{
+ "admins",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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, "user", 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{
+ "admins": resources_core.FormatString(plan.Admins),
+ })
+
+ 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, "create_user", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", 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, "user", 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, "user", 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, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *VcOrgUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcOrgUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcOrgUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcOrgUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state VcOrgUserModel
+ 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.VcOrgID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "admins": resources_core.FormatString(plan.Admins),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *VcOrgUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcOrgUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_user" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.VcOrgID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "admins": resources_core.FormatString(state.Admins),
+ }
+ identityCodes := []string{
+ "admins",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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{
+ "admins": resources_core.FormatString(state.Admins),
+ })
+
+ 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, "delete_user", 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 *VcOrgUserResource) 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
+}
diff --git a/generated/test/go/1_dummy_resource.go b/generated/test/go/1_dummy_resource.go
new file mode 100644
index 0000000..a00d9cc
--- /dev/null
+++ b/generated/test/go/1_dummy_resource.go
@@ -0,0 +1,582 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: dummy
+// Service ID: 1
+
+var _ resource.Resource = &DummyResource{}
+var _ resource.ResourceWithModifyPlan = &DummyResource{}
+var _ resource.ResourceWithImportState = &DummyResource{}
+
+type DummyResource struct {
+ client *core.UniversalClient
+}
+
+// DummyMapExampleModel — вложенная модель для map-fixed параметра mapExample.
+type DummyMapExampleModel struct {
+ Subparam2 types.String `tfsdk:"subparam2" json:"subparam2"`
+ Subparam1 types.String `tfsdk:"subparam1" json:"subparam1"`
+ Secret types.String `tfsdk:"secret" json:"secret"`
+}
+
+// DummyMapFixedModel — вложенная модель для map-fixed параметра mapFixed.
+type DummyMapFixedModel struct {
+ Bol1 types.Bool `tfsdk:"bol1" json:"bol1"`
+ MinStr1 types.String `tfsdk:"min_str1" json:"minStr1"`
+ Param1 types.Int64 `tfsdk:"param1" json:"param1"`
+ Param2 types.String `tfsdk:"param2" json:"param2"`
+ Param3 types.Int64 `tfsdk:"param3" json:"param3"`
+}
+
+type DummyModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // git_revision: при изменении вызывает redeploy вместо modify.
+ // Опциональное поле — если не задано, modify работает как обычно.
+ GitRevision types.String `tfsdk:"git_revision"`
+ Arr types.String `tfsdk:"arr"`
+ ArrayMapFixedExample types.String `tfsdk:"array_map_fixed_example"`
+ Bodymessage types.String `tfsdk:"bodymessage"`
+ DurationMs types.Int64 `tfsdk:"duration_ms"`
+ FailAtStart types.Bool `tfsdk:"fail_at_start"`
+ FailInProgress types.Bool `tfsdk:"fail_in_progress"`
+ JsonExample types.String `tfsdk:"json_example"`
+ MapExample *DummyMapExampleModel `tfsdk:"map_example"`
+ MapFixed *DummyMapFixedModel `tfsdk:"map_fixed"`
+ NestedRefExample types.String `tfsdk:"nested_ref_example"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ WhereFail types.Int64 `tfsdk:"where_fail"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewDummyResource() resource.Resource {
+ return &DummyResource{}
+}
+
+func (r *DummyResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_dummy"
+}
+
+func (r *DummyResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "arr": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания."},
+ "array_map_fixed_example": schema.StringAttribute{Optional: true, MarkdownDescription: "Array of map-fixed Не изменяется после создания."},
+ "bodymessage": schema.StringAttribute{Optional: true, MarkdownDescription: "Дата, уходящая в Vault"},
+ "duration_ms": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "перед завершением подождать (sleep) столько миллисекунд"},
+ "fail_at_start": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "если true, завершить джоб, ничего не записывая в cmdb"},
+ "fail_in_progress": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "если true, корректно начать выполнение джоба, но в процессе завершить аварийно, ничего не записывая в cmdb"},
+ "json_example": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Для тестирования интерфейса", PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()}},
+ "map_example": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "subparam2": schema.StringAttribute{Optional: true},
+ "subparam1": schema.StringAttribute{Optional: true, MarkdownDescription: "это для администратора"},
+ "secret": schema.StringAttribute{Optional: true, MarkdownDescription: "это секрет"},
+ }},
+ "map_fixed": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "bol1": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Булевский, обязательный"},
+ "min_str1": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("abracadabra"), MarkdownDescription: "не меньше 10 символов"},
+ "param1": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Integer, secret"},
+ "param2": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("1"), MarkdownDescription: "Дескрипшен"},
+ "param3": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "целое положительное"},
+ }},
+ "nested_ref_example": schema.StringAttribute{Optional: true, MarkdownDescription: "Не изменяется после создания."},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("dummy"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ "where_fail": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "принимает 1,2,3. Работает с failInProgress. 1 - упасть в prepare. 2 - упасть внутри блока Data fill. 3 - упасть после vault write"},
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // При изменении git_revision вызывается redeploy вместо modify.
+ // Если не задано — modify работает без изменений.
+ "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *DummyResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *DummyModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *DummyModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan DummyModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.Arr.IsNull() && !plan.Arr.IsUnknown() && !state.Arr.IsNull() && !state.Arr.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Arr.ValueString(), state.Arr.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить arr", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ArrayMapFixedExample.IsNull() && !plan.ArrayMapFixedExample.IsUnknown() && !state.ArrayMapFixedExample.IsNull() && !state.ArrayMapFixedExample.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ArrayMapFixedExample.ValueString(), state.ArrayMapFixedExample.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить array_map_fixed_example", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.NestedRefExample.IsNull() && !plan.NestedRefExample.IsUnknown() && !state.NestedRefExample.IsNull() && !state.NestedRefExample.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.NestedRefExample.ValueString(), state.NestedRefExample.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить nested_ref_example", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.Arr.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "arr is required.")
+ return
+ }
+ if config.Arr.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 198: resources_core.FormatInt64(config.DurationMs),
+ 199: resources_core.FormatBool(config.FailAtStart),
+ 200: resources_core.FormatBool(config.FailInProgress),
+ 201: resources_core.FormatInt64(config.WhereFail),
+ 242: resources_core.FormatString(config.ResourceRealm),
+ 286: resources_core.FormatString(config.Bodymessage),
+ 321: resources_core.BuildJSON(map[string]string{"subparam2": fmt.Sprintf("\"%s\"", config.MapExample.Subparam2.ValueString()), "subparam1": fmt.Sprintf("\"%s\"", config.MapExample.Subparam1.ValueString()), "secret": fmt.Sprintf("\"%s\"", config.MapExample.Secret.ValueString())}),
+ 322: resources_core.FormatString(config.JsonExample),
+ 396: resources_core.FormatString(config.NestedRefExample),
+ 647: resources_core.BuildJSON(map[string]string{"bol1": fmt.Sprintf("%v", config.MapFixed.Bol1.ValueBool()), "minStr1": fmt.Sprintf("\"%s\"", config.MapFixed.MinStr1.ValueString()), "param1": fmt.Sprintf("%d", config.MapFixed.Param1.ValueInt64()), "param2": fmt.Sprintf("\"%s\"", config.MapFixed.Param2.ValueString()), "param3": fmt.Sprintf("%d", config.MapFixed.Param3.ValueInt64())}),
+ 654: resources_core.FormatString(config.ArrayMapFixedExample),
+ 863: resources_core.FormatString(config.Arr),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 1, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *DummyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data DummyModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.Arr.IsNull() || data.Arr.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "arr is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 1, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 198: resources_core.FormatInt64(data.DurationMs),
+ 199: resources_core.FormatBool(data.FailAtStart),
+ 200: resources_core.FormatBool(data.FailInProgress),
+ 201: resources_core.FormatInt64(data.WhereFail),
+ 242: resources_core.FormatString(data.ResourceRealm),
+ 286: resources_core.FormatString(data.Bodymessage),
+ 321: resources_core.BuildJSON(map[string]string{"subparam2": fmt.Sprintf("\"%s\"", data.MapExample.Subparam2.ValueString()), "subparam1": fmt.Sprintf("\"%s\"", data.MapExample.Subparam1.ValueString()), "secret": fmt.Sprintf("\"%s\"", data.MapExample.Secret.ValueString())}),
+ 322: resources_core.FormatString(data.JsonExample),
+ 396: resources_core.FormatString(data.NestedRefExample),
+ 647: resources_core.BuildJSON(map[string]string{"bol1": fmt.Sprintf("%v", data.MapFixed.Bol1.ValueBool()), "minStr1": fmt.Sprintf("\"%s\"", data.MapFixed.MinStr1.ValueString()), "param1": fmt.Sprintf("%d", data.MapFixed.Param1.ValueInt64()), "param2": fmt.Sprintf("\"%s\"", data.MapFixed.Param2.ValueString()), "param3": fmt.Sprintf("%d", data.MapFixed.Param3.ValueInt64())}),
+ 654: resources_core.FormatString(data.ArrayMapFixedExample),
+ 863: resources_core.FormatString(data.Arr),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 1, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 1, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "arr", Field: "Arr", Type: "string"},
+ {Code: "arrayMapFixedExample", Field: "ArrayMapFixedExample", Type: "array-map-fixed"},
+ {Code: "bodymessage", Field: "Bodymessage", Type: "string"},
+ {Code: "durationMs", Field: "DurationMs", Type: "int64"},
+ {Code: "failAtStart", Field: "FailAtStart", Type: "bool"},
+ {Code: "failInProgress", Field: "FailInProgress", Type: "bool"},
+ {Code: "jsonExample", Field: "JsonExample", Type: "string"},
+ {Code: "mapExample", Field: "MapExample", Type: "string"},
+ {Code: "mapFixed", Field: "MapFixed", Type: "map-fixed"},
+ {Code: "nestedRefExample", Field: "NestedRefExample", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "whereFail", Field: "WhereFail", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *DummyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state DummyModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 1, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "arr", Field: "Arr", Type: "string"},
+ {Code: "arrayMapFixedExample", Field: "ArrayMapFixedExample", Type: "array-map-fixed"},
+ {Code: "bodymessage", Field: "Bodymessage", Type: "string"},
+ {Code: "durationMs", Field: "DurationMs", Type: "int64"},
+ {Code: "failAtStart", Field: "FailAtStart", Type: "bool"},
+ {Code: "failInProgress", Field: "FailInProgress", Type: "bool"},
+ {Code: "jsonExample", Field: "JsonExample", Type: "string"},
+ {Code: "mapExample", Field: "MapExample", Type: "string"},
+ {Code: "mapFixed", Field: "MapFixed", Type: "map-fixed"},
+ {Code: "nestedRefExample", Field: "NestedRefExample", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "whereFail", Field: "WhereFail", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *DummyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan DummyModel
+ var state DummyModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.DurationMs.IsNull() != state.DurationMs.IsNull() || plan.DurationMs.IsUnknown() != state.DurationMs.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.DurationMs.IsNull() && !plan.DurationMs.IsUnknown() && !state.DurationMs.IsNull() && !state.DurationMs.IsUnknown() {
+ if plan.DurationMs.ValueInt64() != state.DurationMs.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.FailAtStart.IsNull() != state.FailAtStart.IsNull() || plan.FailAtStart.IsUnknown() != state.FailAtStart.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.FailAtStart.IsNull() && !plan.FailAtStart.IsUnknown() && !state.FailAtStart.IsNull() && !state.FailAtStart.IsUnknown() {
+ if plan.FailAtStart.ValueBool() != state.FailAtStart.ValueBool() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.FailInProgress.IsNull() != state.FailInProgress.IsNull() || plan.FailInProgress.IsUnknown() != state.FailInProgress.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.FailInProgress.IsNull() && !plan.FailInProgress.IsUnknown() && !state.FailInProgress.IsNull() && !state.FailInProgress.IsUnknown() {
+ if plan.FailInProgress.ValueBool() != state.FailInProgress.ValueBool() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.WhereFail.IsNull() != state.WhereFail.IsNull() || plan.WhereFail.IsUnknown() != state.WhereFail.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.WhereFail.IsNull() && !plan.WhereFail.IsUnknown() && !state.WhereFail.IsNull() && !state.WhereFail.IsUnknown() {
+ if plan.WhereFail.ValueInt64() != state.WhereFail.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.Bodymessage.IsNull() != state.Bodymessage.IsNull() || plan.Bodymessage.IsUnknown() != state.Bodymessage.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.Bodymessage.IsNull() && !plan.Bodymessage.IsUnknown() && !state.Bodymessage.IsNull() && !state.Bodymessage.IsUnknown() {
+ if !strings.EqualFold(plan.Bodymessage.ValueString(), state.Bodymessage.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.JsonExample.IsNull() != state.JsonExample.IsNull() || plan.JsonExample.IsUnknown() != state.JsonExample.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.JsonExample.IsNull() && !plan.JsonExample.IsUnknown() && !state.JsonExample.IsNull() && !state.JsonExample.IsUnknown() {
+ if !strings.EqualFold(plan.JsonExample.ValueString(), state.JsonExample.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // Проверяем изменился ли git_revision.
+ // Если да — вызываем redeploy вместо modify.
+ // git_revision = "" (не задано) → обычный modify.
+ redeployRequested := false
+ if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() {
+ if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() {
+ redeployRequested = true
+ }
+ }
+
+ if !hasServiceParamChanges && !redeployRequested {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ if redeployRequested {
+ // Вызываем redeploy — пустой params (операция не требует CFS-параметров)
+ if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ } else {
+ params := map[int]string{
+ 287: resources_core.FormatInt64(plan.DurationMs),
+ 288: resources_core.FormatBool(plan.FailAtStart),
+ 289: resources_core.FormatBool(plan.FailInProgress),
+ 290: resources_core.FormatInt64(plan.WhereFail),
+ 291: resources_core.FormatString(plan.Bodymessage),
+ 373: resources_core.BuildJSON(map[string]string{"secret": fmt.Sprintf("\"%s\"", plan.MapExample.Secret.ValueString())}),
+ 374: resources_core.FormatString(plan.JsonExample),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 1, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "arr", Field: "Arr", Type: "string"},
+ {Code: "arrayMapFixedExample", Field: "ArrayMapFixedExample", Type: "array-map-fixed"},
+ {Code: "bodymessage", Field: "Bodymessage", Type: "string"},
+ {Code: "durationMs", Field: "DurationMs", Type: "int64"},
+ {Code: "failAtStart", Field: "FailAtStart", Type: "bool"},
+ {Code: "failInProgress", Field: "FailInProgress", Type: "bool"},
+ {Code: "jsonExample", Field: "JsonExample", Type: "string"},
+ {Code: "mapExample", Field: "MapExample", Type: "string"},
+ {Code: "mapFixed", Field: "MapFixed", Type: "map-fixed"},
+ {Code: "nestedRefExample", Field: "NestedRefExample", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "whereFail", Field: "WhereFail", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *DummyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state DummyModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *DummyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *DummyResource) 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
+}
diff --git a/generated/test/go/20_vc_org_saas_resource.go b/generated/test/go/20_vc_org_saas_resource.go
new file mode 100644
index 0000000..c1277b1
--- /dev/null
+++ b/generated/test/go/20_vc_org_saas_resource.go
@@ -0,0 +1,348 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_org_saas
+// Service ID: 20
+
+var _ resource.Resource = &VcOrgSaasResource{}
+var _ resource.ResourceWithModifyPlan = &VcOrgSaasResource{}
+var _ resource.ResourceWithImportState = &VcOrgSaasResource{}
+
+type VcOrgSaasResource struct {
+ client *core.UniversalClient
+}
+
+type VcOrgSaasModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVcOrgSaasResource() resource.Resource {
+ return &VcOrgSaasResource{}
+}
+
+func (r *VcOrgSaasResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_org_saas"
+}
+
+func (r *VcOrgSaasResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("sandbox.nubes.ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcOrgSaasResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VcOrgSaasModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VcOrgSaasModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VcOrgSaasModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 185: resources_core.FormatString(config.ResourceRealm),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 20, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VcOrgSaasResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VcOrgSaasModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 20, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 185: resources_core.FormatString(data.ResourceRealm),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 20, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 20, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcOrgSaasResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcOrgSaasModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 20, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VcOrgSaasResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcOrgSaasModel
+ var state VcOrgSaasModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 20, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcOrgSaasResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcOrgSaasModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VcOrgSaasResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VcOrgSaasResource) 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
+}
diff --git a/generated/test/go/21_vc_vdc_resource.go b/generated/test/go/21_vc_vdc_resource.go
new file mode 100644
index 0000000..0c32692
--- /dev/null
+++ b/generated/test/go/21_vc_vdc_resource.go
@@ -0,0 +1,573 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_vdc
+// Service ID: 21
+
+var _ resource.Resource = &VcVdcResource{}
+var _ resource.ResourceWithModifyPlan = &VcVdcResource{}
+var _ resource.ResourceWithImportState = &VcVdcResource{}
+
+type VcVdcResource struct {
+ client *core.UniversalClient
+}
+
+type VcVdcModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ CpuAllocated types.Int64 `tfsdk:"cpu_allocated"`
+ CpuGuaranteed types.Int64 `tfsdk:"cpu_guaranteed"`
+ MemAllocated types.Int64 `tfsdk:"mem_allocated"`
+ NetworkProvider types.String `tfsdk:"network_provider"`
+ OrganizationUid types.String `tfsdk:"organization_uid"`
+ ProviderVdc types.String `tfsdk:"provider_vdc"`
+ StorageConfig types.String `tfsdk:"storage_config"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVcVdcResource() resource.Resource {
+ return &VcVdcResource{}
+}
+
+func (r *VcVdcResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_vdc"
+}
+
+func (r *VcVdcResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "cpu_allocated": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(80), MarkdownDescription: "шт"},
+ "cpu_guaranteed": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(20), MarkdownDescription: "% резервирования Не изменяется после создания."},
+ "mem_allocated": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(200), MarkdownDescription: "gb"},
+ "network_provider": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания."},
+ "organization_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Для создания VDC предварительно необходимо создать организацию через услугу `Организация в Cloud Director` Не изменяется после создания."},
+ "provider_vdc": schema.StringAttribute{Required: true, MarkdownDescription: "Приписка `2.8` - Скорость vCPU - 2800MHz
И так далее...
Не изменяется после создания."},
+ "storage_config": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Первая политика в списке указывается как "По умолчанию""},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcVdcResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VcVdcModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VcVdcModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VcVdcModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.CpuGuaranteed.IsNull() && !plan.CpuGuaranteed.IsUnknown() && !state.CpuGuaranteed.IsNull() && !state.CpuGuaranteed.IsUnknown() {
+ if plan.CpuGuaranteed.ValueInt64() != state.CpuGuaranteed.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить cpu_guaranteed", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.NetworkProvider.IsNull() && !plan.NetworkProvider.IsUnknown() && !state.NetworkProvider.IsNull() && !state.NetworkProvider.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.NetworkProvider.ValueString(), state.NetworkProvider.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить network_provider", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.OrganizationUid.IsNull() && !plan.OrganizationUid.IsUnknown() && !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.OrganizationUid.ValueString(), state.OrganizationUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить organization_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ProviderVdc.IsNull() && !plan.ProviderVdc.IsUnknown() && !state.ProviderVdc.IsNull() && !state.ProviderVdc.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ProviderVdc.ValueString(), state.ProviderVdc.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить provider_vdc", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.NetworkProvider.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "network_provider is required.")
+ return
+ }
+ if config.NetworkProvider.IsUnknown() {
+ return
+ }
+ if config.OrganizationUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "organization_uid is required.")
+ return
+ }
+ if config.OrganizationUid.IsUnknown() {
+ return
+ }
+ if config.ProviderVdc.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "provider_vdc is required.")
+ return
+ }
+ if config.ProviderVdc.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.OrganizationUid.IsNull() && !config.OrganizationUid.IsUnknown() {
+ resolvedOrganizationUid, err := r.client.ResolveRefSvcParamValue(ctx, 19, config.OrganizationUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve organization_uid", err.Error())
+ } else if resolvedOrganizationUid != "" && resolvedOrganizationUid != config.OrganizationUid.ValueString() {
+ config.OrganizationUid = types.StringValue(resolvedOrganizationUid)
+ }
+ }
+ params := map[int]string{
+ 30: resources_core.FormatString(config.OrganizationUid),
+ 335: resources_core.FormatString(config.ProviderVdc),
+ 361: resources_core.FormatString(config.StorageConfig),
+ 397: resources_core.FormatInt64(config.CpuGuaranteed),
+ 557: resources_core.FormatInt64(config.CpuAllocated),
+ 558: resources_core.FormatInt64(config.MemAllocated),
+ 746: resources_core.FormatString(config.NetworkProvider),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 21, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VcVdcResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VcVdcModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.NetworkProvider.IsNull() || data.NetworkProvider.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "network_provider is required.")
+ return
+ }
+ if data.OrganizationUid.IsNull() || data.OrganizationUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "organization_uid is required.")
+ return
+ }
+ if data.ProviderVdc.IsNull() || data.ProviderVdc.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "provider_vdc is required.")
+ return
+ }
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalOrganizationUid := data.OrganizationUid
+ if !data.OrganizationUid.IsNull() && !data.OrganizationUid.IsUnknown() {
+ resolvedOrganizationUid, err := r.client.ResolveRefSvcParamValue(ctx, 19, data.OrganizationUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedOrganizationUid != "" && resolvedOrganizationUid != data.OrganizationUid.ValueString() {
+ data.OrganizationUid = types.StringValue(resolvedOrganizationUid)
+ }
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 21, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 30: resources_core.FormatString(data.OrganizationUid),
+ 335: resources_core.FormatString(data.ProviderVdc),
+ 361: resources_core.FormatString(data.StorageConfig),
+ 397: resources_core.FormatInt64(data.CpuGuaranteed),
+ 557: resources_core.FormatInt64(data.CpuAllocated),
+ 558: resources_core.FormatInt64(data.MemAllocated),
+ 746: resources_core.FormatString(data.NetworkProvider),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 21, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 21, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "cpuAllocated", Field: "CpuAllocated", Type: "int64"},
+ {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"},
+ {Code: "memAllocated", Field: "MemAllocated", Type: "int64"},
+ {Code: "networkProvider", Field: "NetworkProvider", Type: "string"},
+ {Code: "organizationUid", Field: "OrganizationUid", Type: "string"},
+ {Code: "providerVdc", Field: "ProviderVdc", Type: "string"},
+ {Code: "storageConfig", Field: "StorageConfig", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() {
+ resolvedOrganizationUid, err := r.client.ResolveRefSvcParamValue(ctx, 19, state.OrganizationUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve organization_uid", err.Error())
+ } else if resolvedOrganizationUid != "" && resolvedOrganizationUid != state.OrganizationUid.ValueString() {
+ state.OrganizationUid = types.StringValue(resolvedOrganizationUid)
+ }
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+ if !originalOrganizationUid.IsNull() && !originalOrganizationUid.IsUnknown() && !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() {
+ if strings.EqualFold(state.OrganizationUid.ValueString(), originalOrganizationUid.ValueString()) {
+ state.OrganizationUid = originalOrganizationUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcVdcResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcVdcModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 21, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "cpuAllocated", Field: "CpuAllocated", Type: "int64"},
+ {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"},
+ {Code: "memAllocated", Field: "MemAllocated", Type: "int64"},
+ {Code: "networkProvider", Field: "NetworkProvider", Type: "string"},
+ {Code: "organizationUid", Field: "OrganizationUid", Type: "string"},
+ {Code: "providerVdc", Field: "ProviderVdc", Type: "string"},
+ {Code: "storageConfig", Field: "StorageConfig", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !newState.OrganizationUid.IsNull() && !newState.OrganizationUid.IsUnknown() {
+ resolvedOrganizationUid, err := r.client.ResolveRefSvcParamValue(ctx, 19, newState.OrganizationUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve organization_uid", err.Error())
+ } else if resolvedOrganizationUid != "" && resolvedOrganizationUid != newState.OrganizationUid.ValueString() {
+ newState.OrganizationUid = types.StringValue(resolvedOrganizationUid)
+ }
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+ if !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() && !newState.OrganizationUid.IsNull() && !newState.OrganizationUid.IsUnknown() {
+ if strings.EqualFold(newState.OrganizationUid.ValueString(), state.OrganizationUid.ValueString()) {
+ newState.OrganizationUid = state.OrganizationUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VcVdcResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcVdcModel
+ var state VcVdcModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.CpuAllocated.IsNull() != state.CpuAllocated.IsNull() || plan.CpuAllocated.IsUnknown() != state.CpuAllocated.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.CpuAllocated.IsNull() && !plan.CpuAllocated.IsUnknown() && !state.CpuAllocated.IsNull() && !state.CpuAllocated.IsUnknown() {
+ if plan.CpuAllocated.ValueInt64() != state.CpuAllocated.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.MemAllocated.IsNull() != state.MemAllocated.IsNull() || plan.MemAllocated.IsUnknown() != state.MemAllocated.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.MemAllocated.IsNull() && !plan.MemAllocated.IsUnknown() && !state.MemAllocated.IsNull() && !state.MemAllocated.IsUnknown() {
+ if plan.MemAllocated.ValueInt64() != state.MemAllocated.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.StorageConfig.IsNull() != state.StorageConfig.IsNull() || plan.StorageConfig.IsUnknown() != state.StorageConfig.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.StorageConfig.IsNull() && !plan.StorageConfig.IsUnknown() && !state.StorageConfig.IsNull() && !state.StorageConfig.IsUnknown() {
+ if !strings.EqualFold(plan.StorageConfig.ValueString(), state.StorageConfig.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 560: resources_core.FormatInt64(plan.CpuAllocated),
+ 561: resources_core.FormatInt64(plan.MemAllocated),
+ 562: resources_core.FormatString(plan.StorageConfig),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 21, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "cpuAllocated", Field: "CpuAllocated", Type: "int64"},
+ {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"},
+ {Code: "memAllocated", Field: "MemAllocated", Type: "int64"},
+ {Code: "networkProvider", Field: "NetworkProvider", Type: "string"},
+ {Code: "organizationUid", Field: "OrganizationUid", Type: "string"},
+ {Code: "providerVdc", Field: "ProviderVdc", Type: "string"},
+ {Code: "storageConfig", Field: "StorageConfig", Type: "array-map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() {
+ resolvedOrganizationUid, err := r.client.ResolveRefSvcParamValue(ctx, 19, state.OrganizationUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve organization_uid", err.Error())
+ } else if resolvedOrganizationUid != "" && resolvedOrganizationUid != state.OrganizationUid.ValueString() {
+ state.OrganizationUid = types.StringValue(resolvedOrganizationUid)
+ }
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+ if !plan.OrganizationUid.IsNull() && !plan.OrganizationUid.IsUnknown() && !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() {
+ if strings.EqualFold(state.OrganizationUid.ValueString(), plan.OrganizationUid.ValueString()) {
+ state.OrganizationUid = plan.OrganizationUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcVdcResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcVdcModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VcVdcResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VcVdcResource) 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
+}
diff --git a/generated/test/go/22_vc_nsxt_resource.go b/generated/test/go/22_vc_nsxt_resource.go
new file mode 100644
index 0000000..ca0acdb
--- /dev/null
+++ b/generated/test/go/22_vc_nsxt_resource.go
@@ -0,0 +1,614 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_nsxt
+// Service ID: 22
+
+var _ resource.Resource = &VcNsxtResource{}
+var _ resource.ResourceWithModifyPlan = &VcNsxtResource{}
+var _ resource.ResourceWithImportState = &VcNsxtResource{}
+
+type VcNsxtResource struct {
+ client *core.UniversalClient
+}
+
+type VcNsxtModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ IpSpaceName types.String `tfsdk:"ip_space_name"`
+ NeedEnableAVI types.Bool `tfsdk:"need_enable_a_v_i"`
+ QosProfile types.String `tfsdk:"qos_profile"`
+ VdcGroupUid types.String `tfsdk:"vdc_group_uid"`
+ VdcType types.String `tfsdk:"vdc_type"`
+ VdcUid types.String `tfsdk:"vdc_uid"`
+ VirtualServicesCount types.Int64 `tfsdk:"virtual_services_count"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVcNsxtResource() resource.Resource {
+ return &VcNsxtResource{}
+}
+
+func (r *VcNsxtResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_nsxt"
+}
+
+func (r *VcNsxtResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "ip_space_name": schema.StringAttribute{Optional: true, MarkdownDescription: "Имя ip Space для внешнего IP"},
+ "need_enable_a_v_i": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включение Load Balancer"},
+ "qos_profile": schema.StringAttribute{Optional: true, MarkdownDescription: "Параметр пока не работает
Должен возвращать поле из ресурсной платформы типа vc из .vcd.hardware.gatewayQoSProfiles
Как по идее должен работать.
Нужно заполнить или CFS vdcUid, или vdcGroupUid
Идеально конечно проверять `if (params.vdcType == "vdc" && params.vdcUid != "")` или `if (params.vdcType == "vdcGroup" && params.vdcGroupUid != "")`
Далее надо пойти по стейту vdc -> org -> resPlatform, взять gatewayQoSProfiles
Или пойти по стейту vdcgroup -> vdc -> org -> resPlatform, взять gatewayQoSProfiles
Регулярку могу написать (Виталя)"},
+ "vdc_group_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID Услуги `Группа виртуальных датацентров (groupvDC)` Не изменяется после создания."},
+ "vdc_type": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("vdc"), MarkdownDescription: "Тип родительской услуги Не изменяется после создания."},
+ "vdc_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID Услуги `Виртуальный датацентр (vDC)` Не изменяется после создания."},
+ "virtual_services_count": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Кол-во виртуальных сервисов, которые **резервируются** на AVI"},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcNsxtResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VcNsxtModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VcNsxtModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VcNsxtModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.VdcGroupUid.IsNull() && !plan.VdcGroupUid.IsUnknown() && !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VdcGroupUid.ValueString(), state.VdcGroupUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vdc_group_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VdcType.IsNull() && !plan.VdcType.IsUnknown() && !state.VdcType.IsNull() && !state.VdcType.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VdcType.ValueString(), state.VdcType.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vdc_type", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VdcUid.ValueString(), state.VdcUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vdc_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.VdcGroupUid.IsNull() && !config.VdcGroupUid.IsUnknown() {
+ resolvedVdcGroupUid, err := r.client.ResolveRefSvcParamValue(ctx, 29, config.VdcGroupUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_group_uid", err.Error())
+ } else if resolvedVdcGroupUid != "" && resolvedVdcGroupUid != config.VdcGroupUid.ValueString() {
+ config.VdcGroupUid = types.StringValue(resolvedVdcGroupUid)
+ }
+ }
+ if !config.VdcUid.IsNull() && !config.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, config.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != config.VdcUid.ValueString() {
+ config.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ params := map[int]string{
+ 8: resources_core.FormatString(config.VdcUid),
+ 340: resources_core.FormatBool(config.NeedEnableAVI),
+ 341: resources_core.FormatInt64(config.VirtualServicesCount),
+ 621: resources_core.FormatString(config.VdcType),
+ 622: resources_core.FormatString(config.VdcGroupUid),
+ 825: resources_core.FormatString(config.QosProfile),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 22, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VcNsxtResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VcNsxtModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalVdcGroupUid := data.VdcGroupUid
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalVdcUid := data.VdcUid
+ if !data.VdcGroupUid.IsNull() && !data.VdcGroupUid.IsUnknown() {
+ resolvedVdcGroupUid, err := r.client.ResolveRefSvcParamValue(ctx, 29, data.VdcGroupUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedVdcGroupUid != "" && resolvedVdcGroupUid != data.VdcGroupUid.ValueString() {
+ data.VdcGroupUid = types.StringValue(resolvedVdcGroupUid)
+ }
+ }
+ if !data.VdcUid.IsNull() && !data.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, data.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedVdcUid != "" && resolvedVdcUid != data.VdcUid.ValueString() {
+ data.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 22, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 8: resources_core.FormatString(data.VdcUid),
+ 340: resources_core.FormatBool(data.NeedEnableAVI),
+ 341: resources_core.FormatInt64(data.VirtualServicesCount),
+ 621: resources_core.FormatString(data.VdcType),
+ 622: resources_core.FormatString(data.VdcGroupUid),
+ 825: resources_core.FormatString(data.QosProfile),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 22, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 22, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"},
+ {Code: "qosProfile", Field: "QosProfile", Type: "string"},
+ {Code: "vdcGroupUid", Field: "VdcGroupUid", Type: "string"},
+ {Code: "vdcType", Field: "VdcType", Type: "string"},
+ {Code: "vdcUid", Field: "VdcUid", Type: "string"},
+ {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() {
+ resolvedVdcGroupUid, err := r.client.ResolveRefSvcParamValue(ctx, 29, state.VdcGroupUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_group_uid", err.Error())
+ } else if resolvedVdcGroupUid != "" && resolvedVdcGroupUid != state.VdcGroupUid.ValueString() {
+ state.VdcGroupUid = types.StringValue(resolvedVdcGroupUid)
+ }
+ }
+ if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() {
+ state.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+ if !originalVdcGroupUid.IsNull() && !originalVdcGroupUid.IsUnknown() && !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() {
+ if strings.EqualFold(state.VdcGroupUid.ValueString(), originalVdcGroupUid.ValueString()) {
+ state.VdcGroupUid = originalVdcGroupUid
+ }
+ }
+ if !originalVdcUid.IsNull() && !originalVdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ if strings.EqualFold(state.VdcUid.ValueString(), originalVdcUid.ValueString()) {
+ state.VdcUid = originalVdcUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcNsxtResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcNsxtModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 22, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"},
+ {Code: "qosProfile", Field: "QosProfile", Type: "string"},
+ {Code: "vdcGroupUid", Field: "VdcGroupUid", Type: "string"},
+ {Code: "vdcType", Field: "VdcType", Type: "string"},
+ {Code: "vdcUid", Field: "VdcUid", Type: "string"},
+ {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !newState.VdcGroupUid.IsNull() && !newState.VdcGroupUid.IsUnknown() {
+ resolvedVdcGroupUid, err := r.client.ResolveRefSvcParamValue(ctx, 29, newState.VdcGroupUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_group_uid", err.Error())
+ } else if resolvedVdcGroupUid != "" && resolvedVdcGroupUid != newState.VdcGroupUid.ValueString() {
+ newState.VdcGroupUid = types.StringValue(resolvedVdcGroupUid)
+ }
+ }
+ if !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, newState.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != newState.VdcUid.ValueString() {
+ newState.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+ if !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() && !newState.VdcGroupUid.IsNull() && !newState.VdcGroupUid.IsUnknown() {
+ if strings.EqualFold(newState.VdcGroupUid.ValueString(), state.VdcGroupUid.ValueString()) {
+ newState.VdcGroupUid = state.VdcGroupUid
+ }
+ }
+ if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() && !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() {
+ if strings.EqualFold(newState.VdcUid.ValueString(), state.VdcUid.ValueString()) {
+ newState.VdcUid = state.VdcUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VcNsxtResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcNsxtModel
+ var state VcNsxtModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.NeedEnableAVI.IsNull() != state.NeedEnableAVI.IsNull() || plan.NeedEnableAVI.IsUnknown() != state.NeedEnableAVI.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.NeedEnableAVI.IsNull() && !plan.NeedEnableAVI.IsUnknown() && !state.NeedEnableAVI.IsNull() && !state.NeedEnableAVI.IsUnknown() {
+ if plan.NeedEnableAVI.ValueBool() != state.NeedEnableAVI.ValueBool() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.VirtualServicesCount.IsNull() != state.VirtualServicesCount.IsNull() || plan.VirtualServicesCount.IsUnknown() != state.VirtualServicesCount.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.VirtualServicesCount.IsNull() && !plan.VirtualServicesCount.IsUnknown() && !state.VirtualServicesCount.IsNull() && !state.VirtualServicesCount.IsUnknown() {
+ if plan.VirtualServicesCount.ValueInt64() != state.VirtualServicesCount.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.IpSpaceName.IsNull() != state.IpSpaceName.IsNull() || plan.IpSpaceName.IsUnknown() != state.IpSpaceName.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.IpSpaceName.IsNull() && !plan.IpSpaceName.IsUnknown() && !state.IpSpaceName.IsNull() && !state.IpSpaceName.IsUnknown() {
+ if !strings.EqualFold(plan.IpSpaceName.ValueString(), state.IpSpaceName.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.QosProfile.IsNull() != state.QosProfile.IsNull() || plan.QosProfile.IsUnknown() != state.QosProfile.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.QosProfile.IsNull() && !plan.QosProfile.IsUnknown() && !state.QosProfile.IsNull() && !state.QosProfile.IsUnknown() {
+ if !strings.EqualFold(plan.QosProfile.ValueString(), state.QosProfile.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 368: resources_core.FormatBool(plan.NeedEnableAVI),
+ 369: resources_core.FormatInt64(plan.VirtualServicesCount),
+ 372: resources_core.FormatString(plan.IpSpaceName),
+ 856: resources_core.FormatString(plan.QosProfile),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 22, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"},
+ {Code: "qosProfile", Field: "QosProfile", Type: "string"},
+ {Code: "vdcGroupUid", Field: "VdcGroupUid", Type: "string"},
+ {Code: "vdcType", Field: "VdcType", Type: "string"},
+ {Code: "vdcUid", Field: "VdcUid", Type: "string"},
+ {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() {
+ resolvedVdcGroupUid, err := r.client.ResolveRefSvcParamValue(ctx, 29, state.VdcGroupUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_group_uid", err.Error())
+ } else if resolvedVdcGroupUid != "" && resolvedVdcGroupUid != state.VdcGroupUid.ValueString() {
+ state.VdcGroupUid = types.StringValue(resolvedVdcGroupUid)
+ }
+ }
+ if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() {
+ state.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+ if !plan.VdcGroupUid.IsNull() && !plan.VdcGroupUid.IsUnknown() && !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() {
+ if strings.EqualFold(state.VdcGroupUid.ValueString(), plan.VdcGroupUid.ValueString()) {
+ state.VdcGroupUid = plan.VdcGroupUid
+ }
+ }
+ if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ if strings.EqualFold(state.VdcUid.ValueString(), plan.VdcUid.ValueString()) {
+ state.VdcUid = plan.VdcUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcNsxtResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcNsxtModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VcNsxtResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VcNsxtResource) 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
+}
diff --git a/generated/test/go/23_vc_vm_resource.go b/generated/test/go/23_vc_vm_resource.go
new file mode 100644
index 0000000..67aea2c
--- /dev/null
+++ b/generated/test/go/23_vc_vm_resource.go
@@ -0,0 +1,581 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_vm
+// Service ID: 23
+
+var _ resource.Resource = &VcVmResource{}
+var _ resource.ResourceWithModifyPlan = &VcVmResource{}
+var _ resource.ResourceWithImportState = &VcVmResource{}
+
+type VcVmResource struct {
+ client *core.UniversalClient
+}
+
+type VcVmModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AccessIpList types.String `tfsdk:"access_ip_list"`
+ IpSpaceName types.String `tfsdk:"ip_space_name"`
+ TemplateName types.String `tfsdk:"template_name"`
+ VappUid types.String `tfsdk:"vapp_uid"`
+ VmCpu types.String `tfsdk:"vm_cpu"`
+ VmDisksize types.String `tfsdk:"vm_disksize"`
+ VmName types.String `tfsdk:"vm_name"`
+ VmRam types.String `tfsdk:"vm_ram"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVcVmResource() resource.Resource {
+ return &VcVmResource{}
+}
+
+func (r *VcVmResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_vm"
+}
+
+func (r *VcVmResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_ip_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("79.111.119.78/32 185.247.187.1/32"), MarkdownDescription: "Вставить содержимое из 'curl ifconfig.io'
IP адреса в CIDR формате через пробел для настройки аксес листов"},
+ "ip_space_name": schema.StringAttribute{Required: true, MarkdownDescription: "Брать из свойств ресурсной платформы
.vcd.ipSpace
Имя IP-space для выделения внешнего IP (IP выделяется обязательно) Не изменяется после создания."},
+ "template_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("RockyLinux_9-16G"), MarkdownDescription: "ОС Не изменяется после создания."},
+ "vapp_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "InstanceUid vApp. vApp должен быть создан Не изменяется после создания."},
+ "vm_cpu": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("2"), MarkdownDescription: "CPU"},
+ "vm_disksize": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("10"), MarkdownDescription: "Размер диска под сервис (GB)"},
+ "vm_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("vm0001"), MarkdownDescription: "Имя ВМ. Не поддерживает спец-символы Не изменяется после создания."},
+ "vm_ram": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("2"), MarkdownDescription: "RAM (GB)"},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcVmResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VcVmModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VcVmModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VcVmModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.IpSpaceName.IsNull() && !plan.IpSpaceName.IsUnknown() && !state.IpSpaceName.IsNull() && !state.IpSpaceName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.IpSpaceName.ValueString(), state.IpSpaceName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить ip_space_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.TemplateName.IsNull() && !plan.TemplateName.IsUnknown() && !state.TemplateName.IsNull() && !state.TemplateName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.TemplateName.ValueString(), state.TemplateName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить template_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VappUid.IsNull() && !plan.VappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VappUid.ValueString(), state.VappUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vapp_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VmName.IsNull() && !plan.VmName.IsUnknown() && !state.VmName.IsNull() && !state.VmName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VmName.ValueString(), state.VmName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vm_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.IpSpaceName.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.")
+ return
+ }
+ if config.IpSpaceName.IsUnknown() {
+ return
+ }
+ if config.VappUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "vapp_uid is required.")
+ return
+ }
+ if config.VappUid.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.VappUid.IsNull() && !config.VappUid.IsUnknown() {
+ resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, config.VappUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error())
+ } else if resolvedVappUid != "" && resolvedVappUid != config.VappUid.ValueString() {
+ config.VappUid = types.StringValue(resolvedVappUid)
+ }
+ }
+ params := map[int]string{
+ 33: resources_core.FormatString(config.VappUid),
+ 35: resources_core.FormatString(config.VmName),
+ 36: resources_core.FormatString(config.VmCpu),
+ 37: resources_core.FormatString(config.VmRam),
+ 38: resources_core.FormatString(config.VmDisksize),
+ 176: resources_core.FormatString(config.TemplateName),
+ 177: resources_core.FormatString(config.AccessIpList),
+ 406: resources_core.FormatString(config.IpSpaceName),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 23, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VcVmResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VcVmModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.IpSpaceName.IsNull() || data.IpSpaceName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.")
+ return
+ }
+ if data.VappUid.IsNull() || data.VappUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "vapp_uid is required.")
+ return
+ }
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalVappUid := data.VappUid
+ if !data.VappUid.IsNull() && !data.VappUid.IsUnknown() {
+ resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, data.VappUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedVappUid != "" && resolvedVappUid != data.VappUid.ValueString() {
+ data.VappUid = types.StringValue(resolvedVappUid)
+ }
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 23, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 33: resources_core.FormatString(data.VappUid),
+ 35: resources_core.FormatString(data.VmName),
+ 36: resources_core.FormatString(data.VmCpu),
+ 37: resources_core.FormatString(data.VmRam),
+ 38: resources_core.FormatString(data.VmDisksize),
+ 176: resources_core.FormatString(data.TemplateName),
+ 177: resources_core.FormatString(data.AccessIpList),
+ 406: resources_core.FormatString(data.IpSpaceName),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 23, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 23, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessIpList", Field: "AccessIpList", Type: "string"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "templateName", Field: "TemplateName", Type: "string"},
+ {Code: "vappUid", Field: "VappUid", Type: "string"},
+ {Code: "vmCpu", Field: "VmCpu", Type: "string"},
+ {Code: "vmDisksize", Field: "VmDisksize", Type: "string"},
+ {Code: "vmName", Field: "VmName", Type: "string"},
+ {Code: "vmRam", Field: "VmRam", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() {
+ resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, state.VappUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error())
+ } else if resolvedVappUid != "" && resolvedVappUid != state.VappUid.ValueString() {
+ state.VappUid = types.StringValue(resolvedVappUid)
+ }
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+ if !originalVappUid.IsNull() && !originalVappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() {
+ if strings.EqualFold(state.VappUid.ValueString(), originalVappUid.ValueString()) {
+ state.VappUid = originalVappUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcVmResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcVmModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 23, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessIpList", Field: "AccessIpList", Type: "string"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "templateName", Field: "TemplateName", Type: "string"},
+ {Code: "vappUid", Field: "VappUid", Type: "string"},
+ {Code: "vmCpu", Field: "VmCpu", Type: "string"},
+ {Code: "vmDisksize", Field: "VmDisksize", Type: "string"},
+ {Code: "vmName", Field: "VmName", Type: "string"},
+ {Code: "vmRam", Field: "VmRam", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !newState.VappUid.IsNull() && !newState.VappUid.IsUnknown() {
+ resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, newState.VappUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error())
+ } else if resolvedVappUid != "" && resolvedVappUid != newState.VappUid.ValueString() {
+ newState.VappUid = types.StringValue(resolvedVappUid)
+ }
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+ if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() && !newState.VappUid.IsNull() && !newState.VappUid.IsUnknown() {
+ if strings.EqualFold(newState.VappUid.ValueString(), state.VappUid.ValueString()) {
+ newState.VappUid = state.VappUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VcVmResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcVmModel
+ var state VcVmModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.VmCpu.IsNull() != state.VmCpu.IsNull() || plan.VmCpu.IsUnknown() != state.VmCpu.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.VmCpu.IsNull() && !plan.VmCpu.IsUnknown() && !state.VmCpu.IsNull() && !state.VmCpu.IsUnknown() {
+ if !strings.EqualFold(plan.VmCpu.ValueString(), state.VmCpu.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.VmRam.IsNull() != state.VmRam.IsNull() || plan.VmRam.IsUnknown() != state.VmRam.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.VmRam.IsNull() && !plan.VmRam.IsUnknown() && !state.VmRam.IsNull() && !state.VmRam.IsUnknown() {
+ if !strings.EqualFold(plan.VmRam.ValueString(), state.VmRam.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.VmDisksize.IsNull() != state.VmDisksize.IsNull() || plan.VmDisksize.IsUnknown() != state.VmDisksize.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.VmDisksize.IsNull() && !plan.VmDisksize.IsUnknown() && !state.VmDisksize.IsNull() && !state.VmDisksize.IsUnknown() {
+ if !strings.EqualFold(plan.VmDisksize.ValueString(), state.VmDisksize.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.AccessIpList.IsNull() != state.AccessIpList.IsNull() || plan.AccessIpList.IsUnknown() != state.AccessIpList.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.AccessIpList.IsNull() && !plan.AccessIpList.IsUnknown() && !state.AccessIpList.IsNull() && !state.AccessIpList.IsUnknown() {
+ if !strings.EqualFold(plan.AccessIpList.ValueString(), state.AccessIpList.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 186: resources_core.FormatString(plan.VmCpu),
+ 187: resources_core.FormatString(plan.VmRam),
+ 188: resources_core.FormatString(plan.VmDisksize),
+ 189: resources_core.FormatString(plan.AccessIpList),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 23, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessIpList", Field: "AccessIpList", Type: "string"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "templateName", Field: "TemplateName", Type: "string"},
+ {Code: "vappUid", Field: "VappUid", Type: "string"},
+ {Code: "vmCpu", Field: "VmCpu", Type: "string"},
+ {Code: "vmDisksize", Field: "VmDisksize", Type: "string"},
+ {Code: "vmName", Field: "VmName", Type: "string"},
+ {Code: "vmRam", Field: "VmRam", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() {
+ resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, state.VappUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error())
+ } else if resolvedVappUid != "" && resolvedVappUid != state.VappUid.ValueString() {
+ state.VappUid = types.StringValue(resolvedVappUid)
+ }
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+ if !plan.VappUid.IsNull() && !plan.VappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() {
+ if strings.EqualFold(state.VappUid.ValueString(), plan.VappUid.ValueString()) {
+ state.VappUid = plan.VappUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcVmResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcVmModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VcVmResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VcVmResource) 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
+}
diff --git a/generated/test/go/25_vcexternalip_resource.go b/generated/test/go/25_vcexternalip_resource.go
new file mode 100644
index 0000000..686e188
--- /dev/null
+++ b/generated/test/go/25_vcexternalip_resource.go
@@ -0,0 +1,604 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vcexternalip
+// Service ID: 25
+
+var _ resource.Resource = &VcexternalipResource{}
+var _ resource.ResourceWithModifyPlan = &VcexternalipResource{}
+var _ resource.ResourceWithImportState = &VcexternalipResource{}
+
+type VcexternalipResource struct {
+ client *core.UniversalClient
+}
+
+type VcexternalipModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ DnatCreate types.Bool `tfsdk:"dnat_create"`
+ FromServiceCloudEdgeName types.String `tfsdk:"from_service_cloud_edge_name"`
+ FromServiceCloudEdgeScope types.String `tfsdk:"from_service_cloud_edge_scope"`
+ FromServiceCloudOrgName types.String `tfsdk:"from_service_cloud_org_name"`
+ FromServiceCloudVdcName types.String `tfsdk:"from_service_cloud_vdc_name"`
+ FromServiceCloudVmwareUrl types.String `tfsdk:"from_service_cloud_vmware_url"`
+ FromServiceNamespace types.String `tfsdk:"from_service_namespace"`
+ FromServiceVdcGroupName types.String `tfsdk:"from_service_vdc_group_name"`
+ InternalAddrAccess types.String `tfsdk:"internal_addr_access"`
+ InternalPortAccess types.String `tfsdk:"internal_port_access"`
+ IpSpaceName types.String `tfsdk:"ip_space_name"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ ServiceUid types.String `tfsdk:"service_uid"`
+ SnatCreate types.Bool `tfsdk:"snat_create"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVcexternalipResource() resource.Resource {
+ return &VcexternalipResource{}
+}
+
+func (r *VcexternalipResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vcexternalip"
+}
+
+func (r *VcexternalipResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "dnat_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Необходимо создать DNAT-правило."},
+ "from_service_cloud_edge_name": schema.StringAttribute{Optional: true, MarkdownDescription: "Edge составной услуги (указывать, если resourceRealm связан с CloudDirector) Не изменяется после создания."},
+ "from_service_cloud_edge_scope": schema.StringAttribute{Optional: true, MarkdownDescription: "Не изменяется после создания."},
+ "from_service_cloud_org_name": schema.StringAttribute{Optional: true, MarkdownDescription: "Org составной услуги (указывать, если resourceRealm связан с CloudDirector) Не изменяется после создания."},
+ "from_service_cloud_vdc_name": schema.StringAttribute{Optional: true, MarkdownDescription: "Vdc составной услуги (указывать, если resourceRealm связан с CloudDirector) Не изменяется после создания."},
+ "from_service_cloud_vmware_url": schema.StringAttribute{Optional: true, MarkdownDescription: "Api-Url составной услуги (указывать, если resourceRealm связан с CloudDirector) Не изменяется после создания."},
+ "from_service_namespace": schema.StringAttribute{Required: true, MarkdownDescription: "Namespace составной услуги (для отображения правил FW) Не изменяется после создания."},
+ "from_service_vdc_group_name": schema.StringAttribute{Optional: true, MarkdownDescription: "Не изменяется после создания."},
+ "internal_addr_access": schema.StringAttribute{Required: true, MarkdownDescription: "Внутренний адрес для связки ната. Требует dnatCreate=true или snatCreate=true"},
+ "internal_port_access": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("5432"), MarkdownDescription: "Порт для связки ната с адресом. Несколько значений можно передавать через пробел. Если передать all, то dnat будет на все порты. Требует dnatCreate=true
"},
+ "ip_space_name": schema.StringAttribute{Required: true, MarkdownDescription: "Тип ip Space (internet-ipv4-v1 / internet-antiddos-v1 / internet-no-antiddos-v1)"},
+ "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("sandbox.nubes.ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ "service_uid": schema.StringAttribute{Required: true},
+ "snat_create": schema.BoolAttribute{Optional: true, MarkdownDescription: "Необходимо создать SNAT-правило.
"},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcexternalipResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VcexternalipModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VcexternalipModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VcexternalipModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.FromServiceCloudEdgeName.IsNull() && !plan.FromServiceCloudEdgeName.IsUnknown() && !state.FromServiceCloudEdgeName.IsNull() && !state.FromServiceCloudEdgeName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.FromServiceCloudEdgeName.ValueString(), state.FromServiceCloudEdgeName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить from_service_cloud_edge_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.FromServiceCloudEdgeScope.IsNull() && !plan.FromServiceCloudEdgeScope.IsUnknown() && !state.FromServiceCloudEdgeScope.IsNull() && !state.FromServiceCloudEdgeScope.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.FromServiceCloudEdgeScope.ValueString(), state.FromServiceCloudEdgeScope.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить from_service_cloud_edge_scope", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.FromServiceCloudOrgName.IsNull() && !plan.FromServiceCloudOrgName.IsUnknown() && !state.FromServiceCloudOrgName.IsNull() && !state.FromServiceCloudOrgName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.FromServiceCloudOrgName.ValueString(), state.FromServiceCloudOrgName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить from_service_cloud_org_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.FromServiceCloudVdcName.IsNull() && !plan.FromServiceCloudVdcName.IsUnknown() && !state.FromServiceCloudVdcName.IsNull() && !state.FromServiceCloudVdcName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.FromServiceCloudVdcName.ValueString(), state.FromServiceCloudVdcName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить from_service_cloud_vdc_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.FromServiceCloudVmwareUrl.IsNull() && !plan.FromServiceCloudVmwareUrl.IsUnknown() && !state.FromServiceCloudVmwareUrl.IsNull() && !state.FromServiceCloudVmwareUrl.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.FromServiceCloudVmwareUrl.ValueString(), state.FromServiceCloudVmwareUrl.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить from_service_cloud_vmware_url", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.FromServiceNamespace.IsNull() && !plan.FromServiceNamespace.IsUnknown() && !state.FromServiceNamespace.IsNull() && !state.FromServiceNamespace.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.FromServiceNamespace.ValueString(), state.FromServiceNamespace.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить from_service_namespace", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.FromServiceVdcGroupName.IsNull() && !plan.FromServiceVdcGroupName.IsUnknown() && !state.FromServiceVdcGroupName.IsNull() && !state.FromServiceVdcGroupName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.FromServiceVdcGroupName.ValueString(), state.FromServiceVdcGroupName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить from_service_vdc_group_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.FromServiceNamespace.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "from_service_namespace is required.")
+ return
+ }
+ if config.FromServiceNamespace.IsUnknown() {
+ return
+ }
+ if config.InternalAddrAccess.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "internal_addr_access is required.")
+ return
+ }
+ if config.InternalAddrAccess.IsUnknown() {
+ return
+ }
+ if config.IpSpaceName.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.")
+ return
+ }
+ if config.IpSpaceName.IsUnknown() {
+ return
+ }
+ if config.ServiceUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "service_uid is required.")
+ return
+ }
+ if config.ServiceUid.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 140: resources_core.FormatString(config.ServiceUid),
+ 141: resources_core.FormatBool(config.DnatCreate),
+ 142: resources_core.FormatString(config.InternalPortAccess),
+ 143: resources_core.FormatBool(config.SnatCreate),
+ 144: resources_core.FormatString(config.InternalAddrAccess),
+ 324: resources_core.FormatString(config.FromServiceNamespace),
+ 325: resources_core.FormatString(config.FromServiceCloudEdgeName),
+ 326: resources_core.FormatString(config.FromServiceCloudVdcName),
+ 327: resources_core.FormatString(config.FromServiceCloudOrgName),
+ 328: resources_core.FormatString(config.FromServiceCloudVmwareUrl),
+ 336: resources_core.FormatString(config.IpSpaceName),
+ 488: resources_core.FormatString(config.ResourceRealm),
+ 624: resources_core.FormatString(config.FromServiceCloudEdgeScope),
+ 626: resources_core.FormatString(config.FromServiceVdcGroupName),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 25, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VcexternalipResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VcexternalipModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.FromServiceNamespace.IsNull() || data.FromServiceNamespace.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "from_service_namespace is required.")
+ return
+ }
+ if data.InternalAddrAccess.IsNull() || data.InternalAddrAccess.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "internal_addr_access is required.")
+ return
+ }
+ if data.IpSpaceName.IsNull() || data.IpSpaceName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.")
+ return
+ }
+ if data.ServiceUid.IsNull() || data.ServiceUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "service_uid is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 25, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 140: resources_core.FormatString(data.ServiceUid),
+ 141: resources_core.FormatBool(data.DnatCreate),
+ 142: resources_core.FormatString(data.InternalPortAccess),
+ 143: resources_core.FormatBool(data.SnatCreate),
+ 144: resources_core.FormatString(data.InternalAddrAccess),
+ 324: resources_core.FormatString(data.FromServiceNamespace),
+ 325: resources_core.FormatString(data.FromServiceCloudEdgeName),
+ 326: resources_core.FormatString(data.FromServiceCloudVdcName),
+ 327: resources_core.FormatString(data.FromServiceCloudOrgName),
+ 328: resources_core.FormatString(data.FromServiceCloudVmwareUrl),
+ 336: resources_core.FormatString(data.IpSpaceName),
+ 488: resources_core.FormatString(data.ResourceRealm),
+ 624: resources_core.FormatString(data.FromServiceCloudEdgeScope),
+ 626: resources_core.FormatString(data.FromServiceVdcGroupName),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 25, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 25, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "dnatCreate", Field: "DnatCreate", Type: "bool"},
+ {Code: "fromServiceCloudEdgeName", Field: "FromServiceCloudEdgeName", Type: "string"},
+ {Code: "fromServiceCloudEdgeScope", Field: "FromServiceCloudEdgeScope", Type: "string"},
+ {Code: "fromServiceCloudOrgName", Field: "FromServiceCloudOrgName", Type: "string"},
+ {Code: "fromServiceCloudVdcName", Field: "FromServiceCloudVdcName", Type: "string"},
+ {Code: "fromServiceCloudVmwareUrl", Field: "FromServiceCloudVmwareUrl", Type: "string"},
+ {Code: "fromServiceNamespace", Field: "FromServiceNamespace", Type: "string"},
+ {Code: "fromServiceVdcGroupName", Field: "FromServiceVdcGroupName", Type: "string"},
+ {Code: "internalAddrAccess", Field: "InternalAddrAccess", Type: "string"},
+ {Code: "internalPortAccess", Field: "InternalPortAccess", Type: "string"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "serviceUid", Field: "ServiceUid", Type: "string"},
+ {Code: "snatCreate", Field: "SnatCreate", Type: "bool"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcexternalipResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcexternalipModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 25, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "dnatCreate", Field: "DnatCreate", Type: "bool"},
+ {Code: "fromServiceCloudEdgeName", Field: "FromServiceCloudEdgeName", Type: "string"},
+ {Code: "fromServiceCloudEdgeScope", Field: "FromServiceCloudEdgeScope", Type: "string"},
+ {Code: "fromServiceCloudOrgName", Field: "FromServiceCloudOrgName", Type: "string"},
+ {Code: "fromServiceCloudVdcName", Field: "FromServiceCloudVdcName", Type: "string"},
+ {Code: "fromServiceCloudVmwareUrl", Field: "FromServiceCloudVmwareUrl", Type: "string"},
+ {Code: "fromServiceNamespace", Field: "FromServiceNamespace", Type: "string"},
+ {Code: "fromServiceVdcGroupName", Field: "FromServiceVdcGroupName", Type: "string"},
+ {Code: "internalAddrAccess", Field: "InternalAddrAccess", Type: "string"},
+ {Code: "internalPortAccess", Field: "InternalPortAccess", Type: "string"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "serviceUid", Field: "ServiceUid", Type: "string"},
+ {Code: "snatCreate", Field: "SnatCreate", Type: "bool"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VcexternalipResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcexternalipModel
+ var state VcexternalipModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.ServiceUid.IsNull() != state.ServiceUid.IsNull() || plan.ServiceUid.IsUnknown() != state.ServiceUid.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ServiceUid.IsNull() && !plan.ServiceUid.IsUnknown() && !state.ServiceUid.IsNull() && !state.ServiceUid.IsUnknown() {
+ if !strings.EqualFold(plan.ServiceUid.ValueString(), state.ServiceUid.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.IpSpaceName.IsNull() != state.IpSpaceName.IsNull() || plan.IpSpaceName.IsUnknown() != state.IpSpaceName.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.IpSpaceName.IsNull() && !plan.IpSpaceName.IsUnknown() && !state.IpSpaceName.IsNull() && !state.IpSpaceName.IsUnknown() {
+ if !strings.EqualFold(plan.IpSpaceName.ValueString(), state.IpSpaceName.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.DnatCreate.IsNull() != state.DnatCreate.IsNull() || plan.DnatCreate.IsUnknown() != state.DnatCreate.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.DnatCreate.IsNull() && !plan.DnatCreate.IsUnknown() && !state.DnatCreate.IsNull() && !state.DnatCreate.IsUnknown() {
+ if plan.DnatCreate.ValueBool() != state.DnatCreate.ValueBool() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.SnatCreate.IsNull() != state.SnatCreate.IsNull() || plan.SnatCreate.IsUnknown() != state.SnatCreate.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.SnatCreate.IsNull() && !plan.SnatCreate.IsUnknown() && !state.SnatCreate.IsNull() && !state.SnatCreate.IsUnknown() {
+ if plan.SnatCreate.ValueBool() != state.SnatCreate.ValueBool() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.InternalPortAccess.IsNull() != state.InternalPortAccess.IsNull() || plan.InternalPortAccess.IsUnknown() != state.InternalPortAccess.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.InternalPortAccess.IsNull() && !plan.InternalPortAccess.IsUnknown() && !state.InternalPortAccess.IsNull() && !state.InternalPortAccess.IsUnknown() {
+ if !strings.EqualFold(plan.InternalPortAccess.ValueString(), state.InternalPortAccess.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.InternalAddrAccess.IsNull() != state.InternalAddrAccess.IsNull() || plan.InternalAddrAccess.IsUnknown() != state.InternalAddrAccess.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.InternalAddrAccess.IsNull() && !plan.InternalAddrAccess.IsUnknown() && !state.InternalAddrAccess.IsNull() && !state.InternalAddrAccess.IsUnknown() {
+ if !strings.EqualFold(plan.InternalAddrAccess.ValueString(), state.InternalAddrAccess.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 627: resources_core.FormatString(plan.ServiceUid),
+ 628: resources_core.FormatString(plan.IpSpaceName),
+ 629: resources_core.FormatBool(plan.DnatCreate),
+ 630: resources_core.FormatBool(plan.SnatCreate),
+ 631: resources_core.FormatString(plan.InternalPortAccess),
+ 632: resources_core.FormatString(plan.InternalAddrAccess),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 25, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "dnatCreate", Field: "DnatCreate", Type: "bool"},
+ {Code: "fromServiceCloudEdgeName", Field: "FromServiceCloudEdgeName", Type: "string"},
+ {Code: "fromServiceCloudEdgeScope", Field: "FromServiceCloudEdgeScope", Type: "string"},
+ {Code: "fromServiceCloudOrgName", Field: "FromServiceCloudOrgName", Type: "string"},
+ {Code: "fromServiceCloudVdcName", Field: "FromServiceCloudVdcName", Type: "string"},
+ {Code: "fromServiceCloudVmwareUrl", Field: "FromServiceCloudVmwareUrl", Type: "string"},
+ {Code: "fromServiceNamespace", Field: "FromServiceNamespace", Type: "string"},
+ {Code: "fromServiceVdcGroupName", Field: "FromServiceVdcGroupName", Type: "string"},
+ {Code: "internalAddrAccess", Field: "InternalAddrAccess", Type: "string"},
+ {Code: "internalPortAccess", Field: "InternalPortAccess", Type: "string"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ {Code: "serviceUid", Field: "ServiceUid", Type: "string"},
+ {Code: "snatCreate", Field: "SnatCreate", Type: "bool"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcexternalipResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcexternalipModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VcexternalipResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VcexternalipResource) 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
+}
diff --git a/generated/test/go/26_vapp_resource.go b/generated/test/go/26_vapp_resource.go
new file mode 100644
index 0000000..68d1ac6
--- /dev/null
+++ b/generated/test/go/26_vapp_resource.go
@@ -0,0 +1,571 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vapp
+// Service ID: 26
+
+var _ resource.Resource = &VappResource{}
+var _ resource.ResourceWithModifyPlan = &VappResource{}
+var _ resource.ResourceWithImportState = &VappResource{}
+
+type VappResource struct {
+ client *core.UniversalClient
+}
+
+type VappModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ NsxtUid types.String `tfsdk:"nsxt_uid"`
+ VappName types.String `tfsdk:"vapp_name"`
+ VdcUid types.String `tfsdk:"vdc_uid"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVappResource() resource.Resource {
+ return &VappResource{}
+}
+
+func (r *VappResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vapp"
+}
+
+func (r *VappResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "nsxt_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID Услуги `Сетевой шлюз периметра (Edge)` Не изменяется после создания."},
+ "vapp_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("lvifmmxj"), MarkdownDescription: "Уникальное в рамках организации Не изменяется после создания."},
+ "vdc_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID Услуги `Виртуальный датацентр (vDC)` Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VappResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VappModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VappModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VappModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.NsxtUid.IsNull() && !plan.NsxtUid.IsUnknown() && !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.NsxtUid.ValueString(), state.NsxtUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить nsxt_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VappName.IsNull() && !plan.VappName.IsUnknown() && !state.VappName.IsNull() && !state.VappName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VappName.ValueString(), state.VappName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vapp_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VdcUid.ValueString(), state.VdcUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vdc_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.NsxtUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "nsxt_uid is required.")
+ return
+ }
+ if config.NsxtUid.IsUnknown() {
+ return
+ }
+ if config.VdcUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "vdc_uid is required.")
+ return
+ }
+ if config.VdcUid.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.NsxtUid.IsNull() && !config.NsxtUid.IsUnknown() {
+ resolvedNsxtUid, err := r.client.ResolveRefSvcParamValue(ctx, 22, config.NsxtUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve nsxt_uid", err.Error())
+ } else if resolvedNsxtUid != "" && resolvedNsxtUid != config.NsxtUid.ValueString() {
+ config.NsxtUid = types.StringValue(resolvedNsxtUid)
+ }
+ }
+ if !config.VdcUid.IsNull() && !config.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, config.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != config.VdcUid.ValueString() {
+ config.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ params := map[int]string{
+ 190: resources_core.FormatString(config.NsxtUid),
+ 191: resources_core.FormatString(config.VappName),
+ 623: resources_core.FormatString(config.VdcUid),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 26, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VappResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VappModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.NsxtUid.IsNull() || data.NsxtUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "nsxt_uid is required.")
+ return
+ }
+ if data.VdcUid.IsNull() || data.VdcUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "vdc_uid is required.")
+ return
+ }
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalNsxtUid := data.NsxtUid
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalVdcUid := data.VdcUid
+ if !data.NsxtUid.IsNull() && !data.NsxtUid.IsUnknown() {
+ resolvedNsxtUid, err := r.client.ResolveRefSvcParamValue(ctx, 22, data.NsxtUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedNsxtUid != "" && resolvedNsxtUid != data.NsxtUid.ValueString() {
+ data.NsxtUid = types.StringValue(resolvedNsxtUid)
+ }
+ }
+ if !data.VdcUid.IsNull() && !data.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, data.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedVdcUid != "" && resolvedVdcUid != data.VdcUid.ValueString() {
+ data.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 26, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 190: resources_core.FormatString(data.NsxtUid),
+ 191: resources_core.FormatString(data.VappName),
+ 623: resources_core.FormatString(data.VdcUid),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 26, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 26, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "nsxtUid", Field: "NsxtUid", Type: "string"},
+ {Code: "vappName", Field: "VappName", Type: "string"},
+ {Code: "vdcUid", Field: "VdcUid", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() {
+ resolvedNsxtUid, err := r.client.ResolveRefSvcParamValue(ctx, 22, state.NsxtUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve nsxt_uid", err.Error())
+ } else if resolvedNsxtUid != "" && resolvedNsxtUid != state.NsxtUid.ValueString() {
+ state.NsxtUid = types.StringValue(resolvedNsxtUid)
+ }
+ }
+ if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() {
+ state.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+ if !originalNsxtUid.IsNull() && !originalNsxtUid.IsUnknown() && !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() {
+ if strings.EqualFold(state.NsxtUid.ValueString(), originalNsxtUid.ValueString()) {
+ state.NsxtUid = originalNsxtUid
+ }
+ }
+ if !originalVdcUid.IsNull() && !originalVdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ if strings.EqualFold(state.VdcUid.ValueString(), originalVdcUid.ValueString()) {
+ state.VdcUid = originalVdcUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VappResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VappModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 26, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "nsxtUid", Field: "NsxtUid", Type: "string"},
+ {Code: "vappName", Field: "VappName", Type: "string"},
+ {Code: "vdcUid", Field: "VdcUid", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !newState.NsxtUid.IsNull() && !newState.NsxtUid.IsUnknown() {
+ resolvedNsxtUid, err := r.client.ResolveRefSvcParamValue(ctx, 22, newState.NsxtUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve nsxt_uid", err.Error())
+ } else if resolvedNsxtUid != "" && resolvedNsxtUid != newState.NsxtUid.ValueString() {
+ newState.NsxtUid = types.StringValue(resolvedNsxtUid)
+ }
+ }
+ if !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, newState.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != newState.VdcUid.ValueString() {
+ newState.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+ if !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() && !newState.NsxtUid.IsNull() && !newState.NsxtUid.IsUnknown() {
+ if strings.EqualFold(newState.NsxtUid.ValueString(), state.NsxtUid.ValueString()) {
+ newState.NsxtUid = state.NsxtUid
+ }
+ }
+ if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() && !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() {
+ if strings.EqualFold(newState.VdcUid.ValueString(), state.VdcUid.ValueString()) {
+ newState.VdcUid = state.VdcUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VappResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VappModel
+ var state VappModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 26, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "nsxtUid", Field: "NsxtUid", Type: "string"},
+ {Code: "vappName", Field: "VappName", Type: "string"},
+ {Code: "vdcUid", Field: "VdcUid", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() {
+ resolvedNsxtUid, err := r.client.ResolveRefSvcParamValue(ctx, 22, state.NsxtUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve nsxt_uid", err.Error())
+ } else if resolvedNsxtUid != "" && resolvedNsxtUid != state.NsxtUid.ValueString() {
+ state.NsxtUid = types.StringValue(resolvedNsxtUid)
+ }
+ }
+ if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() {
+ state.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+ if !plan.NsxtUid.IsNull() && !plan.NsxtUid.IsUnknown() && !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() {
+ if strings.EqualFold(state.NsxtUid.ValueString(), plan.NsxtUid.ValueString()) {
+ state.NsxtUid = plan.NsxtUid
+ }
+ }
+ if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ if strings.EqualFold(state.VdcUid.ValueString(), plan.VdcUid.ValueString()) {
+ state.VdcUid = plan.VdcUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VappResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VappModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VappResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VappResource) 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
+}
diff --git a/generated/test/go/27_vc_vm_v2_resource.go b/generated/test/go/27_vc_vm_v2_resource.go
new file mode 100644
index 0000000..0fe1861
--- /dev/null
+++ b/generated/test/go/27_vc_vm_v2_resource.go
@@ -0,0 +1,350 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_vm_v2
+// Service ID: 27
+
+var _ resource.Resource = &VcVmV2Resource{}
+var _ resource.ResourceWithModifyPlan = &VcVmV2Resource{}
+var _ resource.ResourceWithImportState = &VcVmV2Resource{}
+
+type VcVmV2Resource struct {
+ client *core.UniversalClient
+}
+
+type VcVmV2Model struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ ServerListJson types.String `tfsdk:"server_list_json"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVcVmV2Resource() resource.Resource {
+ return &VcVmV2Resource{}
+}
+
+func (r *VcVmV2Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_vm_v2"
+}
+
+func (r *VcVmV2Resource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "server_list_json": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Необходимо изменить vappUid и ipSpaceName
Пример конфигурации взять по ссылке ниже
Объект серверов вида [{}] (Map(Object))
Более подробнее смотреть в README.md (https://gitea.mgmt.nubes.ru/cloudServices/vcVmV2)", PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()}},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcVmV2Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VcVmV2Model
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VcVmV2Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VcVmV2Model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 301: resources_core.FormatString(config.ServerListJson),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 27, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VcVmV2Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VcVmV2Model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 27, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 301: resources_core.FormatString(data.ServerListJson),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 27, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 27, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "serverListJson", Field: "ServerListJson", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcVmV2Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcVmV2Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 27, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "serverListJson", Field: "ServerListJson", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VcVmV2Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcVmV2Model
+ var state VcVmV2Model
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.ServerListJson.IsNull() != state.ServerListJson.IsNull() || plan.ServerListJson.IsUnknown() != state.ServerListJson.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ServerListJson.IsNull() && !plan.ServerListJson.IsUnknown() && !state.ServerListJson.IsNull() && !state.ServerListJson.IsUnknown() {
+ if !strings.EqualFold(plan.ServerListJson.ValueString(), state.ServerListJson.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 302: resources_core.FormatString(plan.ServerListJson),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 27, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "serverListJson", Field: "ServerListJson", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcVmV2Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcVmV2Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VcVmV2Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VcVmV2Resource) 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
+}
diff --git a/generated/test/go/28_vc_vm_v3_resource.go b/generated/test/go/28_vc_vm_v3_resource.go
new file mode 100644
index 0000000..58d967f
--- /dev/null
+++ b/generated/test/go/28_vc_vm_v3_resource.go
@@ -0,0 +1,697 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_vm_v3
+// Service ID: 28
+
+var _ resource.Resource = &VcVmV3Resource{}
+var _ resource.ResourceWithModifyPlan = &VcVmV3Resource{}
+var _ resource.ResourceWithImportState = &VcVmV3Resource{}
+
+type VcVmV3Resource struct {
+ client *core.UniversalClient
+}
+
+type VcVmV3Model struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // git_revision: при изменении вызывает redeploy вместо modify.
+ // Опциональное поле — если не задано, modify работает как обычно.
+ GitRevision types.String `tfsdk:"git_revision"`
+ AccessIpList types.String `tfsdk:"access_ip_list"`
+ AccessPortList types.String `tfsdk:"access_port_list"`
+ CloudInit types.String `tfsdk:"cloud_init"`
+ ImageVm types.String `tfsdk:"image_vm"`
+ IpSpaceName types.String `tfsdk:"ip_space_name"`
+ UserLogin types.String `tfsdk:"user_login"`
+ UserPublicKey types.String `tfsdk:"user_public_key"`
+ VappUid types.String `tfsdk:"vapp_uid"`
+ VmCpu types.Int64 `tfsdk:"vm_cpu"`
+ VmDisk types.Int64 `tfsdk:"vm_disk"`
+ VmName types.String `tfsdk:"vm_name"`
+ VmRam types.Int64 `tfsdk:"vm_ram"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVcVmV3Resource() resource.Resource {
+ return &VcVmV3Resource{}
+}
+
+func (r *VcVmV3Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_vm_v3"
+}
+
+func (r *VcVmV3Resource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_ip_list": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Формат массива
Каким адресам будет предоставлен доступ до вм
Требует выделенного внешнего IP
Если ничего не передано, то выставлено ["0.0.0.0/0"]", PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()}},
+ "access_port_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[\"22\"]"), MarkdownDescription: "По каким портам открыть доступ к ВМ.
Порты 22, 10050 проставляются автоматически
Пока доступно:
22 - SSH,
80 - HTTP,
443 - HTTPS,
10050 - ZABBIX"},
+ "cloud_init": schema.StringAttribute{Optional: true, MarkdownDescription: "Можно передать создание дополнительных пользователей.
На текущий момент не реализовано выполнения в cloud-init, установки пакетов, выполнения команд и записи файлов. Не изменяется после создания."},
+ "image_vm": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания."},
+ "ip_space_name": schema.StringAttribute{Required: true},
+ "user_login": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("myuser"), MarkdownDescription: "Учётка SSH Не изменяется после создания."},
+ "user_public_key": schema.StringAttribute{Required: true, MarkdownDescription: "Публичная часть SSH-Ключа для пользователя Не изменяется после создания."},
+ "vapp_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UI display_name for service_id=26 (mapped to UUID in core) Не изменяется после создания."},
+ "vm_cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "шт"},
+ "vm_disk": schema.Int64Attribute{Optional: true, MarkdownDescription: "Основной диск у vm зависит от образа (см. документацию)
Если необходимо дополнительное место для vm, введите колличество Gb в эту форму"},
+ "vm_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("web01"), MarkdownDescription: "Не изменяется после создания."},
+ "vm_ram": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Gb"},
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // При изменении git_revision вызывается redeploy вместо modify.
+ // Если не задано — modify работает без изменений.
+ "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcVmV3Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VcVmV3Model
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VcVmV3Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VcVmV3Model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.CloudInit.IsNull() && !plan.CloudInit.IsUnknown() && !state.CloudInit.IsNull() && !state.CloudInit.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.CloudInit.ValueString(), state.CloudInit.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить cloud_init", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ImageVm.IsNull() && !plan.ImageVm.IsUnknown() && !state.ImageVm.IsNull() && !state.ImageVm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ImageVm.ValueString(), state.ImageVm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить image_vm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.UserLogin.IsNull() && !plan.UserLogin.IsUnknown() && !state.UserLogin.IsNull() && !state.UserLogin.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.UserLogin.ValueString(), state.UserLogin.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить user_login", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.UserPublicKey.IsNull() && !plan.UserPublicKey.IsUnknown() && !state.UserPublicKey.IsNull() && !state.UserPublicKey.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.UserPublicKey.ValueString(), state.UserPublicKey.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить user_public_key", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VappUid.IsNull() && !plan.VappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VappUid.ValueString(), state.VappUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vapp_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.VmName.IsNull() && !plan.VmName.IsUnknown() && !state.VmName.IsNull() && !state.VmName.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VmName.ValueString(), state.VmName.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vm_name", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.ImageVm.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "image_vm is required.")
+ return
+ }
+ if config.ImageVm.IsUnknown() {
+ return
+ }
+ if config.IpSpaceName.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.")
+ return
+ }
+ if config.IpSpaceName.IsUnknown() {
+ return
+ }
+ if config.UserPublicKey.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "user_public_key is required.")
+ return
+ }
+ if config.UserPublicKey.IsUnknown() {
+ return
+ }
+ if config.VappUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "vapp_uid is required.")
+ return
+ }
+ if config.VappUid.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.VappUid.IsNull() && !config.VappUid.IsUnknown() {
+ resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, config.VappUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error())
+ } else if resolvedVappUid != "" && resolvedVappUid != config.VappUid.ValueString() {
+ config.VappUid = types.StringValue(resolvedVappUid)
+ }
+ }
+ params := map[int]string{
+ 407: resources_core.FormatString(config.VappUid),
+ 408: resources_core.FormatString(config.VmName),
+ 409: resources_core.FormatInt64(config.VmCpu),
+ 410: resources_core.FormatInt64(config.VmRam),
+ 411: resources_core.FormatInt64(config.VmDisk),
+ 412: resources_core.FormatString(config.IpSpaceName),
+ 413: resources_core.FormatString(config.AccessIpList),
+ 414: resources_core.FormatString(config.ImageVm),
+ 415: resources_core.FormatString(config.CloudInit),
+ 416: resources_core.FormatString(config.UserLogin),
+ 417: resources_core.FormatString(config.UserPublicKey),
+ 448: resources_core.FormatString(config.AccessPortList),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 28, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VcVmV3Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VcVmV3Model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.ImageVm.IsNull() || data.ImageVm.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "image_vm is required.")
+ return
+ }
+ if data.IpSpaceName.IsNull() || data.IpSpaceName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.")
+ return
+ }
+ if data.UserPublicKey.IsNull() || data.UserPublicKey.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "user_public_key is required.")
+ return
+ }
+ if data.VappUid.IsNull() || data.VappUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "vapp_uid is required.")
+ return
+ }
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalVappUid := data.VappUid
+ if !data.VappUid.IsNull() && !data.VappUid.IsUnknown() {
+ resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, data.VappUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedVappUid != "" && resolvedVappUid != data.VappUid.ValueString() {
+ data.VappUid = types.StringValue(resolvedVappUid)
+ }
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 28, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 407: resources_core.FormatString(data.VappUid),
+ 408: resources_core.FormatString(data.VmName),
+ 409: resources_core.FormatInt64(data.VmCpu),
+ 410: resources_core.FormatInt64(data.VmRam),
+ 411: resources_core.FormatInt64(data.VmDisk),
+ 412: resources_core.FormatString(data.IpSpaceName),
+ 413: resources_core.FormatString(data.AccessIpList),
+ 414: resources_core.FormatString(data.ImageVm),
+ 415: resources_core.FormatString(data.CloudInit),
+ 416: resources_core.FormatString(data.UserLogin),
+ 417: resources_core.FormatString(data.UserPublicKey),
+ 448: resources_core.FormatString(data.AccessPortList),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 28, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 28, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessIpList", Field: "AccessIpList", Type: "string"},
+ {Code: "accessPortList", Field: "AccessPortList", Type: "string"},
+ {Code: "cloudInit", Field: "CloudInit", Type: "string"},
+ {Code: "imageVm", Field: "ImageVm", Type: "string"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "userLogin", Field: "UserLogin", Type: "string"},
+ {Code: "userPublicKey", Field: "UserPublicKey", Type: "string"},
+ {Code: "vappUid", Field: "VappUid", Type: "string"},
+ {Code: "vmCpu", Field: "VmCpu", Type: "int64"},
+ {Code: "vmDisk", Field: "VmDisk", Type: "int64"},
+ {Code: "vmName", Field: "VmName", Type: "string"},
+ {Code: "vmRam", Field: "VmRam", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() {
+ resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, state.VappUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error())
+ } else if resolvedVappUid != "" && resolvedVappUid != state.VappUid.ValueString() {
+ state.VappUid = types.StringValue(resolvedVappUid)
+ }
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+ if !originalVappUid.IsNull() && !originalVappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() {
+ if strings.EqualFold(state.VappUid.ValueString(), originalVappUid.ValueString()) {
+ state.VappUid = originalVappUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcVmV3Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcVmV3Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 28, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessIpList", Field: "AccessIpList", Type: "string"},
+ {Code: "accessPortList", Field: "AccessPortList", Type: "string"},
+ {Code: "cloudInit", Field: "CloudInit", Type: "string"},
+ {Code: "imageVm", Field: "ImageVm", Type: "string"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "userLogin", Field: "UserLogin", Type: "string"},
+ {Code: "userPublicKey", Field: "UserPublicKey", Type: "string"},
+ {Code: "vappUid", Field: "VappUid", Type: "string"},
+ {Code: "vmCpu", Field: "VmCpu", Type: "int64"},
+ {Code: "vmDisk", Field: "VmDisk", Type: "int64"},
+ {Code: "vmName", Field: "VmName", Type: "string"},
+ {Code: "vmRam", Field: "VmRam", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !newState.VappUid.IsNull() && !newState.VappUid.IsUnknown() {
+ resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, newState.VappUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error())
+ } else if resolvedVappUid != "" && resolvedVappUid != newState.VappUid.ValueString() {
+ newState.VappUid = types.StringValue(resolvedVappUid)
+ }
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+ if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() && !newState.VappUid.IsNull() && !newState.VappUid.IsUnknown() {
+ if strings.EqualFold(newState.VappUid.ValueString(), state.VappUid.ValueString()) {
+ newState.VappUid = state.VappUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VcVmV3Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcVmV3Model
+ var state VcVmV3Model
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.VmCpu.IsNull() != state.VmCpu.IsNull() || plan.VmCpu.IsUnknown() != state.VmCpu.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.VmCpu.IsNull() && !plan.VmCpu.IsUnknown() && !state.VmCpu.IsNull() && !state.VmCpu.IsUnknown() {
+ if plan.VmCpu.ValueInt64() != state.VmCpu.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.VmRam.IsNull() != state.VmRam.IsNull() || plan.VmRam.IsUnknown() != state.VmRam.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.VmRam.IsNull() && !plan.VmRam.IsUnknown() && !state.VmRam.IsNull() && !state.VmRam.IsUnknown() {
+ if plan.VmRam.ValueInt64() != state.VmRam.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.VmDisk.IsNull() != state.VmDisk.IsNull() || plan.VmDisk.IsUnknown() != state.VmDisk.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.VmDisk.IsNull() && !plan.VmDisk.IsUnknown() && !state.VmDisk.IsNull() && !state.VmDisk.IsUnknown() {
+ if plan.VmDisk.ValueInt64() != state.VmDisk.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.IpSpaceName.IsNull() != state.IpSpaceName.IsNull() || plan.IpSpaceName.IsUnknown() != state.IpSpaceName.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.IpSpaceName.IsNull() && !plan.IpSpaceName.IsUnknown() && !state.IpSpaceName.IsNull() && !state.IpSpaceName.IsUnknown() {
+ if !strings.EqualFold(plan.IpSpaceName.ValueString(), state.IpSpaceName.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.AccessIpList.IsNull() != state.AccessIpList.IsNull() || plan.AccessIpList.IsUnknown() != state.AccessIpList.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.AccessIpList.IsNull() && !plan.AccessIpList.IsUnknown() && !state.AccessIpList.IsNull() && !state.AccessIpList.IsUnknown() {
+ if !strings.EqualFold(plan.AccessIpList.ValueString(), state.AccessIpList.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.AccessPortList.IsNull() != state.AccessPortList.IsNull() || plan.AccessPortList.IsUnknown() != state.AccessPortList.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.AccessPortList.IsNull() && !plan.AccessPortList.IsUnknown() && !state.AccessPortList.IsNull() && !state.AccessPortList.IsUnknown() {
+ if !strings.EqualFold(plan.AccessPortList.ValueString(), state.AccessPortList.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // Проверяем изменился ли git_revision.
+ // Если да — вызываем redeploy вместо modify.
+ // git_revision = "" (не задано) → обычный modify.
+ redeployRequested := false
+ if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() {
+ if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() {
+ redeployRequested = true
+ }
+ }
+
+ if !hasServiceParamChanges && !redeployRequested {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ if redeployRequested {
+ // Вызываем redeploy — пустой params (операция не требует CFS-параметров)
+ if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ } else {
+ params := map[int]string{
+ 493: resources_core.FormatInt64(plan.VmCpu),
+ 494: resources_core.FormatInt64(plan.VmRam),
+ 495: resources_core.FormatInt64(plan.VmDisk),
+ 496: resources_core.FormatString(plan.IpSpaceName),
+ 497: resources_core.FormatString(plan.AccessIpList),
+ 498: resources_core.FormatString(plan.AccessPortList),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 28, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessIpList", Field: "AccessIpList", Type: "string"},
+ {Code: "accessPortList", Field: "AccessPortList", Type: "string"},
+ {Code: "cloudInit", Field: "CloudInit", Type: "string"},
+ {Code: "imageVm", Field: "ImageVm", Type: "string"},
+ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"},
+ {Code: "userLogin", Field: "UserLogin", Type: "string"},
+ {Code: "userPublicKey", Field: "UserPublicKey", Type: "string"},
+ {Code: "vappUid", Field: "VappUid", Type: "string"},
+ {Code: "vmCpu", Field: "VmCpu", Type: "int64"},
+ {Code: "vmDisk", Field: "VmDisk", Type: "int64"},
+ {Code: "vmName", Field: "VmName", Type: "string"},
+ {Code: "vmRam", Field: "VmRam", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() {
+ resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, state.VappUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error())
+ } else if resolvedVappUid != "" && resolvedVappUid != state.VappUid.ValueString() {
+ state.VappUid = types.StringValue(resolvedVappUid)
+ }
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+ if !plan.VappUid.IsNull() && !plan.VappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() {
+ if strings.EqualFold(state.VappUid.ValueString(), plan.VappUid.ValueString()) {
+ state.VappUid = plan.VappUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcVmV3Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcVmV3Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VcVmV3Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VcVmV3Resource) 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
+}
diff --git a/generated/test/go/29_vc_vdc_group_resource.go b/generated/test/go/29_vc_vdc_group_resource.go
new file mode 100644
index 0000000..4166b73
--- /dev/null
+++ b/generated/test/go/29_vc_vdc_group_resource.go
@@ -0,0 +1,449 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_vdc_group
+// Service ID: 29
+
+var _ resource.Resource = &VcVdcGroupResource{}
+var _ resource.ResourceWithModifyPlan = &VcVdcGroupResource{}
+var _ resource.ResourceWithImportState = &VcVdcGroupResource{}
+
+type VcVdcGroupResource struct {
+ client *core.UniversalClient
+}
+
+type VcVdcGroupModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ VdcUid types.String `tfsdk:"vdc_uid"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVcVdcGroupResource() resource.Resource {
+ return &VcVdcGroupResource{}
+}
+
+func (r *VcVdcGroupResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_vdc_group"
+}
+
+func (r *VcVdcGroupResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "vdc_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UI display_name for service_id=21 (mapped to UUID in core) Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcVdcGroupResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VcVdcGroupModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VcVdcGroupModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VcVdcGroupModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.VdcUid.ValueString(), state.VdcUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить vdc_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.VdcUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "vdc_uid is required.")
+ return
+ }
+ if config.VdcUid.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.VdcUid.IsNull() && !config.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, config.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != config.VdcUid.ValueString() {
+ config.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ params := map[int]string{
+ 618: resources_core.FormatString(config.VdcUid),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 29, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VcVdcGroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VcVdcGroupModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.VdcUid.IsNull() || data.VdcUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "vdc_uid is required.")
+ return
+ }
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalVdcUid := data.VdcUid
+ if !data.VdcUid.IsNull() && !data.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, data.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedVdcUid != "" && resolvedVdcUid != data.VdcUid.ValueString() {
+ data.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 29, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 618: resources_core.FormatString(data.VdcUid),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 29, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 29, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "vdcUid", Field: "VdcUid", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() {
+ state.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+ if !originalVdcUid.IsNull() && !originalVdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ if strings.EqualFold(state.VdcUid.ValueString(), originalVdcUid.ValueString()) {
+ state.VdcUid = originalVdcUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcVdcGroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcVdcGroupModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 29, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "vdcUid", Field: "VdcUid", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, newState.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != newState.VdcUid.ValueString() {
+ newState.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+ if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() && !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() {
+ if strings.EqualFold(newState.VdcUid.ValueString(), state.VdcUid.ValueString()) {
+ newState.VdcUid = state.VdcUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VcVdcGroupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcVdcGroupModel
+ var state VcVdcGroupModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 29, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "vdcUid", Field: "VdcUid", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error())
+ } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() {
+ state.VdcUid = types.StringValue(resolvedVdcUid)
+ }
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+ if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() {
+ if strings.EqualFold(state.VdcUid.ValueString(), plan.VdcUid.ValueString()) {
+ state.VdcUid = plan.VdcUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcVdcGroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcVdcGroupModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VcVdcGroupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VcVdcGroupResource) 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
+}
diff --git a/generated/test/go/29_vc_vdc_group_vdc_resource.go b/generated/test/go/29_vc_vdc_group_vdc_resource.go
new file mode 100644
index 0000000..3e3e8ef
--- /dev/null
+++ b/generated/test/go/29_vc_vdc_group_vdc_resource.go
@@ -0,0 +1,326 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vc_vdc_group
+// Subresource: vdc
+
+var _ resource.Resource = &VcVdcGroupVdcResource{}
+
+// VcVdcGroupVdcResource manages a subresource via operations.
+type VcVdcGroupVdcResource struct {
+ client *core.UniversalClient
+}
+
+type VcVdcGroupVdcModel struct {
+ ID types.String `tfsdk:"id"`
+ VcVdcGroupID types.String `tfsdk:"vc_vdc_group_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"`
+}
+
+func NewVcVdcGroupVdcResource() resource.Resource {
+ return &VcVdcGroupVdcResource{}
+}
+
+func (r *VcVdcGroupVdcResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vc_vdc_group_vdc"
+}
+
+func (r *VcVdcGroupVdcResource) 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()}},
+ "vc_vdc_group_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."},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VcVdcGroupVdcResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan VcVdcGroupVdcModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.VcVdcGroupID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{}
+ identityCodes := []string{}
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("vdc", 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, "vdc", 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{})
+
+ 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, "", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", 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, "vdc", 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, "vdc", 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, "vdc", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *VcVdcGroupVdcResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VcVdcGroupVdcModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VcVdcGroupVdcResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VcVdcGroupVdcModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state VcVdcGroupVdcModel
+ 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.VcVdcGroupID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{}
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "vdc", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *VcVdcGroupVdcResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VcVdcGroupVdcModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.VcVdcGroupID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{}
+ identityCodes := []string{}
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("vdc", 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{})
+
+ 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, "", 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 *VcVdcGroupVdcResource) 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
+}
diff --git a/generated/test/go/2_template_resource.go b/generated/test/go/2_template_resource.go
new file mode 100644
index 0000000..b45b9e6
--- /dev/null
+++ b/generated/test/go/2_template_resource.go
@@ -0,0 +1,397 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: template
+// Service ID: 2
+
+var _ resource.Resource = &TemplateResource{}
+var _ resource.ResourceWithModifyPlan = &TemplateResource{}
+var _ resource.ResourceWithImportState = &TemplateResource{}
+
+type TemplateResource struct {
+ client *core.UniversalClient
+}
+
+// TemplateAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type TemplateAccessConfigurationModel struct {
+ Type types.String `tfsdk:"type" json:"type"`
+ Domain types.String `tfsdk:"domain" json:"domain"`
+ MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"`
+ MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"`
+ SlaveIpSpace types.String `tfsdk:"slave_ip_space" json:"slaveIpSpace"`
+ SlaveAccessList types.String `tfsdk:"slave_access_list" json:"slaveAccessList"`
+}
+
+// TemplateClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type TemplateClusterConfigurationModel struct {
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+ Disk types.Int64 `tfsdk:"disk" json:"disk"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+}
+
+// TemplateStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type TemplateStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+}
+
+type TemplateModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AccessConfiguration *TemplateAccessConfigurationModel `tfsdk:"access_configuration"`
+ ClusterConfiguration *TemplateClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ StartupConfiguration *TemplateStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewTemplateResource() resource.Resource {
+ return &TemplateResource{}
+}
+
+func (r *TemplateResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_template"
+}
+
+func (r *TemplateResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "type": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("ingress")},
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("abracadabra-pin1v6zw")},
+ "master_ip_space": schema.StringAttribute{Required: true},
+ "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]")},
+ "slave_ip_space": schema.StringAttribute{Required: true},
+ "slave_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]")},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100)},
+ "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(5)},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(128)},
+ }},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Required: true},
+ }},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *TemplateResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *TemplateModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *TemplateModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan TemplateModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 813: resources_core.BuildJSON(map[string]string{"replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64())}),
+ 814: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}),
+ 815: resources_core.BuildJSON(map[string]string{"type": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Type.ValueString()), "domain": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Domain.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveAccessList.ValueString())}),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 2, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *TemplateResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data TemplateModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 2, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 813: resources_core.BuildJSON(map[string]string{"replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64())}),
+ 814: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}),
+ 815: resources_core.BuildJSON(map[string]string{"type": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Type.ValueString()), "domain": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Domain.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveAccessList.ValueString())}),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 2, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 2, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *TemplateResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state TemplateModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 2, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan TemplateModel
+ var state TemplateModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 816: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64())}),
+ 817: resources_core.BuildJSON(map[string]string{"type": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.Type.ValueString()), "domain": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.Domain.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveAccessList.ValueString())}),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 2, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *TemplateResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state TemplateModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *TemplateResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *TemplateResource) 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
+}
diff --git a/generated/test/go/32_vmpostgre_resource.go b/generated/test/go/32_vmpostgre_resource.go
new file mode 100644
index 0000000..14a037e
--- /dev/null
+++ b/generated/test/go/32_vmpostgre_resource.go
@@ -0,0 +1,322 @@
+package resources_gen
+
+import (
+ "context"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: vmpostgre
+// Service ID: 32
+
+var _ resource.Resource = &VmpostgreResource{}
+var _ resource.ResourceWithModifyPlan = &VmpostgreResource{}
+var _ resource.ResourceWithImportState = &VmpostgreResource{}
+
+type VmpostgreResource struct {
+ client *core.UniversalClient
+}
+
+type VmpostgreModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewVmpostgreResource() resource.Resource {
+ return &VmpostgreResource{}
+}
+
+func (r *VmpostgreResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vmpostgre"
+}
+
+func (r *VmpostgreResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *VmpostgreResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *VmpostgreModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *VmpostgreModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan VmpostgreModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{}
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 32, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *VmpostgreResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data VmpostgreModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 32, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{}
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 32, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 32, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{})
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VmpostgreResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state VmpostgreModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 32, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{})
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *VmpostgreResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan VmpostgreModel
+ var state VmpostgreModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 32, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{})
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *VmpostgreResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state VmpostgreModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *VmpostgreResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *VmpostgreResource) 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
+}
diff --git a/generated/test/go/50_nextcloud_backup_resource.go b/generated/test/go/50_nextcloud_backup_resource.go
new file mode 100644
index 0000000..feb1f0e
--- /dev/null
+++ b/generated/test/go/50_nextcloud_backup_resource.go
@@ -0,0 +1,344 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: nextcloud
+// Subresource: backup
+
+var _ resource.Resource = &NextcloudBackupResource{}
+
+// NextcloudBackupResource manages a subresource via operations.
+type NextcloudBackupResource struct {
+ client *core.UniversalClient
+}
+
+type NextcloudBackupModel struct {
+ ID types.String `tfsdk:"id"`
+ NextcloudID types.String `tfsdk:"nextcloud_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"`
+ StartupConfiguration types.String `tfsdk:"startup_configuration"`
+}
+
+func NewNextcloudBackupResource() resource.Resource {
+ return &NextcloudBackupResource{}
+}
+
+func (r *NextcloudBackupResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_nextcloud_backup"
+}
+
+func (r *NextcloudBackupResource) 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()}},
+ "nextcloud_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."},
+ "startup_configuration": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *NextcloudBackupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan NextcloudBackupModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if plan.StartupConfiguration.IsNull() || plan.StartupConfiguration.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "startup_configuration is required.")
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.NextcloudID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "startupConfiguration": resources_core.FormatString(plan.StartupConfiguration),
+ }
+ identityCodes := []string{
+ "startupConfiguration",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("backup", 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, "backup", 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{
+ "startupConfiguration": resources_core.FormatString(plan.StartupConfiguration),
+ })
+
+ 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, "create_backup", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_backup", 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, "backup", 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, "backup", 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, "backup", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *NextcloudBackupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state NextcloudBackupModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *NextcloudBackupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan NextcloudBackupModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state NextcloudBackupModel
+ 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.NextcloudID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "startupConfiguration": resources_core.FormatString(plan.StartupConfiguration),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *NextcloudBackupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state NextcloudBackupModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.NextcloudID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "startupConfiguration": resources_core.FormatString(state.StartupConfiguration),
+ }
+ identityCodes := []string{
+ "startupConfiguration",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("backup", 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{})
+
+ 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, "", 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 *NextcloudBackupResource) 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
+}
diff --git a/generated/test/go/50_nextcloud_resource.go b/generated/test/go/50_nextcloud_resource.go
new file mode 100644
index 0000000..43d2c4e
--- /dev/null
+++ b/generated/test/go/50_nextcloud_resource.go
@@ -0,0 +1,414 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: nextcloud
+// Service ID: 50
+
+var _ resource.Resource = &NextcloudResource{}
+var _ resource.ResourceWithModifyPlan = &NextcloudResource{}
+var _ resource.ResourceWithImportState = &NextcloudResource{}
+
+type NextcloudResource struct {
+ client *core.UniversalClient
+}
+
+// NextcloudAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type NextcloudAccessConfigurationModel struct {
+ Domain types.String `tfsdk:"domain" json:"domain"`
+}
+
+// NextcloudAppConfigurationModel — вложенная модель для map-fixed параметра appConfiguration.
+type NextcloudAppConfigurationModel struct {
+ Version types.String `tfsdk:"version" json:"version"`
+ DocumentServer types.String `tfsdk:"document_server" json:"documentServer"`
+ LogLevel types.String `tfsdk:"log_level" json:"logLevel"`
+}
+
+// NextcloudClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type NextcloudClusterConfigurationModel struct {
+ Disk types.Int64 `tfsdk:"disk" json:"disk"`
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+}
+
+// NextcloudStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type NextcloudStartupConfigurationModel struct {
+ PsqlEnabled types.Bool `tfsdk:"psql_enabled" json:"psqlEnabled"`
+ RedisEnabled types.Bool `tfsdk:"redis_enabled" json:"redisEnabled"`
+ PsqlUid types.String `tfsdk:"psql_uid" json:"psqlUid"`
+ RedisUid types.String `tfsdk:"redis_uid" json:"redisUid"`
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+}
+
+type NextcloudModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AccessConfiguration *NextcloudAccessConfigurationModel `tfsdk:"access_configuration"`
+ AppConfiguration *NextcloudAppConfigurationModel `tfsdk:"app_configuration"`
+ ClusterConfiguration *NextcloudClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ StartupConfiguration *NextcloudStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewNextcloudResource() resource.Resource {
+ return &NextcloudResource{}
+}
+
+func (r *NextcloudResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_nextcloud"
+}
+
+func (r *NextcloudResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("nextcloud-c2xizgvp"), MarkdownDescription: "Если указан FQDN в формате: `myapp.example.com`, предварительно будет проверяться DNS-запись (А-запись **должна смотреть на IP Ingress кластера ресурсной платформы**)
Если указан в формате `myapp`, DNS-запись будет автоматически создана в служебной DNS-зоне"},
+ }},
+ "app_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("33.0.4")},
+ "document_server": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("collabora"), MarkdownDescription: "Выбор сервера документов (onlyoffice)/(collabora). onlyoffice пока не доступен
Размер пода collabora:
1.8 ядра. 2Gi ОЗУ"},
+ "log_level": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("INFO"), MarkdownDescription: "Включает отображение логов самого nextcloud
Логи уходят в /dev/stdout контейнера
`NONE` отключает логирование"},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Указывается в GIGAbytes
На данном диске хранятся данные Nextcloud"},
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1000), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1024), MarkdownDescription: "Указывается в Megabytes"},
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ }},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "psql_enabled": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Инстанс обязан быть развёрнут на той же ресурсной платформе, где и будет располагаться Nextcloud"},
+ "redis_enabled": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Инстанс обязан быть развёрнут на той же ресурсной платформе, где и будет располагаться Nextcloud"},
+ "psql_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Необходимо выбирать, когда выбран параметр `Подключить PostgreSQL из Личного кабинета`"},
+ "redis_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Необходимо выбирать, когда выбран параметр `Подключить Redis из Личного кабинета`"},
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"},
+ }},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *NextcloudResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *NextcloudModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *NextcloudModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan NextcloudModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 1055: resources_core.BuildJSON(map[string]string{"psqlEnabled": fmt.Sprintf("%v", config.StartupConfiguration.PsqlEnabled.ValueBool()), "redisEnabled": fmt.Sprintf("%v", config.StartupConfiguration.RedisEnabled.ValueBool()), "psqlUid": fmt.Sprintf("\"%s\"", config.StartupConfiguration.PsqlUid.ValueString()), "redisUid": fmt.Sprintf("\"%s\"", config.StartupConfiguration.RedisUid.ValueString()), "resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1056: resources_core.BuildJSON(map[string]string{"disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64())}),
+ 1057: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Domain.ValueString())}),
+ 1058: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.AppConfiguration.Version.ValueString()), "documentServer": fmt.Sprintf("\"%s\"", config.AppConfiguration.DocumentServer.ValueString()), "logLevel": fmt.Sprintf("\"%s\"", config.AppConfiguration.LogLevel.ValueString())}),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 50, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *NextcloudResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data NextcloudModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 50, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 1055: resources_core.BuildJSON(map[string]string{"psqlEnabled": fmt.Sprintf("%v", data.StartupConfiguration.PsqlEnabled.ValueBool()), "redisEnabled": fmt.Sprintf("%v", data.StartupConfiguration.RedisEnabled.ValueBool()), "psqlUid": fmt.Sprintf("\"%s\"", data.StartupConfiguration.PsqlUid.ValueString()), "redisUid": fmt.Sprintf("\"%s\"", data.StartupConfiguration.RedisUid.ValueString()), "resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1056: resources_core.BuildJSON(map[string]string{"disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64())}),
+ 1057: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Domain.ValueString())}),
+ 1058: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.AppConfiguration.Version.ValueString()), "documentServer": fmt.Sprintf("\"%s\"", data.AppConfiguration.DocumentServer.ValueString()), "logLevel": fmt.Sprintf("\"%s\"", data.AppConfiguration.LogLevel.ValueString())}),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 50, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 50, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *NextcloudResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state NextcloudModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 50, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *NextcloudResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan NextcloudModel
+ var state NextcloudModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 1059: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64())}),
+ 1060: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.AppConfiguration.Version.ValueString()), "documentServer": fmt.Sprintf("\"%s\"", plan.AppConfiguration.DocumentServer.ValueString()), "logLevel": fmt.Sprintf("\"%s\"", plan.AppConfiguration.LogLevel.ValueString())}),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 50, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *NextcloudResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state NextcloudModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *NextcloudResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *NextcloudResource) 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
+}
diff --git a/generated/test/go/81_superset_resource.go b/generated/test/go/81_superset_resource.go
new file mode 100644
index 0000000..c66e1d8
--- /dev/null
+++ b/generated/test/go/81_superset_resource.go
@@ -0,0 +1,447 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: superset
+// Service ID: 81
+
+var _ resource.Resource = &SupersetResource{}
+var _ resource.ResourceWithModifyPlan = &SupersetResource{}
+var _ resource.ResourceWithImportState = &SupersetResource{}
+
+type SupersetResource struct {
+ client *core.UniversalClient
+}
+
+type SupersetModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ Domain types.String `tfsdk:"domain"`
+ Emails types.String `tfsdk:"emails"`
+ ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"`
+ ResourceDisk types.Int64 `tfsdk:"resource_disk"`
+ ResourceInstances types.Int64 `tfsdk:"resource_instances"`
+ ResourceMemory types.Int64 `tfsdk:"resource_memory"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewSupersetResource() resource.Resource {
+ return &SupersetResource{}
+}
+
+func (r *SupersetResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_superset"
+}
+
+func (r *SupersetResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("superset-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."},
+ "emails": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("testuser1"), MarkdownDescription: "Учётная запись, которой будут выданы админские права. В формате email [ну не совсем. Для доменных учетных записей - без доменного суффикса]. Обязана находиться в Keycloak. Можно передавать несколько через пробел Не изменяется после создания."},
+ "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Квота ядра пода (Указывается в mili). 1000 Mili = 1 ядро Не изменяется после создания."},
+ "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Квота диска (Указывается в GIGAbytes) Не изменяется после создания."},
+ "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов Не изменяется после создания."},
+ "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Квота памяти пода (Указывается в Megabytes) Не изменяется после создания."},
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *SupersetResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *SupersetModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *SupersetModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan SupersetModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.Emails.IsNull() && !plan.Emails.IsUnknown() && !state.Emails.IsNull() && !state.Emails.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Emails.ValueString(), state.Emails.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить emails", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() {
+ if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_c_p_u", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() {
+ if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_disk", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() {
+ if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_instances", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() {
+ if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_memory", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.ResourceRealm.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+ if config.ResourceRealm.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 14: resources_core.FormatString(config.Domain),
+ 20: resources_core.FormatString(config.Emails),
+ 96: resources_core.FormatInt64(config.ResourceCPU),
+ 97: resources_core.FormatInt64(config.ResourceMemory),
+ 98: resources_core.FormatInt64(config.ResourceDisk),
+ 109: resources_core.FormatString(config.ResourceRealm),
+ 227: resources_core.FormatInt64(config.ResourceInstances),
+ }
+ desiredDomain := ""
+ if !config.Domain.IsNull() && !config.Domain.IsUnknown() {
+ desiredDomain = config.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 81, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *SupersetResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data SupersetModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ if !data.Domain.IsNull() && !data.Domain.IsUnknown() {
+ desiredDomain = data.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 81, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 14: resources_core.FormatString(data.Domain),
+ 20: resources_core.FormatString(data.Emails),
+ 96: resources_core.FormatInt64(data.ResourceCPU),
+ 97: resources_core.FormatInt64(data.ResourceMemory),
+ 98: resources_core.FormatInt64(data.ResourceDisk),
+ 109: resources_core.FormatString(data.ResourceRealm),
+ 227: resources_core.FormatInt64(data.ResourceInstances),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 81, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 81, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "emails", Field: "Emails", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *SupersetResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state SupersetModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 81, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "emails", Field: "Emails", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *SupersetResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan SupersetModel
+ var state SupersetModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 81, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "emails", Field: "Emails", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *SupersetResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state SupersetModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *SupersetResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *SupersetResource) 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
+}
diff --git a/generated/test/go/82_harbor_resource.go b/generated/test/go/82_harbor_resource.go
new file mode 100644
index 0000000..ab83b24
--- /dev/null
+++ b/generated/test/go/82_harbor_resource.go
@@ -0,0 +1,482 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: harbor
+// Service ID: 82
+
+var _ resource.Resource = &HarborResource{}
+var _ resource.ResourceWithModifyPlan = &HarborResource{}
+var _ resource.ResourceWithImportState = &HarborResource{}
+
+type HarborResource struct {
+ client *core.UniversalClient
+}
+
+type HarborModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ Domain types.String `tfsdk:"domain"`
+ ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"`
+ ResourceInstances types.Int64 `tfsdk:"resource_instances"`
+ ResourceMemory types.Int64 `tfsdk:"resource_memory"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ S3Uid types.String `tfsdk:"s3_uid"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewHarborResource() resource.Resource {
+ return &HarborResource{}
+}
+
+func (r *HarborResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_harbor"
+}
+
+func (r *HarborResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("harbor-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."},
+ "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100), MarkdownDescription: "Квота ядра пода (Указывается в mili). 1000Mili = 1 Ядро"},
+ "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов"},
+ "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(200), MarkdownDescription: "Квота памяти пода (Указывается в Megabytes)"},
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ "s3_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID экземпляра услуги S3 Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *HarborResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *HarborModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *HarborModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan HarborModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.S3Uid.IsNull() && !plan.S3Uid.IsUnknown() && !state.S3Uid.IsNull() && !state.S3Uid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.S3Uid.ValueString(), state.S3Uid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить s3_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.ResourceRealm.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+ if config.ResourceRealm.IsUnknown() {
+ return
+ }
+ if config.S3Uid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "s3_uid is required.")
+ return
+ }
+ if config.S3Uid.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.S3Uid.IsNull() && !config.S3Uid.IsUnknown() {
+ resolvedS3Uid, err := r.client.ResolveRefSvcParamValue(ctx, 12, config.S3Uid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve s3_uid", err.Error())
+ } else if resolvedS3Uid != "" && resolvedS3Uid != config.S3Uid.ValueString() {
+ config.S3Uid = types.StringValue(resolvedS3Uid)
+ }
+ }
+ params := map[int]string{
+ 16: resources_core.FormatString(config.Domain),
+ 110: resources_core.FormatString(config.ResourceRealm),
+ 228: resources_core.FormatInt64(config.ResourceCPU),
+ 229: resources_core.FormatInt64(config.ResourceMemory),
+ 230: resources_core.FormatInt64(config.ResourceInstances),
+ 231: resources_core.FormatString(config.S3Uid),
+ }
+ desiredDomain := ""
+ if !config.Domain.IsNull() && !config.Domain.IsUnknown() {
+ desiredDomain = config.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 82, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *HarborResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data HarborModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+ if data.S3Uid.IsNull() || data.S3Uid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "s3_uid is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ if !data.Domain.IsNull() && !data.Domain.IsUnknown() {
+ desiredDomain = data.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 82, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 16: resources_core.FormatString(data.Domain),
+ 110: resources_core.FormatString(data.ResourceRealm),
+ 228: resources_core.FormatInt64(data.ResourceCPU),
+ 229: resources_core.FormatInt64(data.ResourceMemory),
+ 230: resources_core.FormatInt64(data.ResourceInstances),
+ 231: resources_core.FormatString(data.S3Uid),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 82, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 82, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *HarborResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state HarborModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 82, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *HarborResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan HarborModel
+ var state HarborModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.ResourceInstances.IsNull() != state.ResourceInstances.IsNull() || plan.ResourceInstances.IsUnknown() != state.ResourceInstances.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() {
+ if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() {
+ if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() {
+ if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 822: resources_core.FormatInt64(plan.ResourceInstances),
+ 823: resources_core.FormatInt64(plan.ResourceCPU),
+ 824: resources_core.FormatInt64(plan.ResourceMemory),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 82, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *HarborResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state HarborModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *HarborResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *HarborResource) 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
+}
diff --git a/generated/test/go/86_k8s_velero_resource.go b/generated/test/go/86_k8s_velero_resource.go
new file mode 100644
index 0000000..4ad3d14
--- /dev/null
+++ b/generated/test/go/86_k8s_velero_resource.go
@@ -0,0 +1,401 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: k8s_velero
+// Service ID: 86
+
+var _ resource.Resource = &K8sVeleroResource{}
+var _ resource.ResourceWithModifyPlan = &K8sVeleroResource{}
+var _ resource.ResourceWithImportState = &K8sVeleroResource{}
+
+type K8sVeleroResource struct {
+ client *core.UniversalClient
+}
+
+// K8sVeleroBackupConfigurationModel — вложенная модель для map-fixed параметра backupConfiguration.
+type K8sVeleroBackupConfigurationModel struct {
+ S3BucketUid types.String `tfsdk:"s3_bucket_uid" json:"s3BucketUid"`
+}
+
+// K8sVeleroClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type K8sVeleroClusterConfigurationModel struct {
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+}
+
+// K8sVeleroNodeAgentConfigurationModel — вложенная модель для map-fixed параметра nodeAgentConfiguration.
+type K8sVeleroNodeAgentConfigurationModel struct {
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+}
+
+// K8sVeleroStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type K8sVeleroStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+}
+
+type K8sVeleroModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ BackupConfiguration *K8sVeleroBackupConfigurationModel `tfsdk:"backup_configuration"`
+ ClusterConfiguration *K8sVeleroClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ NodeAgentConfiguration *K8sVeleroNodeAgentConfigurationModel `tfsdk:"node_agent_configuration"`
+ StartupConfiguration *K8sVeleroStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewK8sVeleroResource() resource.Resource {
+ return &K8sVeleroResource{}
+}
+
+func (r *K8sVeleroResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_k8s_velero"
+}
+
+func (r *K8sVeleroResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "backup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "s3_bucket_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Экземпляр S3-Бакета обязателен
Учётная запись будет создана автоматически, если её нету"},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1000), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2048), MarkdownDescription: "Указывается в Megabytes"},
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ }},
+ "node_agent_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2000), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2048), MarkdownDescription: "Указывается в Megabytes"},
+ }},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Required: true},
+ }},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *K8sVeleroResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *K8sVeleroModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *K8sVeleroModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan K8sVeleroModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 962: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64())}),
+ 963: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}),
+ 964: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.NodeAgentConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.NodeAgentConfiguration.Memory.ValueInt64())}),
+ 1076: resources_core.BuildJSON(map[string]string{"s3BucketUid": fmt.Sprintf("\"%s\"", config.BackupConfiguration.S3BucketUid.ValueString())}),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 86, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *K8sVeleroResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data K8sVeleroModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 86, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 962: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64())}),
+ 963: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}),
+ 964: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.NodeAgentConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.NodeAgentConfiguration.Memory.ValueInt64())}),
+ 1076: resources_core.BuildJSON(map[string]string{"s3BucketUid": fmt.Sprintf("\"%s\"", data.BackupConfiguration.S3BucketUid.ValueString())}),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 86, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 86, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "nodeAgentConfiguration", Field: "NodeAgentConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8sVeleroResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state K8sVeleroModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 86, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "nodeAgentConfiguration", Field: "NodeAgentConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *K8sVeleroResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan K8sVeleroModel
+ var state K8sVeleroModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 965: resources_core.BuildJSON(map[string]string{"replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64())}),
+ 966: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.NodeAgentConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.NodeAgentConfiguration.Memory.ValueInt64())}),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 86, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "nodeAgentConfiguration", Field: "NodeAgentConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8sVeleroResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state K8sVeleroModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *K8sVeleroResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *K8sVeleroResource) 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
+}
diff --git a/generated/test/go/88_k8s_ziti_controller_resource.go b/generated/test/go/88_k8s_ziti_controller_resource.go
new file mode 100644
index 0000000..4c54fce
--- /dev/null
+++ b/generated/test/go/88_k8s_ziti_controller_resource.go
@@ -0,0 +1,431 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: k8s_ziti_controller
+// Service ID: 88
+
+var _ resource.Resource = &K8sZitiControllerResource{}
+var _ resource.ResourceWithModifyPlan = &K8sZitiControllerResource{}
+var _ resource.ResourceWithImportState = &K8sZitiControllerResource{}
+
+type K8sZitiControllerResource struct {
+ client *core.UniversalClient
+}
+
+type K8sZitiControllerModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ Domain types.String `tfsdk:"domain"`
+ ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"`
+ ResourceDisk types.Int64 `tfsdk:"resource_disk"`
+ ResourceInstances types.Int64 `tfsdk:"resource_instances"`
+ ResourceMemory types.Int64 `tfsdk:"resource_memory"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewK8sZitiControllerResource() resource.Resource {
+ return &K8sZitiControllerResource{}
+}
+
+func (r *K8sZitiControllerResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_k8s_ziti_controller"
+}
+
+func (r *K8sZitiControllerResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8sZitiController-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."},
+ "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(300), MarkdownDescription: "Квота ядра пода (milicores) Не изменяется после создания."},
+ "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Квота диска (Указывается в GIGAbytes) Не изменяется после создания."},
+ "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов Не изменяется после создания."},
+ "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Квота памяти пода (MBytes) Не изменяется после создания."},
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *K8sZitiControllerResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *K8sZitiControllerModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *K8sZitiControllerModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan K8sZitiControllerModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() {
+ if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_c_p_u", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() {
+ if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_disk", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() {
+ if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_instances", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() {
+ if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_memory", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.ResourceRealm.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+ if config.ResourceRealm.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 294: resources_core.FormatInt64(config.ResourceCPU),
+ 295: resources_core.FormatInt64(config.ResourceMemory),
+ 296: resources_core.FormatInt64(config.ResourceDisk),
+ 297: resources_core.FormatInt64(config.ResourceInstances),
+ 298: resources_core.FormatString(config.ResourceRealm),
+ 299: resources_core.FormatString(config.Domain),
+ }
+ desiredDomain := ""
+ if !config.Domain.IsNull() && !config.Domain.IsUnknown() {
+ desiredDomain = config.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 88, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *K8sZitiControllerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data K8sZitiControllerModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ if !data.Domain.IsNull() && !data.Domain.IsUnknown() {
+ desiredDomain = data.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 88, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 294: resources_core.FormatInt64(data.ResourceCPU),
+ 295: resources_core.FormatInt64(data.ResourceMemory),
+ 296: resources_core.FormatInt64(data.ResourceDisk),
+ 297: resources_core.FormatInt64(data.ResourceInstances),
+ 298: resources_core.FormatString(data.ResourceRealm),
+ 299: resources_core.FormatString(data.Domain),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 88, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 88, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8sZitiControllerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state K8sZitiControllerModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 88, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *K8sZitiControllerResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan K8sZitiControllerModel
+ var state K8sZitiControllerModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 88, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *K8sZitiControllerResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state K8sZitiControllerModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *K8sZitiControllerResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *K8sZitiControllerResource) 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
+}
diff --git a/generated/test/go/89_flask_resource.go b/generated/test/go/89_flask_resource.go
new file mode 100644
index 0000000..1fe8642
--- /dev/null
+++ b/generated/test/go/89_flask_resource.go
@@ -0,0 +1,449 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: flask
+// Service ID: 89
+
+var _ resource.Resource = &FlaskResource{}
+var _ resource.ResourceWithModifyPlan = &FlaskResource{}
+var _ resource.ResourceWithImportState = &FlaskResource{}
+
+type FlaskResource struct {
+ client *core.UniversalClient
+}
+
+// FlaskAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type FlaskAccessConfigurationModel struct {
+ Domain types.String `tfsdk:"domain" json:"domain"`
+}
+
+// FlaskAppConfigurationModel — вложенная модель для map-fixed параметра appConfiguration.
+type FlaskAppConfigurationModel struct {
+ Version types.String `tfsdk:"version" json:"version"`
+ GitPath types.String `tfsdk:"git_path" json:"gitPath"`
+ HealthPath types.String `tfsdk:"health_path" json:"healthPath"`
+}
+
+// FlaskClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type FlaskClusterConfigurationModel struct {
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+}
+
+// FlaskStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type FlaskStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+}
+
+type FlaskModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // git_revision: при изменении вызывает redeploy вместо modify.
+ // Опциональное поле — если не задано, modify работает как обычно.
+ GitRevision types.String `tfsdk:"git_revision"`
+ AccessConfiguration *FlaskAccessConfigurationModel `tfsdk:"access_configuration"`
+ AppConfiguration *FlaskAppConfigurationModel `tfsdk:"app_configuration"`
+ ClusterConfiguration *FlaskClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ JsonEnv types.String `tfsdk:"json_env"`
+ StartupConfiguration *FlaskStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewFlaskResource() resource.Resource {
+ return &FlaskResource{}
+}
+
+func (r *FlaskResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_flask"
+}
+
+func (r *FlaskResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("flask-mdpw2bcy"), MarkdownDescription: "Если указан FQDN в формате: `myapp.example.com`, предварительно будет проверяться DNS-запись (А-запись **должна смотреть на IP Ingress кластера ресурсной платформы**)
Если указан в формате `myapp`, DNS-запись будет автоматически создана в служебной DNS-зоне"},
+ }},
+ "app_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("3.12")},
+ "git_path": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("https://github.com/Foxyhhd/Baldurs-Gate-test.git"), MarkdownDescription: "Путь до репозитория приложения
Обязательно должен быть передан как URL: `https://....`
Должен быть сетевой доступ до URL для скачивания исходников"},
+ "health_path": schema.StringAttribute{Optional: true, MarkdownDescription: "Путь до healtcheck для запуска пода в Kubernetes, который отдаст 200 код
Пустое значение означает, что будет проверяться исключительно доступность приложения по порту"},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(200), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(256), MarkdownDescription: "Указывается в Megabytes"},
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ }},
+ "json_env": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("{}")},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"},
+ }},
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // При изменении git_revision вызывается redeploy вместо modify.
+ // Если не задано — modify работает без изменений.
+ "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *FlaskResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *FlaskModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *FlaskModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan FlaskModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 1037: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1038: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64())}),
+ 1039: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Domain.ValueString())}),
+ 1040: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.HealthPath.ValueString())}),
+ 1041: resources_core.FormatString(config.JsonEnv),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 89, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *FlaskResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data FlaskModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 89, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 1037: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1038: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64())}),
+ 1039: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Domain.ValueString())}),
+ 1040: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.HealthPath.ValueString())}),
+ 1041: resources_core.FormatString(data.JsonEnv),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 89, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 89, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *FlaskResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state FlaskModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 89, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *FlaskResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan FlaskModel
+ var state FlaskModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.JsonEnv.IsNull() != state.JsonEnv.IsNull() || plan.JsonEnv.IsUnknown() != state.JsonEnv.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.JsonEnv.IsNull() && !plan.JsonEnv.IsUnknown() && !state.JsonEnv.IsNull() && !state.JsonEnv.IsUnknown() {
+ if !strings.EqualFold(plan.JsonEnv.ValueString(), state.JsonEnv.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // Проверяем изменился ли git_revision.
+ // Если да — вызываем redeploy вместо modify.
+ // git_revision = "" (не задано) → обычный modify.
+ redeployRequested := false
+ if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() {
+ if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() {
+ redeployRequested = true
+ }
+ }
+
+ if !hasServiceParamChanges && !redeployRequested {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ if redeployRequested {
+ // Вызываем redeploy — пустой params (операция не требует CFS-параметров)
+ if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ } else {
+ params := map[int]string{
+ 1042: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64())}),
+ 1043: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.HealthPath.ValueString())}),
+ 1044: resources_core.FormatString(plan.JsonEnv),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 89, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *FlaskResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state FlaskModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *FlaskResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *FlaskResource) 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
+}
diff --git a/generated/test/go/90_postgres_backup_resource.go b/generated/test/go/90_postgres_backup_resource.go
new file mode 100644
index 0000000..957eb3e
--- /dev/null
+++ b/generated/test/go/90_postgres_backup_resource.go
@@ -0,0 +1,326 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: postgres
+// Subresource: backup
+
+var _ resource.Resource = &PostgresBackupResource{}
+
+// PostgresBackupResource manages a subresource via operations.
+type PostgresBackupResource struct {
+ client *core.UniversalClient
+}
+
+type PostgresBackupModel struct {
+ ID types.String `tfsdk:"id"`
+ PostgresID types.String `tfsdk:"postgres_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"`
+}
+
+func NewPostgresBackupResource() resource.Resource {
+ return &PostgresBackupResource{}
+}
+
+func (r *PostgresBackupResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_postgres_backup"
+}
+
+func (r *PostgresBackupResource) 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()}},
+ "postgres_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."},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *PostgresBackupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan PostgresBackupModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.PostgresID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{}
+ identityCodes := []string{}
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("backup", 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, "backup", 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{})
+
+ 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, "create_backup", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_backup", 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, "backup", 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, "backup", 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, "backup", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *PostgresBackupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state PostgresBackupModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *PostgresBackupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan PostgresBackupModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state PostgresBackupModel
+ 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.PostgresID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{}
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *PostgresBackupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state PostgresBackupModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.PostgresID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{}
+ identityCodes := []string{}
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("backup", 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{})
+
+ 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, "", 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 *PostgresBackupResource) 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
+}
diff --git a/generated/test/go/90_postgres_database_resource.go b/generated/test/go/90_postgres_database_resource.go
new file mode 100644
index 0000000..7310720
--- /dev/null
+++ b/generated/test/go/90_postgres_database_resource.go
@@ -0,0 +1,358 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: postgres
+// Subresource: database
+
+var _ resource.Resource = &PostgresDatabaseResource{}
+
+// PostgresDatabaseResource manages a subresource via operations.
+type PostgresDatabaseResource struct {
+ client *core.UniversalClient
+}
+
+type PostgresDatabaseModel struct {
+ ID types.String `tfsdk:"id"`
+ PostgresID types.String `tfsdk:"postgres_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"`
+ DbName types.String `tfsdk:"db_name"`
+ DbOwner types.String `tfsdk:"db_owner"`
+}
+
+func NewPostgresDatabaseResource() resource.Resource {
+ return &PostgresDatabaseResource{}
+}
+
+func (r *PostgresDatabaseResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_postgres_database"
+}
+
+func (r *PostgresDatabaseResource) 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()}},
+ "postgres_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."},
+ "db_name": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "db_owner": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *PostgresDatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan PostgresDatabaseModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if plan.DbName.IsNull() || plan.DbName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "db_name is required.")
+ return
+ }
+ if plan.DbOwner.IsNull() || plan.DbOwner.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "db_owner is required.")
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.PostgresID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "dbName": resources_core.FormatString(plan.DbName),
+ "dbOwner": resources_core.FormatString(plan.DbOwner),
+ }
+ identityCodes := []string{
+ "dbName",
+ "dbOwner",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", 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, "database", 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{
+ "dbName": resources_core.FormatString(plan.DbName),
+ "dbOwner": resources_core.FormatString(plan.DbOwner),
+ })
+
+ 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, "create_database", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_database", 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, "database", 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, "database", 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, "database", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *PostgresDatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state PostgresDatabaseModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *PostgresDatabaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan PostgresDatabaseModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state PostgresDatabaseModel
+ 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.PostgresID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "dbName": resources_core.FormatString(plan.DbName),
+ "dbOwner": resources_core.FormatString(plan.DbOwner),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *PostgresDatabaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state PostgresDatabaseModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_database" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.PostgresID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "dbName": resources_core.FormatString(state.DbName),
+ "dbOwner": resources_core.FormatString(state.DbOwner),
+ }
+ identityCodes := []string{
+ "dbName",
+ "dbOwner",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", 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{
+ "dbName": resources_core.FormatString(state.DbName),
+ })
+
+ 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, "delete_database", 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 *PostgresDatabaseResource) 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
+}
diff --git a/generated/test/go/90_postgres_resource.go b/generated/test/go/90_postgres_resource.go
new file mode 100644
index 0000000..4b3c2ca
--- /dev/null
+++ b/generated/test/go/90_postgres_resource.go
@@ -0,0 +1,475 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: postgres
+// Service ID: 90
+
+var _ resource.Resource = &PostgresResource{}
+var _ resource.ResourceWithModifyPlan = &PostgresResource{}
+var _ resource.ResourceWithImportState = &PostgresResource{}
+
+type PostgresResource struct {
+ client *core.UniversalClient
+}
+
+// PostgresAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type PostgresAccessConfigurationModel struct {
+ MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"`
+ SlaveIpSpace types.String `tfsdk:"slave_ip_space" json:"slaveIpSpace"`
+ SlaveAccessList types.String `tfsdk:"slave_access_list" json:"slaveAccessList"`
+ MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"`
+}
+
+// PostgresAutoscaleConfigurationModel — вложенная модель для map-fixed параметра autoscaleConfiguration.
+type PostgresAutoscaleConfigurationModel struct {
+ Enabled types.Bool `tfsdk:"enabled" json:"enabled"`
+ Schedule types.Int64 `tfsdk:"schedule" json:"schedule"`
+ Quota types.Int64 `tfsdk:"quota" json:"quota"`
+ Percent types.Int64 `tfsdk:"percent" json:"percent"`
+}
+
+// PostgresBackupConfigurationModel — вложенная модель для map-fixed параметра backupConfiguration.
+type PostgresBackupConfigurationModel struct {
+ Schedule types.String `tfsdk:"schedule" json:"schedule"`
+ S3Uid types.String `tfsdk:"s3_uid" json:"s3Uid"`
+ Retain types.Int64 `tfsdk:"retain" json:"retain"`
+}
+
+// PostgresClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type PostgresClusterConfigurationModel struct {
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+ Disk types.Int64 `tfsdk:"disk" json:"disk"`
+}
+
+// PostgresPostgresConfigurationModel — вложенная модель для map-fixed параметра postgresConfiguration.
+type PostgresPostgresConfigurationModel struct {
+ Version types.String `tfsdk:"version" json:"version"`
+ SslRequired types.Bool `tfsdk:"ssl_required" json:"sslRequired"`
+ PoolerMaster types.Bool `tfsdk:"pooler_master" json:"poolerMaster"`
+ PoolerSlave types.Bool `tfsdk:"pooler_slave" json:"poolerSlave"`
+}
+
+// PostgresStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type PostgresStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+}
+
+type PostgresModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AccessConfiguration *PostgresAccessConfigurationModel `tfsdk:"access_configuration"`
+ AutoscaleConfiguration *PostgresAutoscaleConfigurationModel `tfsdk:"autoscale_configuration"`
+ BackupConfiguration *PostgresBackupConfigurationModel `tfsdk:"backup_configuration"`
+ ClusterConfiguration *PostgresClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ PostgresConf types.String `tfsdk:"postgres_conf"`
+ PostgresConfiguration *PostgresPostgresConfigurationModel `tfsdk:"postgres_configuration"`
+ StartupConfiguration *PostgresStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewPostgresResource() resource.Resource {
+ return &PostgresResource{}
+}
+
+func (r *PostgresResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_postgres"
+}
+
+func (r *PostgresResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"},
+ "slave_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"},
+ "slave_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"},
+ "master_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"},
+ }},
+ "autoscale_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "enabled": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает автоскейлинг PV. При включении параметра необходимо настроить autoScalePercentage и autoScaleTechWindow. Алерт при срабатывании вызывает modify у текущей услуги"},
+ "schedule": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "Требует включения autoScale. Окно обновления (пока не реализовано). Указывается час в виде Integer (0 / 5 / 12 / 23)"},
+ "quota": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100), MarkdownDescription: "До какого размера возможно увеличивать диск, когда включен Autoscale"},
+ "percent": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Требует включения autoScale. Позволяет указать в процентах расширение от текущего макс объема
Минимальный размер на который расширяется - **1Gb**"},
+ }},
+ "backup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "schedule": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("0 0 * * *")},
+ "s3_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Экземпляр S3 обязателен"},
+ "retain": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(7)},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Указывается в Megabytes"},
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Указывается в GIGAbytes"},
+ }},
+ "postgres_conf": schema.StringAttribute{Optional: true},
+ "postgres_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("17")},
+ "ssl_required": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "По умолчанию подключение обязательное (sslmode=require)"},
+ "pooler_master": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает pgPooler (на запись). Будет создан новый под
Размер пода **200m/400Mi**"},
+ "pooler_slave": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает pgPooler (на чтение). Будет создан новый под
Размер пода **200m/400Mi**"},
+ }},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"},
+ }},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *PostgresResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *PostgresModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *PostgresModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan PostgresModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 788: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64())}),
+ 789: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}),
+ 790: resources_core.BuildJSON(map[string]string{"masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveAccessList.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString())}),
+ 791: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.PostgresConfiguration.Version.ValueString()), "sslRequired": fmt.Sprintf("%v", config.PostgresConfiguration.SslRequired.ValueBool()), "poolerMaster": fmt.Sprintf("%v", config.PostgresConfiguration.PoolerMaster.ValueBool()), "poolerSlave": fmt.Sprintf("%v", config.PostgresConfiguration.PoolerSlave.ValueBool())}),
+ 792: resources_core.FormatString(config.PostgresConf),
+ 793: resources_core.BuildJSON(map[string]string{"schedule": fmt.Sprintf("\"%s\"", config.BackupConfiguration.Schedule.ValueString()), "s3Uid": fmt.Sprintf("\"%s\"", config.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", config.BackupConfiguration.Retain.ValueInt64())}),
+ 794: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", config.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", config.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", config.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", config.AutoscaleConfiguration.Percent.ValueInt64())}),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 90, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *PostgresResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data PostgresModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 90, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 788: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64())}),
+ 789: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}),
+ 790: resources_core.BuildJSON(map[string]string{"masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveAccessList.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString())}),
+ 791: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.PostgresConfiguration.Version.ValueString()), "sslRequired": fmt.Sprintf("%v", data.PostgresConfiguration.SslRequired.ValueBool()), "poolerMaster": fmt.Sprintf("%v", data.PostgresConfiguration.PoolerMaster.ValueBool()), "poolerSlave": fmt.Sprintf("%v", data.PostgresConfiguration.PoolerSlave.ValueBool())}),
+ 792: resources_core.FormatString(data.PostgresConf),
+ 793: resources_core.BuildJSON(map[string]string{"schedule": fmt.Sprintf("\"%s\"", data.BackupConfiguration.Schedule.ValueString()), "s3Uid": fmt.Sprintf("\"%s\"", data.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", data.BackupConfiguration.Retain.ValueInt64())}),
+ 794: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", data.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", data.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", data.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", data.AutoscaleConfiguration.Percent.ValueInt64())}),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 90, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 90, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"},
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "postgresConf", Field: "PostgresConf", Type: "array-map-fixed"},
+ {Code: "postgresConfiguration", Field: "PostgresConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *PostgresResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state PostgresModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 90, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"},
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "postgresConf", Field: "PostgresConf", Type: "array-map-fixed"},
+ {Code: "postgresConfiguration", Field: "PostgresConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *PostgresResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan PostgresModel
+ var state PostgresModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.PostgresConf.IsNull() != state.PostgresConf.IsNull() || plan.PostgresConf.IsUnknown() != state.PostgresConf.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.PostgresConf.IsNull() && !plan.PostgresConf.IsUnknown() && !state.PostgresConf.IsNull() && !state.PostgresConf.IsUnknown() {
+ if !strings.EqualFold(plan.PostgresConf.ValueString(), state.PostgresConf.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 795: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64())}),
+ 796: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveAccessList.ValueString())}),
+ 797: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.PostgresConfiguration.Version.ValueString()), "sslRequired": fmt.Sprintf("%v", plan.PostgresConfiguration.SslRequired.ValueBool()), "poolerMaster": fmt.Sprintf("%v", plan.PostgresConfiguration.PoolerMaster.ValueBool()), "poolerSlave": fmt.Sprintf("%v", plan.PostgresConfiguration.PoolerSlave.ValueBool())}),
+ 798: resources_core.FormatString(plan.PostgresConf),
+ 799: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", plan.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.Schedule.ValueString())}),
+ 800: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", plan.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Percent.ValueInt64())}),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 90, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"},
+ {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "postgresConf", Field: "PostgresConf", Type: "array-map-fixed"},
+ {Code: "postgresConfiguration", Field: "PostgresConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *PostgresResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state PostgresModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *PostgresResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *PostgresResource) 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
+}
diff --git a/generated/test/go/90_postgres_user_resource.go b/generated/test/go/90_postgres_user_resource.go
new file mode 100644
index 0000000..5101eca
--- /dev/null
+++ b/generated/test/go/90_postgres_user_resource.go
@@ -0,0 +1,358 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: postgres
+// Subresource: user
+
+var _ resource.Resource = &PostgresUserResource{}
+
+// PostgresUserResource manages a subresource via operations.
+type PostgresUserResource struct {
+ client *core.UniversalClient
+}
+
+type PostgresUserModel struct {
+ ID types.String `tfsdk:"id"`
+ PostgresID types.String `tfsdk:"postgres_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"`
+ Role types.String `tfsdk:"role"`
+ Username types.String `tfsdk:"username"`
+}
+
+func NewPostgresUserResource() resource.Resource {
+ return &PostgresUserResource{}
+}
+
+func (r *PostgresUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_postgres_user"
+}
+
+func (r *PostgresUserResource) 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()}},
+ "postgres_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."},
+ "role": schema.StringAttribute{Required: true, MarkdownDescription: "Выбрать роль из списка Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ "username": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *PostgresUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan PostgresUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if plan.Role.IsNull() || plan.Role.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "role is required.")
+ return
+ }
+ if plan.Username.IsNull() || plan.Username.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "username is required.")
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.PostgresID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "role": resources_core.FormatString(plan.Role),
+ "username": resources_core.FormatString(plan.Username),
+ }
+ identityCodes := []string{
+ "role",
+ "username",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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, "user", 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{
+ "username": resources_core.FormatString(plan.Username),
+ "role": resources_core.FormatString(plan.Role),
+ })
+
+ 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, "create_user", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", 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, "user", 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, "user", 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, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *PostgresUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state PostgresUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *PostgresUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan PostgresUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "" == "" {
+ var state PostgresUserModel
+ 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.PostgresID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{})
+
+ idParams := map[string]string{
+ "role": resources_core.FormatString(plan.Role),
+ "username": resources_core.FormatString(plan.Username),
+ }
+
+ 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, "", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *PostgresUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state PostgresUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_user" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.PostgresID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "role": resources_core.FormatString(state.Role),
+ "username": resources_core.FormatString(state.Username),
+ }
+ identityCodes := []string{
+ "role",
+ "username",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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{
+ "username": resources_core.FormatString(state.Username),
+ })
+
+ 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, "delete_user", 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 *PostgresUserResource) 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
+}
diff --git a/generated/test/go/91_redis_resource.go b/generated/test/go/91_redis_resource.go
new file mode 100644
index 0000000..864cd27
--- /dev/null
+++ b/generated/test/go/91_redis_resource.go
@@ -0,0 +1,393 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: redis
+// Service ID: 91
+
+var _ resource.Resource = &RedisResource{}
+var _ resource.ResourceWithModifyPlan = &RedisResource{}
+var _ resource.ResourceWithImportState = &RedisResource{}
+
+type RedisResource struct {
+ client *core.UniversalClient
+}
+
+// RedisAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type RedisAccessConfigurationModel struct {
+ SlaveAccessList types.String `tfsdk:"slave_access_list" json:"slaveAccessList"`
+ MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"`
+ MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"`
+ SlaveIpSpace types.String `tfsdk:"slave_ip_space" json:"slaveIpSpace"`
+}
+
+// RedisClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type RedisClusterConfigurationModel struct {
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+ Disk types.Int64 `tfsdk:"disk" json:"disk"`
+}
+
+// RedisStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type RedisStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+}
+
+type RedisModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AccessConfiguration *RedisAccessConfigurationModel `tfsdk:"access_configuration"`
+ ClusterConfiguration *RedisClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ StartupConfiguration *RedisStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewRedisResource() resource.Resource {
+ return &RedisResource{}
+}
+
+func (r *RedisResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_redis"
+}
+
+func (r *RedisResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "slave_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"},
+ "master_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"},
+ "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"},
+ "slave_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Указывается в Megabytes"},
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Указывается в GIGAbytes"},
+ }},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"},
+ }},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *RedisResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *RedisModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *RedisModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan RedisModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 1061: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1062: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64())}),
+ 1063: resources_core.BuildJSON(map[string]string{"slaveAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveAccessList.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveIpSpace.ValueString())}),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 91, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *RedisResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data RedisModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 91, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 1061: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1062: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64())}),
+ 1063: resources_core.BuildJSON(map[string]string{"slaveAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveAccessList.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveIpSpace.ValueString())}),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 91, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 91, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *RedisResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state RedisModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 91, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *RedisResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan RedisModel
+ var state RedisModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 1064: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64())}),
+ 1065: resources_core.BuildJSON(map[string]string{"slaveAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveAccessList.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveIpSpace.ValueString())}),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 91, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *RedisResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state RedisModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *RedisResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *RedisResource) 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
+}
diff --git a/generated/test/go/92_mongodb_resource.go b/generated/test/go/92_mongodb_resource.go
new file mode 100644
index 0000000..d4f5b8a
--- /dev/null
+++ b/generated/test/go/92_mongodb_resource.go
@@ -0,0 +1,389 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: mongodb
+// Service ID: 92
+
+var _ resource.Resource = &MongodbResource{}
+var _ resource.ResourceWithModifyPlan = &MongodbResource{}
+var _ resource.ResourceWithImportState = &MongodbResource{}
+
+type MongodbResource struct {
+ client *core.UniversalClient
+}
+
+// MongodbAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type MongodbAccessConfigurationModel struct {
+ MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"`
+ MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"`
+}
+
+// MongodbClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type MongodbClusterConfigurationModel struct {
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+ Disk types.Int64 `tfsdk:"disk" json:"disk"`
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+}
+
+// MongodbStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type MongodbStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+}
+
+type MongodbModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ AccessConfiguration *MongodbAccessConfigurationModel `tfsdk:"access_configuration"`
+ ClusterConfiguration *MongodbClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ StartupConfiguration *MongodbStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewMongodbResource() resource.Resource {
+ return &MongodbResource{}
+}
+
+func (r *MongodbResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_mongodb"
+}
+
+func (r *MongodbResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "master_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"},
+ "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1024)},
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(5), MarkdownDescription: "Указывается в Gb"},
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1000)},
+ }},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"},
+ }},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *MongodbResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *MongodbModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *MongodbModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan MongodbModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 1089: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1090: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64())}),
+ 1091: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString())}),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 92, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *MongodbResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data MongodbModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 92, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 1089: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1090: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64())}),
+ 1091: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString())}),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 92, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 92, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *MongodbResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state MongodbModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 92, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *MongodbResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan MongodbModel
+ var state MongodbModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 1092: resources_core.BuildJSON(map[string]string{"disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64())}),
+ 1093: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString())}),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 92, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *MongodbResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state MongodbModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *MongodbResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *MongodbResource) 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
+}
diff --git a/generated/test/go/92_mongodb_user_resource.go b/generated/test/go/92_mongodb_user_resource.go
new file mode 100644
index 0000000..6f0e40d
--- /dev/null
+++ b/generated/test/go/92_mongodb_user_resource.go
@@ -0,0 +1,364 @@
+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/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: mongodb
+// Subresource: user
+
+var _ resource.Resource = &MongodbUserResource{}
+
+// MongodbUserResource manages a subresource via operations.
+type MongodbUserResource struct {
+ client *core.UniversalClient
+}
+
+type MongodbUserModel struct {
+ ID types.String `tfsdk:"id"`
+ MongodbID types.String `tfsdk:"mongodb_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"`
+ DbName types.String `tfsdk:"db_name"`
+ Role types.String `tfsdk:"role"`
+ Username types.String `tfsdk:"username"`
+}
+
+func NewMongodbUserResource() resource.Resource {
+ return &MongodbUserResource{}
+}
+
+func (r *MongodbUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_mongodb_user"
+}
+
+func (r *MongodbUserResource) 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()}},
+ "mongodb_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."},
+ "db_name": schema.StringAttribute{Required: true},
+ "role": schema.StringAttribute{Required: true},
+ "username": schema.StringAttribute{Required: true, MarkdownDescription: "Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
+ }
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *MongodbUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan MongodbUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if plan.DbName.IsNull() || plan.DbName.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "db_name is required.")
+ return
+ }
+ if plan.Role.IsNull() || plan.Role.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "role is required.")
+ return
+ }
+ if plan.Username.IsNull() || plan.Username.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "username is required.")
+ return
+ }
+
+ instanceUID := strings.TrimSpace(plan.MongodbID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+ adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
+
+ idParams := map[string]string{
+ "username": resources_core.FormatString(plan.Username),
+ }
+ identityCodes := []string{
+ "username",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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, "user", 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{
+ "username": resources_core.FormatString(plan.Username),
+ "dbName": resources_core.FormatString(plan.DbName),
+ "role": resources_core.FormatString(plan.Role),
+ })
+
+ 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, "create_user", params, operationTimeout)
+ if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
+ createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", 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, "user", 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, "user", 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, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *MongodbUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state MongodbUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *MongodbUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan MongodbUserModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "modify_user" == "" {
+ var state MongodbUserModel
+ 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.MongodbID.ValueString())
+ if instanceUID == "" {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
+ return
+ }
+
+ params := resources_core.CompactParams(map[string]string{
+ "username": resources_core.FormatString(plan.Username),
+ "dbName": resources_core.FormatString(plan.DbName),
+ "role": resources_core.FormatString(plan.Role),
+ })
+
+ idParams := map[string]string{
+ "username": resources_core.FormatString(plan.Username),
+ }
+
+ 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, "modify_user", params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams))
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *MongodbUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state MongodbUserModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if "delete_user" == "" {
+ return
+ }
+
+ instanceUID := strings.TrimSpace(state.MongodbID.ValueString())
+ if instanceUID == "" {
+ return
+ }
+ skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
+
+ idParams := map[string]string{
+ "username": resources_core.FormatString(state.Username),
+ }
+ identityCodes := []string{
+ "username",
+ }
+ listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", 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{
+ "username": resources_core.FormatString(state.Username),
+ })
+
+ 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, "delete_user", 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 *MongodbUserResource) 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
+}
diff --git a/generated/test/go/93_rabbitmq_resource.go b/generated/test/go/93_rabbitmq_resource.go
new file mode 100644
index 0000000..a6dbba5
--- /dev/null
+++ b/generated/test/go/93_rabbitmq_resource.go
@@ -0,0 +1,462 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: rabbitmq
+// Service ID: 93
+
+var _ resource.Resource = &RabbitmqResource{}
+var _ resource.ResourceWithModifyPlan = &RabbitmqResource{}
+var _ resource.ResourceWithImportState = &RabbitmqResource{}
+
+type RabbitmqResource struct {
+ client *core.UniversalClient
+}
+
+type RabbitmqModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ IpSpaceNameMaster types.String `tfsdk:"ip_space_name_master"`
+ NeedExternalAddressMaster types.Bool `tfsdk:"need_external_address_master"`
+ ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"`
+ ResourceDisk types.Int64 `tfsdk:"resource_disk"`
+ ResourceInstances types.Int64 `tfsdk:"resource_instances"`
+ ResourceMemory types.Int64 `tfsdk:"resource_memory"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewRabbitmqResource() resource.Resource {
+ return &RabbitmqResource{}
+}
+
+func (r *RabbitmqResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_rabbitmq"
+}
+
+func (r *RabbitmqResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "ip_space_name_master": schema.StringAttribute{Optional: true, MarkdownDescription: "Пример: `internet-no-antiddos-v1`"},
+ "need_external_address_master": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Флаг, определяющий, нужно ли выделять внешний IP для доступа к master. Пример: `true` или `false`"},
+ "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1000), MarkdownDescription: "Пример: `2000`"},
+ "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(30), MarkdownDescription: "Пример: `20`"},
+ "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Пример: `1`"},
+ "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1124), MarkdownDescription: "Пример: `2048`"},
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Выбрать платформу из списка Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *RabbitmqResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *RabbitmqModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *RabbitmqModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan RabbitmqModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.ResourceRealm.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+ if config.ResourceRealm.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 116: resources_core.FormatInt64(config.ResourceCPU),
+ 117: resources_core.FormatInt64(config.ResourceMemory),
+ 118: resources_core.FormatInt64(config.ResourceDisk),
+ 119: resources_core.FormatString(config.ResourceRealm),
+ 224: resources_core.FormatInt64(config.ResourceInstances),
+ 225: resources_core.FormatBool(config.NeedExternalAddressMaster),
+ 543: resources_core.FormatString(config.IpSpaceNameMaster),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 93, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *RabbitmqResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data RabbitmqModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 93, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 116: resources_core.FormatInt64(data.ResourceCPU),
+ 117: resources_core.FormatInt64(data.ResourceMemory),
+ 118: resources_core.FormatInt64(data.ResourceDisk),
+ 119: resources_core.FormatString(data.ResourceRealm),
+ 224: resources_core.FormatInt64(data.ResourceInstances),
+ 225: resources_core.FormatBool(data.NeedExternalAddressMaster),
+ 543: resources_core.FormatString(data.IpSpaceNameMaster),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 93, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 93, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"},
+ {Code: "needExternalAddressMaster", Field: "NeedExternalAddressMaster", Type: "bool"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *RabbitmqResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state RabbitmqModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 93, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"},
+ {Code: "needExternalAddressMaster", Field: "NeedExternalAddressMaster", Type: "bool"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *RabbitmqResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan RabbitmqModel
+ var state RabbitmqModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() {
+ if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() {
+ if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceDisk.IsNull() != state.ResourceDisk.IsNull() || plan.ResourceDisk.IsUnknown() != state.ResourceDisk.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() {
+ if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceInstances.IsNull() != state.ResourceInstances.IsNull() || plan.ResourceInstances.IsUnknown() != state.ResourceInstances.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() {
+ if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.NeedExternalAddressMaster.IsNull() != state.NeedExternalAddressMaster.IsNull() || plan.NeedExternalAddressMaster.IsUnknown() != state.NeedExternalAddressMaster.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.NeedExternalAddressMaster.IsNull() && !plan.NeedExternalAddressMaster.IsUnknown() && !state.NeedExternalAddressMaster.IsNull() && !state.NeedExternalAddressMaster.IsUnknown() {
+ if plan.NeedExternalAddressMaster.ValueBool() != state.NeedExternalAddressMaster.ValueBool() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.IpSpaceNameMaster.IsNull() != state.IpSpaceNameMaster.IsNull() || plan.IpSpaceNameMaster.IsUnknown() != state.IpSpaceNameMaster.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.IpSpaceNameMaster.IsNull() && !plan.IpSpaceNameMaster.IsUnknown() && !state.IpSpaceNameMaster.IsNull() && !state.IpSpaceNameMaster.IsUnknown() {
+ if !strings.EqualFold(plan.IpSpaceNameMaster.ValueString(), state.IpSpaceNameMaster.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 304: resources_core.FormatInt64(plan.ResourceCPU),
+ 305: resources_core.FormatInt64(plan.ResourceMemory),
+ 306: resources_core.FormatInt64(plan.ResourceDisk),
+ 307: resources_core.FormatInt64(plan.ResourceInstances),
+ 308: resources_core.FormatBool(plan.NeedExternalAddressMaster),
+ 545: resources_core.FormatString(plan.IpSpaceNameMaster),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 93, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"},
+ {Code: "needExternalAddressMaster", Field: "NeedExternalAddressMaster", Type: "bool"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *RabbitmqResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state RabbitmqModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *RabbitmqResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *RabbitmqResource) 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
+}
diff --git a/generated/test/go/94_lucee_resource.go b/generated/test/go/94_lucee_resource.go
new file mode 100644
index 0000000..4f1a93b
--- /dev/null
+++ b/generated/test/go/94_lucee_resource.go
@@ -0,0 +1,449 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: lucee
+// Service ID: 94
+
+var _ resource.Resource = &LuceeResource{}
+var _ resource.ResourceWithModifyPlan = &LuceeResource{}
+var _ resource.ResourceWithImportState = &LuceeResource{}
+
+type LuceeResource struct {
+ client *core.UniversalClient
+}
+
+// LuceeAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type LuceeAccessConfigurationModel struct {
+ Domain types.String `tfsdk:"domain" json:"domain"`
+}
+
+// LuceeAppConfigurationModel — вложенная модель для map-fixed параметра appConfiguration.
+type LuceeAppConfigurationModel struct {
+ GitPath types.String `tfsdk:"git_path" json:"gitPath"`
+ HealthPath types.String `tfsdk:"health_path" json:"healthPath"`
+ Version types.String `tfsdk:"version" json:"version"`
+}
+
+// LuceeClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type LuceeClusterConfigurationModel struct {
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+}
+
+// LuceeStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type LuceeStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+}
+
+type LuceeModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // git_revision: при изменении вызывает redeploy вместо modify.
+ // Опциональное поле — если не задано, modify работает как обычно.
+ GitRevision types.String `tfsdk:"git_revision"`
+ AccessConfiguration *LuceeAccessConfigurationModel `tfsdk:"access_configuration"`
+ AppConfiguration *LuceeAppConfigurationModel `tfsdk:"app_configuration"`
+ ClusterConfiguration *LuceeClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ JsonEnv types.String `tfsdk:"json_env"`
+ StartupConfiguration *LuceeStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewLuceeResource() resource.Resource {
+ return &LuceeResource{}
+}
+
+func (r *LuceeResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_lucee"
+}
+
+func (r *LuceeResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("lucee-k7rjv4db"), MarkdownDescription: "Если указан FQDN в формате: `myapp.example.com`, предварительно будет проверяться DNS-запись (А-запись **должна смотреть на IP Ingress кластера ресурсной платформы**)
Если указан в формате `myapp`, DNS-запись будет автоматически создана в служебной DNS-зоне"},
+ }},
+ "app_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "git_path": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("https://github.com/xahys/testlucee"), MarkdownDescription: "Путь до репозитория приложения
Обязательно должен быть передан как URL: `https://....`
Должен быть сетевой доступ до URL для скачивания исходников"},
+ "health_path": schema.StringAttribute{Optional: true, MarkdownDescription: "Путь до healtcheck для запуска пода в Kubernetes, который отдаст 200 код
Пустое значение означает, что будет проверяться исключительно доступность приложения по порту"},
+ "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("6.0")},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(200), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(256), MarkdownDescription: "Указывается в Megabytes"},
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ }},
+ "json_env": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("{}"), MarkdownDescription: "Передаётся в формате КЛЮЧ:ЗНАЧЕНИЕ"},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"},
+ }},
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // При изменении git_revision вызывается redeploy вместо modify.
+ // Если не задано — modify работает без изменений.
+ "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *LuceeResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *LuceeModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *LuceeModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan LuceeModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 1019: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1020: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64())}),
+ 1021: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Domain.ValueString())}),
+ 1022: resources_core.BuildJSON(map[string]string{"gitPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.HealthPath.ValueString()), "version": fmt.Sprintf("\"%s\"", config.AppConfiguration.Version.ValueString())}),
+ 1023: resources_core.FormatString(config.JsonEnv),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 94, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *LuceeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data LuceeModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 94, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 1019: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1020: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64())}),
+ 1021: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Domain.ValueString())}),
+ 1022: resources_core.BuildJSON(map[string]string{"gitPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.HealthPath.ValueString()), "version": fmt.Sprintf("\"%s\"", data.AppConfiguration.Version.ValueString())}),
+ 1023: resources_core.FormatString(data.JsonEnv),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 94, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 94, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *LuceeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state LuceeModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 94, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *LuceeResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan LuceeModel
+ var state LuceeModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.JsonEnv.IsNull() != state.JsonEnv.IsNull() || plan.JsonEnv.IsUnknown() != state.JsonEnv.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.JsonEnv.IsNull() && !plan.JsonEnv.IsUnknown() && !state.JsonEnv.IsNull() && !state.JsonEnv.IsUnknown() {
+ if !strings.EqualFold(plan.JsonEnv.ValueString(), state.JsonEnv.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // Проверяем изменился ли git_revision.
+ // Если да — вызываем redeploy вместо modify.
+ // git_revision = "" (не задано) → обычный modify.
+ redeployRequested := false
+ if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() {
+ if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() {
+ redeployRequested = true
+ }
+ }
+
+ if !hasServiceParamChanges && !redeployRequested {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ if redeployRequested {
+ // Вызываем redeploy — пустой params (операция не требует CFS-параметров)
+ if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ } else {
+ params := map[int]string{
+ 1024: resources_core.BuildJSON(map[string]string{"replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64())}),
+ 1025: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.HealthPath.ValueString())}),
+ 1026: resources_core.FormatString(plan.JsonEnv),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 94, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *LuceeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state LuceeModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *LuceeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *LuceeResource) 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
+}
diff --git a/generated/test/go/95_nodejs_resource.go b/generated/test/go/95_nodejs_resource.go
new file mode 100644
index 0000000..8822a79
--- /dev/null
+++ b/generated/test/go/95_nodejs_resource.go
@@ -0,0 +1,449 @@
+package resources_gen
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: nodejs
+// Service ID: 95
+
+var _ resource.Resource = &NodejsResource{}
+var _ resource.ResourceWithModifyPlan = &NodejsResource{}
+var _ resource.ResourceWithImportState = &NodejsResource{}
+
+type NodejsResource struct {
+ client *core.UniversalClient
+}
+
+// NodejsAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration.
+type NodejsAccessConfigurationModel struct {
+ Domain types.String `tfsdk:"domain" json:"domain"`
+}
+
+// NodejsAppConfigurationModel — вложенная модель для map-fixed параметра appConfiguration.
+type NodejsAppConfigurationModel struct {
+ Version types.String `tfsdk:"version" json:"version"`
+ GitPath types.String `tfsdk:"git_path" json:"gitPath"`
+ HealthPath types.String `tfsdk:"health_path" json:"healthPath"`
+}
+
+// NodejsClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration.
+type NodejsClusterConfigurationModel struct {
+ Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
+ Memory types.Int64 `tfsdk:"memory" json:"memory"`
+ Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
+}
+
+// NodejsStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration.
+type NodejsStartupConfigurationModel struct {
+ ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"`
+}
+
+type NodejsModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // git_revision: при изменении вызывает redeploy вместо modify.
+ // Опциональное поле — если не задано, modify работает как обычно.
+ GitRevision types.String `tfsdk:"git_revision"`
+ AccessConfiguration *NodejsAccessConfigurationModel `tfsdk:"access_configuration"`
+ AppConfiguration *NodejsAppConfigurationModel `tfsdk:"app_configuration"`
+ ClusterConfiguration *NodejsClusterConfigurationModel `tfsdk:"cluster_configuration"`
+ JsonEnv types.String `tfsdk:"json_env"`
+ StartupConfiguration *NodejsStartupConfigurationModel `tfsdk:"startup_configuration"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewNodejsResource() resource.Resource {
+ return &NodejsResource{}
+}
+
+func (r *NodejsResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_nodejs"
+}
+
+func (r *NodejsResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "access_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("node-evwflsbl"), MarkdownDescription: "Если указан FQDN в формате: `myapp.example.com`, предварительно будет проверяться DNS-запись (А-запись **должна смотреть на IP Ingress кластера ресурсной платформы**)
Если указан в формате `myapp`, DNS-запись будет автоматически создана в служебной DNS-зоне"},
+ }},
+ "app_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("23")},
+ "git_path": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("https://github.com/xahys/testnode.git"), MarkdownDescription: "Путь до репозитория приложения
Обязательно должен быть передан как URL: `https://....`
Должен быть сетевой доступ до URL для скачивания исходников"},
+ "health_path": schema.StringAttribute{Optional: true, MarkdownDescription: "Путь до healtcheck для запуска пода в Kubernetes, который отдаст 200 код
Пустое значение означает, что будет проверяться исключительно доступность приложения по порту"},
+ }},
+ "cluster_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(200), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"},
+ "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(256), MarkdownDescription: "Указывается в Megabytes"},
+ "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)},
+ }},
+ "json_env": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("{}"), MarkdownDescription: "Передаётся в формате КЛЮЧ:ЗНАЧЕНИЕ"},
+ "startup_configuration": schema.SingleNestedAttribute{Required: true,
+ Attributes: map[string]schema.Attribute{
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"},
+ }},
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // При изменении git_revision вызывается redeploy вместо modify.
+ // Если не задано — modify работает без изменений.
+ "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *NodejsResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *NodejsModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *NodejsModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan NodejsModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 1028: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1029: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64())}),
+ 1030: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Domain.ValueString())}),
+ 1031: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.HealthPath.ValueString())}),
+ 1032: resources_core.FormatString(config.JsonEnv),
+ }
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 95, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *NodejsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data NodejsModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 95, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 1028: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}),
+ 1029: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64())}),
+ 1030: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Domain.ValueString())}),
+ 1031: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.HealthPath.ValueString())}),
+ 1032: resources_core.FormatString(data.JsonEnv),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 95, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 95, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *NodejsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state NodejsModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 95, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *NodejsResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan NodejsModel
+ var state NodejsModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.JsonEnv.IsNull() != state.JsonEnv.IsNull() || plan.JsonEnv.IsUnknown() != state.JsonEnv.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.JsonEnv.IsNull() && !plan.JsonEnv.IsUnknown() && !state.JsonEnv.IsNull() && !state.JsonEnv.IsUnknown() {
+ if !strings.EqualFold(plan.JsonEnv.ValueString(), state.JsonEnv.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // Проверяем изменился ли git_revision.
+ // Если да — вызываем redeploy вместо modify.
+ // git_revision = "" (не задано) → обычный modify.
+ redeployRequested := false
+ if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() {
+ if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() {
+ redeployRequested = true
+ }
+ }
+
+ if !hasServiceParamChanges && !redeployRequested {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ if redeployRequested {
+ // Вызываем redeploy — пустой params (операция не требует CFS-параметров)
+ if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ } else {
+ params := map[int]string{
+ 1033: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64())}),
+ 1034: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.HealthPath.ValueString())}),
+ 1035: resources_core.FormatString(plan.JsonEnv),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 95, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"},
+ {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"},
+ {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *NodejsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state NodejsModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *NodejsResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *NodejsResource) 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
+}
diff --git a/generated/test/go/96_pgadmin_resource.go b/generated/test/go/96_pgadmin_resource.go
new file mode 100644
index 0000000..661b70b
--- /dev/null
+++ b/generated/test/go/96_pgadmin_resource.go
@@ -0,0 +1,515 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: pgadmin
+// Service ID: 96
+
+var _ resource.Resource = &PgadminResource{}
+var _ resource.ResourceWithModifyPlan = &PgadminResource{}
+var _ resource.ResourceWithImportState = &PgadminResource{}
+
+type PgadminResource struct {
+ client *core.UniversalClient
+}
+
+type PgadminModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // git_revision: при изменении вызывает redeploy вместо modify.
+ // Опциональное поле — если не задано, modify работает как обычно.
+ GitRevision types.String `tfsdk:"git_revision"`
+ Domain types.String `tfsdk:"domain"`
+ Login types.String `tfsdk:"login"`
+ Password types.String `tfsdk:"password"`
+ ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"`
+ ResourceDisk types.Int64 `tfsdk:"resource_disk"`
+ ResourceMemory types.Int64 `tfsdk:"resource_memory"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewPgadminResource() resource.Resource {
+ return &PgadminResource{}
+}
+
+func (r *PgadminResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_pgadmin"
+}
+
+func (r *PgadminResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("pgadmin-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."},
+ "login": schema.StringAttribute{Required: true, MarkdownDescription: "Логин для подключения к pgAdmin, в формате email (login@email.net) Не изменяется после создания."},
+ "password": schema.StringAttribute{Required: true, MarkdownDescription: "Пароль для подключения к pgAdmin Не изменяется после создания.", Sensitive: true},
+ "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Квота ядра пода (milicores). 1000Mili = 1 Ядро"},
+ "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Квота диска (Указывается в GIGAbytes)"},
+ "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Квота памяти пода (MBytes)"},
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // При изменении git_revision вызывается redeploy вместо modify.
+ // Если не задано — modify работает без изменений.
+ "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *PgadminResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *PgadminModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *PgadminModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan PgadminModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.Login.IsNull() && !plan.Login.IsUnknown() && !state.Login.IsNull() && !state.Login.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Login.ValueString(), state.Login.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить login", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.Password.IsNull() && !plan.Password.IsUnknown() && !state.Password.IsNull() && !state.Password.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Password.ValueString(), state.Password.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить password", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.Login.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "login is required.")
+ return
+ }
+ if config.Login.IsUnknown() {
+ return
+ }
+ if config.Password.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "password is required.")
+ return
+ }
+ if config.Password.IsUnknown() {
+ return
+ }
+ if config.ResourceRealm.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+ if config.ResourceRealm.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 164: resources_core.FormatString(config.Domain),
+ 165: resources_core.FormatInt64(config.ResourceCPU),
+ 166: resources_core.FormatInt64(config.ResourceMemory),
+ 167: resources_core.FormatInt64(config.ResourceDisk),
+ 169: resources_core.FormatString(config.ResourceRealm),
+ 170: resources_core.FormatString(config.Login),
+ 171: resources_core.FormatString(config.Password),
+ }
+ desiredDomain := ""
+ if !config.Domain.IsNull() && !config.Domain.IsUnknown() {
+ desiredDomain = config.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 96, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *PgadminResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data PgadminModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.Login.IsNull() || data.Login.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "login is required.")
+ return
+ }
+ if data.Password.IsNull() || data.Password.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "password is required.")
+ return
+ }
+ if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ if !data.Domain.IsNull() && !data.Domain.IsUnknown() {
+ desiredDomain = data.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 96, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 164: resources_core.FormatString(data.Domain),
+ 165: resources_core.FormatInt64(data.ResourceCPU),
+ 166: resources_core.FormatInt64(data.ResourceMemory),
+ 167: resources_core.FormatInt64(data.ResourceDisk),
+ 169: resources_core.FormatString(data.ResourceRealm),
+ 170: resources_core.FormatString(data.Login),
+ 171: resources_core.FormatString(data.Password),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 96, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 96, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "login", Field: "Login", Type: "string"},
+ {Code: "password", Field: "Password", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *PgadminResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state PgadminModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 96, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "login", Field: "Login", Type: "string"},
+ {Code: "password", Field: "Password", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *PgadminResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan PgadminModel
+ var state PgadminModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() {
+ if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() {
+ if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceDisk.IsNull() != state.ResourceDisk.IsNull() || plan.ResourceDisk.IsUnknown() != state.ResourceDisk.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() {
+ if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // Проверяем изменился ли git_revision.
+ // Если да — вызываем redeploy вместо modify.
+ // git_revision = "" (не задано) → обычный modify.
+ redeployRequested := false
+ if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() {
+ if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() {
+ redeployRequested = true
+ }
+ }
+
+ if !hasServiceParamChanges && !redeployRequested {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ if redeployRequested {
+ // Вызываем redeploy — пустой params (операция не требует CFS-параметров)
+ if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ } else {
+ params := map[int]string{
+ 172: resources_core.FormatInt64(plan.ResourceCPU),
+ 173: resources_core.FormatInt64(plan.ResourceMemory),
+ 174: resources_core.FormatInt64(plan.ResourceDisk),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 96, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "login", Field: "Login", Type: "string"},
+ {Code: "password", Field: "Password", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *PgadminResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state PgadminModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *PgadminResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *PgadminResource) 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
+}
diff --git a/generated/test/go/97_nodered_resource.go b/generated/test/go/97_nodered_resource.go
new file mode 100644
index 0000000..f8b17d5
--- /dev/null
+++ b/generated/test/go/97_nodered_resource.go
@@ -0,0 +1,431 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: nodered
+// Service ID: 97
+
+var _ resource.Resource = &NoderedResource{}
+var _ resource.ResourceWithModifyPlan = &NoderedResource{}
+var _ resource.ResourceWithImportState = &NoderedResource{}
+
+type NoderedResource struct {
+ client *core.UniversalClient
+}
+
+type NoderedModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ Domain types.String `tfsdk:"domain"`
+ ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"`
+ ResourceDisk types.Int64 `tfsdk:"resource_disk"`
+ ResourceInstances types.Int64 `tfsdk:"resource_instances"`
+ ResourceMemory types.Int64 `tfsdk:"resource_memory"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewNoderedResource() resource.Resource {
+ return &NoderedResource{}
+}
+
+func (r *NoderedResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_nodered"
+}
+
+func (r *NoderedResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("nodered-3"), MarkdownDescription: "Наименование поддомена, будет добавлено в k8stemplate.dev.nubes.ru Не изменяется после создания."},
+ "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Квота ядра пода (Указывается в mili). 1000Mili = 1 Ядро Не изменяется после создания."},
+ "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Квота диска (Указывается в GIGAbytes) Не изменяется после создания."},
+ "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов Не изменяется после создания."},
+ "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Квота памяти пода (Указывается в Megabytes) Не изменяется после создания."},
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *NoderedResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *NoderedModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *NoderedModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan NoderedModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() {
+ if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_c_p_u", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() {
+ if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_disk", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() {
+ if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_instances", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() {
+ if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_memory", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.ResourceRealm.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+ if config.ResourceRealm.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 193: resources_core.FormatInt64(config.ResourceCPU),
+ 194: resources_core.FormatInt64(config.ResourceMemory),
+ 195: resources_core.FormatInt64(config.ResourceDisk),
+ 196: resources_core.FormatString(config.ResourceRealm),
+ 197: resources_core.FormatString(config.Domain),
+ 323: resources_core.FormatInt64(config.ResourceInstances),
+ }
+ desiredDomain := ""
+ if !config.Domain.IsNull() && !config.Domain.IsUnknown() {
+ desiredDomain = config.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 97, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *NoderedResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data NoderedModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ if !data.Domain.IsNull() && !data.Domain.IsUnknown() {
+ desiredDomain = data.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 97, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 193: resources_core.FormatInt64(data.ResourceCPU),
+ 194: resources_core.FormatInt64(data.ResourceMemory),
+ 195: resources_core.FormatInt64(data.ResourceDisk),
+ 196: resources_core.FormatString(data.ResourceRealm),
+ 197: resources_core.FormatString(data.Domain),
+ 323: resources_core.FormatInt64(data.ResourceInstances),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 97, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 97, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *NoderedResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state NoderedModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 97, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *NoderedResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan NoderedModel
+ var state NoderedModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 97, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *NoderedResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state NoderedModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "delete"
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *NoderedResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *NoderedResource) 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
+}
diff --git a/generated/test/go/98_http_resource.go b/generated/test/go/98_http_resource.go
new file mode 100644
index 0000000..5fe21d9
--- /dev/null
+++ b/generated/test/go/98_http_resource.go
@@ -0,0 +1,509 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: http
+// Service ID: 98
+
+var _ resource.Resource = &HttpResource{}
+var _ resource.ResourceWithModifyPlan = &HttpResource{}
+var _ resource.ResourceWithImportState = &HttpResource{}
+
+type HttpResource struct {
+ client *core.UniversalClient
+}
+
+type HttpModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // git_revision: при изменении вызывает redeploy вместо modify.
+ // Опциональное поле — если не задано, modify работает как обычно.
+ GitRevision types.String `tfsdk:"git_revision"`
+ Domain types.String `tfsdk:"domain"`
+ IngressAnnotations types.String `tfsdk:"ingress_annotations"`
+ JsonEnv types.String `tfsdk:"json_env"`
+ RegistryPath types.String `tfsdk:"registry_path"`
+ ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"`
+ ResourceInstances types.Int64 `tfsdk:"resource_instances"`
+ ResourceMemory types.Int64 `tfsdk:"resource_memory"`
+ ResourceRealm types.String `tfsdk:"resource_realm"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewHttpResource() resource.Resource {
+ return &HttpResource{}
+}
+
+func (r *HttpResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_http"
+}
+
+func (r *HttpResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("http-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."},
+ "ingress_annotations": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("{}"), MarkdownDescription: "Формат ключ/значение
Поддерживает пока простые аннотации Не изменяется после создания."},
+ "json_env": schema.StringAttribute{Optional: true, MarkdownDescription: "Массив переменных, будут переданы в env приложения Не изменяется после создания."},
+ "registry_path": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("nexus-sa.tst.nubes.ru/docker-nubes/jolt"), MarkdownDescription: "Путь до image, который будет установлен как deployment Не изменяется после создания."},
+ "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100), MarkdownDescription: "Квота ядра пода (Указывается в mili)"},
+ "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов"},
+ "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(200), MarkdownDescription: "Квота памяти пода (Указывается в Megabytes)"},
+ "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания Не изменяется после создания."},
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // При изменении git_revision вызывается redeploy вместо modify.
+ // Если не задано — modify работает без изменений.
+ "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *HttpResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *HttpModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *HttpModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan HttpModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.IngressAnnotations.IsNull() && !plan.IngressAnnotations.IsUnknown() && !state.IngressAnnotations.IsNull() && !state.IngressAnnotations.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.IngressAnnotations.ValueString(), state.IngressAnnotations.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить ingress_annotations", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.JsonEnv.IsNull() && !plan.JsonEnv.IsUnknown() && !state.JsonEnv.IsNull() && !state.JsonEnv.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.JsonEnv.ValueString(), state.JsonEnv.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить json_env", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.RegistryPath.IsNull() && !plan.RegistryPath.IsUnknown() && !state.RegistryPath.IsNull() && !state.RegistryPath.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.RegistryPath.ValueString(), state.RegistryPath.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить registry_path", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.ResourceRealm.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+ if config.ResourceRealm.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ params := map[int]string{
+ 212: resources_core.FormatString(config.Domain),
+ 213: resources_core.FormatInt64(config.ResourceCPU),
+ 214: resources_core.FormatInt64(config.ResourceMemory),
+ 215: resources_core.FormatInt64(config.ResourceInstances),
+ 216: resources_core.FormatString(config.ResourceRealm),
+ 217: resources_core.FormatString(config.RegistryPath),
+ 241: resources_core.FormatString(config.JsonEnv),
+ 782: resources_core.FormatString(config.IngressAnnotations),
+ }
+ desiredDomain := ""
+ if !config.Domain.IsNull() && !config.Domain.IsUnknown() {
+ desiredDomain = config.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 98, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *HttpResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data HttpModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.")
+ return
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ if !data.Domain.IsNull() && !data.Domain.IsUnknown() {
+ desiredDomain = data.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 98, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 212: resources_core.FormatString(data.Domain),
+ 213: resources_core.FormatInt64(data.ResourceCPU),
+ 214: resources_core.FormatInt64(data.ResourceMemory),
+ 215: resources_core.FormatInt64(data.ResourceInstances),
+ 216: resources_core.FormatString(data.ResourceRealm),
+ 217: resources_core.FormatString(data.RegistryPath),
+ 241: resources_core.FormatString(data.JsonEnv),
+ 782: resources_core.FormatString(data.IngressAnnotations),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 98, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 98, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "ingressAnnotations", Field: "IngressAnnotations", Type: "string"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "registryPath", Field: "RegistryPath", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *HttpResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state HttpModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 98, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "ingressAnnotations", Field: "IngressAnnotations", Type: "string"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "registryPath", Field: "RegistryPath", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *HttpResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan HttpModel
+ var state HttpModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() {
+ if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() {
+ if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceInstances.IsNull() != state.ResourceInstances.IsNull() || plan.ResourceInstances.IsUnknown() != state.ResourceInstances.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() {
+ if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // Проверяем изменился ли git_revision.
+ // Если да — вызываем redeploy вместо modify.
+ // git_revision = "" (не задано) → обычный modify.
+ redeployRequested := false
+ if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() {
+ if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() {
+ redeployRequested = true
+ }
+ }
+
+ if !hasServiceParamChanges && !redeployRequested {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ if redeployRequested {
+ // Вызываем redeploy — пустой params (операция не требует CFS-параметров)
+ if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ } else {
+ params := map[int]string{
+ 218: resources_core.FormatInt64(plan.ResourceCPU),
+ 219: resources_core.FormatInt64(plan.ResourceMemory),
+ 220: resources_core.FormatInt64(plan.ResourceInstances),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 98, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "ingressAnnotations", Field: "IngressAnnotations", Type: "string"},
+ {Code: "jsonEnv", Field: "JsonEnv", Type: "string"},
+ {Code: "registryPath", Field: "RegistryPath", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *HttpResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state HttpModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *HttpResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *HttpResource) 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
+}
diff --git a/generated/test/go/99_gitea_resource.go b/generated/test/go/99_gitea_resource.go
new file mode 100644
index 0000000..0e8a084
--- /dev/null
+++ b/generated/test/go/99_gitea_resource.go
@@ -0,0 +1,579 @@
+package resources_gen
+
+import (
+ "context"
+ "strings"
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+// Service: gitea
+// Service ID: 99
+
+var _ resource.Resource = &GiteaResource{}
+var _ resource.ResourceWithModifyPlan = &GiteaResource{}
+var _ resource.ResourceWithImportState = &GiteaResource{}
+
+type GiteaResource struct {
+ client *core.UniversalClient
+}
+
+type GiteaModel struct {
+ ID types.String `tfsdk:"id"`
+ ResourceName types.String `tfsdk:"resource_name"`
+ OperationTimeout types.String `tfsdk:"operation_timeout"`
+ LogLevel types.String `tfsdk:"log_level"`
+ Domain types.String `tfsdk:"domain"`
+ IngressExtraAnnotations types.String `tfsdk:"ingress_extra_annotations"`
+ IniServerParams types.String `tfsdk:"ini_server_params"`
+ PsqlUid types.String `tfsdk:"psql_uid"`
+ ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"`
+ ResourceDisk types.Int64 `tfsdk:"resource_disk"`
+ ResourceInstances types.Int64 `tfsdk:"resource_instances"`
+ ResourceMemory types.Int64 `tfsdk:"resource_memory"`
+ SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"`
+ AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"`
+ StateParams types.Map `tfsdk:"state_params"`
+ StateOut types.Map `tfsdk:"state_out"`
+ StateParamsFlat types.Map `tfsdk:"state_params_flat"`
+ StateOutFlat types.Map `tfsdk:"state_out_flat"`
+ VaultSecrets types.Map `tfsdk:"vault_secrets"`
+ VaultUrl types.String `tfsdk:"vault_url"`
+ VaultUserPath types.String `tfsdk:"vault_user_path"`
+ VaultFields types.List `tfsdk:"vault_fields"`
+}
+
+func NewGiteaResource() resource.Resource {
+ return &GiteaResource{}
+}
+
+func (r *GiteaResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_gitea"
+}
+
+func (r *GiteaResource) 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()}},
+ "resource_name": schema.StringAttribute{Required: 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."},
+ "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("gitea-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."},
+ "ingress_extra_annotations": schema.StringAttribute{Optional: true},
+ "ini_server_params": schema.StringAttribute{Optional: true},
+ "psql_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID Услуги k8sPgsql. Пользователя techGitea, базу данных gitea создаёт автоматически Не изменяется после создания."},
+ "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Квота ядра пода (Указывается в mili). 1000Mili = 1 Ядро"},
+ "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Квота диска (Указывается в GIGAbytes)"},
+ "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов"},
+ "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Квота памяти пода (Указывается в Megabytes)"},
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
+ "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType},
+ "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true},
+ "vault_url": schema.StringAttribute{Computed: true},
+ "vault_user_path": schema.StringAttribute{Computed: true},
+ "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType},
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *GiteaResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *GiteaModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *GiteaModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan GiteaModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ if !plan.PsqlUid.IsNull() && !plan.PsqlUid.IsUnknown() && !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() {
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.PsqlUid.ValueString(), state.PsqlUid.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить psql_uid", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ }
+ return
+ }
+ if config.PsqlUid.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "psql_uid is required.")
+ return
+ }
+ if config.PsqlUid.IsUnknown() {
+ return
+ }
+
+ if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
+ return
+ }
+ adoptExistingOnCreate := false
+ if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
+ adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
+ }
+ if !config.PsqlUid.IsNull() && !config.PsqlUid.IsUnknown() {
+ resolvedPsqlUid, err := r.client.ResolveRefSvcParamValue(ctx, 90, config.PsqlUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve psql_uid", err.Error())
+ } else if resolvedPsqlUid != "" && resolvedPsqlUid != config.PsqlUid.ValueString() {
+ config.PsqlUid = types.StringValue(resolvedPsqlUid)
+ }
+ }
+ params := map[int]string{
+ 232: resources_core.FormatInt64(config.ResourceCPU),
+ 233: resources_core.FormatInt64(config.ResourceMemory),
+ 234: resources_core.FormatInt64(config.ResourceDisk),
+ 235: resources_core.FormatInt64(config.ResourceInstances),
+ 237: resources_core.FormatString(config.Domain),
+ 238: resources_core.FormatString(config.PsqlUid),
+ 818: resources_core.FormatString(config.IngressExtraAnnotations),
+ 821: resources_core.FormatString(config.IniServerParams),
+ }
+ desiredDomain := ""
+ if !config.Domain.IsNull() && !config.Domain.IsUnknown() {
+ desiredDomain = config.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+
+ resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 99, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
+}
+
+func (r *GiteaResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data GiteaModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if data.PsqlUid.IsNull() || data.PsqlUid.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "psql_uid is required.")
+ return
+ }
+ // ══════════════════════════════════════════════════════════════════════════
+ // ПРАВИЛО TERRAFORM (официальная документация):
+ // «If an attribute value is configured, it is NEVER valid to change that
+ // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
+ // написал пользователь). Менять plan запрещено на уровне фреймворка.
+ //
+ // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
+ // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
+ // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
+ // «Provider produced inconsistent result after apply».
+ //
+ // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
+ // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
+ // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
+ // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
+ // регистр в state через strings.EqualFold (сравниваем без учёта регистра).
+ // ══════════════════════════════════════════════════════════════════════════
+ originalPsqlUid := data.PsqlUid
+ if !data.PsqlUid.IsNull() && !data.PsqlUid.IsUnknown() {
+ resolvedPsqlUid, err := r.client.ResolveRefSvcParamValue(ctx, 90, data.PsqlUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if resolvedPsqlUid != "" && resolvedPsqlUid != data.PsqlUid.ValueString() {
+ data.PsqlUid = types.StringValue(resolvedPsqlUid)
+ }
+ }
+
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ if !data.Domain.IsNull() && !data.Domain.IsUnknown() {
+ desiredDomain = data.Domain.ValueString()
+ }
+ domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151}
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 99, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ 232: resources_core.FormatInt64(data.ResourceCPU),
+ 233: resources_core.FormatInt64(data.ResourceMemory),
+ 234: resources_core.FormatInt64(data.ResourceDisk),
+ 235: resources_core.FormatInt64(data.ResourceInstances),
+ 237: resources_core.FormatString(data.Domain),
+ 238: resources_core.FormatString(data.PsqlUid),
+ 818: resources_core.FormatString(data.IngressExtraAnnotations),
+ 821: resources_core.FormatString(data.IniServerParams),
+ }
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 99, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 99, data, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "ingressExtraAnnotations", Field: "IngressExtraAnnotations", Type: "string"},
+ {Code: "iniServerParams", Field: "IniServerParams", Type: "string"},
+ {Code: "psqlUid", Field: "PsqlUid", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() {
+ resolvedPsqlUid, err := r.client.ResolveRefSvcParamValue(ctx, 90, state.PsqlUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve psql_uid", err.Error())
+ } else if resolvedPsqlUid != "" && resolvedPsqlUid != state.PsqlUid.ValueString() {
+ state.PsqlUid = types.StringValue(resolvedPsqlUid)
+ }
+ }
+ // Restore user-provided casing в state (Create).
+ // EqualFold = «совпадают ли значения без учёта регистра?»
+ // Если да — значит API вернул «туже» строку, только в другом регистре.
+ // Заменяем state на original (то что было в plan/config пользователя).
+ // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
+ if !originalPsqlUid.IsNull() && !originalPsqlUid.IsUnknown() && !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() {
+ if strings.EqualFold(state.PsqlUid.ValueString(), originalPsqlUid.ValueString()) {
+ state.PsqlUid = originalPsqlUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *GiteaResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state GiteaModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 99, state, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "ingressExtraAnnotations", Field: "IngressExtraAnnotations", Type: "string"},
+ {Code: "iniServerParams", Field: "IniServerParams", Type: "string"},
+ {Code: "psqlUid", Field: "PsqlUid", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !newState.PsqlUid.IsNull() && !newState.PsqlUid.IsUnknown() {
+ resolvedPsqlUid, err := r.client.ResolveRefSvcParamValue(ctx, 90, newState.PsqlUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve psql_uid", err.Error())
+ } else if resolvedPsqlUid != "" && resolvedPsqlUid != newState.PsqlUid.ValueString() {
+ newState.PsqlUid = types.StringValue(resolvedPsqlUid)
+ }
+ }
+ // Restore user-provided casing в state (Read).
+ // При чтении у нас нет plan — но предыдущий state уже хранит значение
+ // в регистре пользователя (после первого Create оно было восстановлено).
+ // Берём prior state (переменная state) как эталон регистра.
+ // Если API вернул то же UUID только строчными буквами — восстанавливаем.
+ if !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() && !newState.PsqlUid.IsNull() && !newState.PsqlUid.IsUnknown() {
+ if strings.EqualFold(newState.PsqlUid.ValueString(), state.PsqlUid.ValueString()) {
+ newState.PsqlUid = state.PsqlUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *GiteaResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan GiteaModel
+ var state GiteaModel
+ 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("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ if !hasServiceParamChanges {
+ if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() {
+ if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() {
+ if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceDisk.IsNull() != state.ResourceDisk.IsNull() || plan.ResourceDisk.IsUnknown() != state.ResourceDisk.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() {
+ if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.ResourceInstances.IsNull() != state.ResourceInstances.IsNull() || plan.ResourceInstances.IsUnknown() != state.ResourceInstances.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() {
+ if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.IngressExtraAnnotations.IsNull() != state.IngressExtraAnnotations.IsNull() || plan.IngressExtraAnnotations.IsUnknown() != state.IngressExtraAnnotations.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.IngressExtraAnnotations.IsNull() && !plan.IngressExtraAnnotations.IsUnknown() && !state.IngressExtraAnnotations.IsNull() && !state.IngressExtraAnnotations.IsUnknown() {
+ if !strings.EqualFold(plan.IngressExtraAnnotations.ValueString(), state.IngressExtraAnnotations.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+ if !hasServiceParamChanges {
+ if plan.IniServerParams.IsNull() != state.IniServerParams.IsNull() || plan.IniServerParams.IsUnknown() != state.IniServerParams.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.IniServerParams.IsNull() && !plan.IniServerParams.IsUnknown() && !state.IniServerParams.IsNull() && !state.IniServerParams.IsUnknown() {
+ if !strings.EqualFold(plan.IniServerParams.ValueString(), state.IniServerParams.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ }
+ }
+
+ if !hasServiceParamChanges {
+ plan.ID = instanceID
+ plan.StateParams = state.StateParams
+ plan.StateOut = state.StateOut
+ plan.StateParamsFlat = state.StateParamsFlat
+ plan.StateOutFlat = state.StateOutFlat
+ plan.VaultSecrets = state.VaultSecrets
+ plan.VaultUrl = state.VaultUrl
+ plan.VaultUserPath = state.VaultUserPath
+ plan.VaultFields = state.VaultFields
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ return
+ }
+
+ 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())
+ }
+ params := map[int]string{
+ 259: resources_core.FormatInt64(plan.ResourceCPU),
+ 260: resources_core.FormatInt64(plan.ResourceMemory),
+ 261: resources_core.FormatInt64(plan.ResourceDisk),
+ 262: resources_core.FormatInt64(plan.ResourceInstances),
+ 819: resources_core.FormatString(plan.IngressExtraAnnotations),
+ 820: resources_core.FormatString(plan.IniServerParams),
+ }
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 99, plan, []resources_core.StateField{
+ {Code: "state_params"},
+ {Code: "state_out"},
+ {Code: "state_params_flat"},
+ {Code: "state_out_flat"},
+ {Code: "vault_secrets"},
+ {Code: "vault_url"},
+ {Code: "vault_user_path"},
+ {Code: "vault_fields"},
+ }, []resources_core.InputField{
+ {Code: "domain", Field: "Domain", Type: "string"},
+ {Code: "ingressExtraAnnotations", Field: "IngressExtraAnnotations", Type: "string"},
+ {Code: "iniServerParams", Field: "IniServerParams", Type: "string"},
+ {Code: "psqlUid", Field: "PsqlUid", Type: "string"},
+ {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"},
+ {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"},
+ {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"},
+ {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"},
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() {
+ resolvedPsqlUid, err := r.client.ResolveRefSvcParamValue(ctx, 90, state.PsqlUid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddWarning("Failed to resolve psql_uid", err.Error())
+ } else if resolvedPsqlUid != "" && resolvedPsqlUid != state.PsqlUid.ValueString() {
+ state.PsqlUid = types.StringValue(resolvedPsqlUid)
+ }
+ }
+ // Restore user-provided casing в state (Update).
+ // После API-вызова modify state содержит значения в lower-case от API.
+ // Plan == config == то что написал пользователь (регистр неизменён).
+ // EqualFold: если UUID совпадает без учёта регистра — берём из plan.
+ if !plan.PsqlUid.IsNull() && !plan.PsqlUid.IsUnknown() && !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() {
+ if strings.EqualFold(state.PsqlUid.ValueString(), plan.PsqlUid.ValueString()) {
+ state.PsqlUid = plan.PsqlUid
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *GiteaResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state GiteaModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+
+ 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.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *GiteaResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *GiteaResource) 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
+}
diff --git a/generated/test/go/registry.go b/generated/test/go/registry.go
new file mode 100644
index 0000000..e0f02fc
--- /dev/null
+++ b/generated/test/go/registry.go
@@ -0,0 +1,78 @@
+package resources_gen
+
+import "github.com/hashicorp/terraform-plugin-framework/resource"
+
+// Code generated by tools/gen_v2. DO NOT EDIT.
+func AllResources() []func() resource.Resource {
+ return []func() resource.Resource{
+ NewAkhqResource,
+ NewClickhouseResource,
+ NewDnsrecordResource,
+ NewDnszoneResource,
+ NewDummyResource,
+ NewFlaskResource,
+ NewGiteaResource,
+ NewGiteaComplexResource,
+ NewHarborResource,
+ NewHttpResource,
+ NewK8sOpenbaoResource,
+ NewK8sSthutrvalClusterResource,
+ NewK8sVeleroResource,
+ NewK8sZitiControllerResource,
+ NewKafkaResource,
+ NewLlmAiResource,
+ NewLuceeResource,
+ NewMariadbResource,
+ NewMongodbResource,
+ NewNextcloudResource,
+ NewNifiResource,
+ NewNodejsResource,
+ NewNoderedResource,
+ NewOpenwhiskResource,
+ NewPgadminResource,
+ NewPostgresResource,
+ NewRabbitmqResource,
+ NewRedisResource,
+ NewS3Resource,
+ NewS3bucketResource,
+ NewSupersetResource,
+ NewTemplateResource,
+ NewTenantResource,
+ NewValoTenantResource,
+ NewVappResource,
+ NewVcComplexResource,
+ NewVcMgmtSthutrvalClusterResource,
+ NewVcNsxtResource,
+ NewVcOrgResource,
+ NewVcOrgSaasResource,
+ NewVcVdcResource,
+ NewVcVdcGroupResource,
+ NewVcVmResource,
+ NewVcVmV2Resource,
+ NewVcVmV3Resource,
+ NewVcexternalipResource,
+ NewVmpostgreResource,
+ NewZonesV2Resource,
+ NewClickhouseDatabaseResource,
+ NewClickhouseUserResource,
+ NewK8sOpenbaoKvResource,
+ NewK8sOpenbaoPolicyResource,
+ NewK8sOpenbaoSecretsResource,
+ NewK8sSthutrvalClusterUserResource,
+ NewKafkaTopicResource,
+ NewKafkaUserResource,
+ NewLlmAiSecretsResource,
+ NewLlmAiTeamResource,
+ NewMariadbDatabaseResource,
+ NewMariadbUserResource,
+ NewMongodbUserResource,
+ NewNextcloudBackupResource,
+ NewPostgresBackupResource,
+ NewPostgresDatabaseResource,
+ NewPostgresUserResource,
+ NewS3SubUserResource,
+ NewVcOrgUserResource,
+ NewVcVdcGroupVdcResource,
+ NewZonesV2RecordResource,
+ }
+}
diff --git a/provider/main.go b/provider/main.go
index 56c70be..5a49677 100644
--- a/provider/main.go
+++ b/provider/main.go
@@ -17,7 +17,7 @@ import (
)
var (
- version string = "5.0.73"
+ version string = "5.0.74"
)
func main() {