From 8f552ecacc3844b78883c016f3c62d907de679d5 Mon Sep 17 00:00:00 2001 From: Repinoid Date: Sun, 20 Sep 2026 18:11:17 +0300 Subject: [PATCH] fix: harden modifier resource lifecycle Validate required modifier parameters, refresh modifier state from parent state_params, preserve operation timeout and log level during update, and normalize nested modifier payloads as JSON. Document the modifier contract, vcOrg/vcNsxt usage, and the intentionally unsupported rollback semantics. --- .../internal/loader/loader.go | 5 +++ .../internal/templates/modifier.go | 36 +++++++++++++++++-- docs/ARCHITECTURE_NEW.md | 19 ++++++++++ 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/TOOLS/resource-generator/internal/loader/loader.go b/TOOLS/resource-generator/internal/loader/loader.go index b6364d6..f914bb7 100644 --- a/TOOLS/resource-generator/internal/loader/loader.go +++ b/TOOLS/resource-generator/internal/loader/loader.go @@ -70,6 +70,11 @@ func LoadSpecs(dir string) ([]types.GenResource, []types.GenSubresource, []types Params: ConvertParams(op.Params), } modifier.SchemaParams = modifier.Params + for idx := range modifier.SchemaParams { + if modifier.SchemaParams[idx].HasSubParams { + modifier.SchemaParams[idx].IsJson = true + } + } params.Analyze(&modifier.UsesBool, &modifier.UsesInt64, &modifier.UsesString, &modifier.HasDefaults, &modifier.NeedsBoolDefault, &modifier.NeedsInt64Default, &modifier.NeedsStringDefault, modifier.SchemaParams) modifier.NeedsJsonPlanMod = params.AnalyzeJsonPlanMod(modifier.SchemaParams) modifiers = append(modifiers, modifier) diff --git a/TOOLS/resource-generator/internal/templates/modifier.go b/TOOLS/resource-generator/internal/templates/modifier.go index a6ca433..dd0898e 100644 --- a/TOOLS/resource-generator/internal/templates/modifier.go +++ b/TOOLS/resource-generator/internal/templates/modifier.go @@ -76,6 +76,14 @@ func (r *{{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Resource) Create var plan {{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Model resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) if resp.Diagnostics.HasError() { return } + {{- range .SchemaParams }} + {{- if and .Required (eq (ParamDefaultExpr .) "") }} + if plan.{{ToCamel .Code}}.IsNull() || plan.{{ToCamel .Code}}.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "{{ToSnake .Code}} is required.") + return + } + {{- end }} + {{- end }} instanceUID := strings.TrimSpace(plan.{{ToCamel .ServiceName}}ID.ValueString()) if instanceUID == "" { resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра"); return } params := resources_core.CompactParams(map[string]string{ @@ -97,20 +105,44 @@ func (r *{{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Resource) Read(c var state {{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Model resp.Diagnostics.Append(req.State.Get(ctx, &state)...) if resp.Diagnostics.HasError() { return } - resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) + if state.ID.IsNull() || state.ID.IsUnknown() { return } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.{{ToCamel .ServiceName}}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.{{ToCamel .ServiceName}}ID.ValueString(), {{.ServiceID}}, state, nil, []resources_core.InputField{ + {{- range .SchemaParams }} + {Code: "{{.Code}}", Field: "{{ToCamel .Code}}", Type: "{{.Type}}"}, + {{- end }} + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) } func (r *{{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { var plan {{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Model resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) if resp.Diagnostics.HasError() { return } + {{- range .SchemaParams }} + {{- if and .Required (eq (ParamDefaultExpr .) "") }} + if plan.{{ToCamel .Code}}.IsNull() || plan.{{ToCamel .Code}}.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "{{ToSnake .Code}} is required.") + return + } + {{- end }} + {{- end }} instanceUID := strings.TrimSpace(plan.{{ToCamel .ServiceName}}ID.ValueString()) params := resources_core.CompactParams(map[string]string{ {{- range .SchemaParams }} "{{.Code}}": {{ParamFormat . (printf "plan.%s" (ToCamel .Code))}}, {{- end }} }) - if err := resources_core.RunOperationByCode(ctx, r.client, instanceUID, "{{.OperationName}}", params); err != nil { + operationTimeout := "" + if !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, "{{.OperationName}}", params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()); return } plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "{{.OperationName}}", "{{.ModifierName}}")) diff --git a/docs/ARCHITECTURE_NEW.md b/docs/ARCHITECTURE_NEW.md index 73f6ae6..0a4d558 100644 --- a/docs/ARCHITECTURE_NEW.md +++ b/docs/ARCHITECTURE_NEW.md @@ -43,6 +43,25 @@ - `universal_rebuild/tools/gen/main.go` - Читает YAML и генерирует ресурсы + `registry.go`. +### 1.6 Отдельные modifier-ресурсы +Для parent-level операций `modify`, которые должны выполняться отдельным шагом Terraform-цепочки, используется `kind: modifier`. + +Пример: + +```yaml +- name: modify + kind: modifier + action: modify + modifier: ip_space + params: [] +``` + +Такой блок не попадает в обычный instance CRUD. Go-генератор создаёт отдельный ресурс с именем `nubes__`. Ресурс принимает ID родительского инстанса и параметры операции, выполняет parent `modify` при Create/Update и читает актуальные значения из `state_params` при Read. + +Для `vcOrg` используется modifier `ip_space` с параметром `vIPConfigure`; для `vcNsxt` используется modifier `network` с параметрами операции сетевой настройки. Nested API-параметры modifier-ресурсов передаются как JSON-строки, поэтому их Terraform-значения должны быть валидным JSON. + +Удаление modifier пока является no-op: подтверждённого обратного payload для отмены выделенных IP или SNAT нет. Операции удаления родительского сервиса не являются rollback и намеренно не вызываются. + --- ## 2) Как получить параметры сервиса (без instanceUid)