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.
This commit is contained in:
@@ -70,6 +70,11 @@ func LoadSpecs(dir string) ([]types.GenResource, []types.GenSubresource, []types
|
|||||||
Params: ConvertParams(op.Params),
|
Params: ConvertParams(op.Params),
|
||||||
}
|
}
|
||||||
modifier.SchemaParams = modifier.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)
|
params.Analyze(&modifier.UsesBool, &modifier.UsesInt64, &modifier.UsesString, &modifier.HasDefaults, &modifier.NeedsBoolDefault, &modifier.NeedsInt64Default, &modifier.NeedsStringDefault, modifier.SchemaParams)
|
||||||
modifier.NeedsJsonPlanMod = params.AnalyzeJsonPlanMod(modifier.SchemaParams)
|
modifier.NeedsJsonPlanMod = params.AnalyzeJsonPlanMod(modifier.SchemaParams)
|
||||||
modifiers = append(modifiers, modifier)
|
modifiers = append(modifiers, modifier)
|
||||||
|
|||||||
@@ -76,6 +76,14 @@ func (r *{{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Resource) Create
|
|||||||
var plan {{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Model
|
var plan {{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Model
|
||||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||||
if resp.Diagnostics.HasError() { return }
|
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())
|
instanceUID := strings.TrimSpace(plan.{{ToCamel .ServiceName}}ID.ValueString())
|
||||||
if instanceUID == "" { resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра"); return }
|
if instanceUID == "" { resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра"); return }
|
||||||
params := resources_core.CompactParams(map[string]string{
|
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
|
var state {{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Model
|
||||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||||
if resp.Diagnostics.HasError() { return }
|
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) {
|
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
|
var plan {{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Model
|
||||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||||
if resp.Diagnostics.HasError() { return }
|
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())
|
instanceUID := strings.TrimSpace(plan.{{ToCamel .ServiceName}}ID.ValueString())
|
||||||
params := resources_core.CompactParams(map[string]string{
|
params := resources_core.CompactParams(map[string]string{
|
||||||
{{- range .SchemaParams }}
|
{{- range .SchemaParams }}
|
||||||
"{{.Code}}": {{ParamFormat . (printf "plan.%s" (ToCamel .Code))}},
|
"{{.Code}}": {{ParamFormat . (printf "plan.%s" (ToCamel .Code))}},
|
||||||
{{- end }}
|
{{- 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
|
resp.Diagnostics.AddError("Ошибка клиента", err.Error()); return
|
||||||
}
|
}
|
||||||
plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "{{.OperationName}}", "{{.ModifierName}}"))
|
plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "{{.OperationName}}", "{{.ModifierName}}"))
|
||||||
|
|||||||
@@ -43,6 +43,25 @@
|
|||||||
- `universal_rebuild/tools/gen/main.go`
|
- `universal_rebuild/tools/gen/main.go`
|
||||||
- Читает YAML и генерирует ресурсы + `registry.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_<service>_<modifier>`. Ресурс принимает 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)
|
## 2) Как получить параметры сервиса (без instanceUid)
|
||||||
|
|||||||
Reference in New Issue
Block a user