10 Commits
Author SHA1 Message Date
Repinoid 4b218fbe33 fix: publish providers to registry bucket 2026-09-20 21:42:07 +03:00
Repinoid f32159b3af fix: publish generated providers to working registry 2026-09-20 20:00:34 +03:00
Repinoid a44894d877 1 2026-09-20 18:15:17 +03:00
Repinoid 8f552ecacc 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.
2026-09-20 18:11:17 +03:00
Repinoid a48b658d78 feat: add generated parent modify resources
Introduce the modifier YAML kind for delayed parent-level modify operations and generate dedicated Terraform resources with typed parameters. Keep modifier parameters out of the ordinary instance CRUD resource, register modifiers separately, and leave delete as a no-op until an inverse API payload is confirmed. Mark vcOrg and vcNsxt modify operations during YAML generation so the contract survives regeneration.
2026-09-20 18:05:24 +03:00
Repinoid 33672e05bf docs: add modifier resources ideology and architecture specification 2026-09-19 08:32:28 +03:00
Repinoid 0464a30642 docs(strategy): оркестрация взаимозависимых ресурсов и паттерн привязок
- complex_provisioning_workflow.md — цепочка vcOrg -> vcVdc -> vcNsxt -> Штурвал,
  включая modify-шаги и пошаговые модификации
- shturval_dev_provisioning_spec.md — точные параметры и ID операций DEV-стенда
  для цепочки развёртывания k8s_sthutrval_cluster
- terraform_association_pattern_and_pipeline.md — Resource Association Pattern
  (разделение сущности и ресурсов-привязок/модификаций)
2026-09-18 18:44:36 +03:00
Repinoid 106ddbe092 stand: switch every stand to actual provider versions (prod=1.0.0, dev=2.0.0, test=3.0.0)
Легаси-версии (5.0.x/5.1.x/3.1.x/2.1.x) в реестре отсутствуют (404), стенды не могли
пройти terraform init. Приведены к новой схеме нумерации от 2026-09-03.

- DEV  (nubes-dev):  CRUD, POSTGRES, SHTURVAL_MGMT -> 2.0.0
- TEST (nubes-test): CRUD, PG, POSTGRES, MARIA_DB, buck0, kuber, IOT_RMQ_DEMO -> 3.0.0
                     (DEV_STAND/IOT_KAFKA_DEMO тоже nubes-test -> 3.0.0)
- PROD (nubes):      PG1, POSTGRES, RABBIT -> 1.0.0
- README стендов TEST_STAND/buck0, TEST_STAND/PG: версии приведены к 3.0.0
- getting-started: убрана versioned-ссылка на доки (2.1.7) -> актуальный домен без версии
- .gitignore: откатано правило TMP/ (на remote TMP/init-test-*/main.tf трекаются)
2026-09-16 16:34:40 +03:00
Repinoid dbaeea5c89 stand(PGwNewRegistry): pin provider to 3.0.0 (new TEST scheme) 2026-09-16 16:33:27 +03:00
Repinoid 7eb8eb8859 chore(git): ignore TMP/ (local terraform init scratch) 2026-09-16 16:33:27 +03:00
39 changed files with 1060 additions and 39 deletions
+2
View File
@@ -68,6 +68,8 @@ HAR/*.har
*.token
secrets/private_key.asc
secrets/.s3cfg_registry
secrets/.s3cfg_provider
secrets/.s3cfg*
secrets/pearlharbor_registry.txt
secrets/id_ed25519.txt
+1 -1
View File
@@ -2,7 +2,7 @@ terraform {
required_providers {
nubes = {
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes"
version = "3.1.13"
version = "2.0.0"
}
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ terraform {
required_providers {
nubes = {
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-test/nubes"
version = "5.1.16"
version = "3.0.0"
}
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ terraform {
required_providers {
nubes = {
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes"
version = "3.1.1"
version = "2.0.0"
}
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ terraform {
required_providers {
nubes = {
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes"
version = "3.1.13"
version = "2.0.0"
}
}
}
+4 -4
View File
@@ -53,16 +53,16 @@ cd /home/naeel/TF/tf_provider
| Стенд | Namespace | Бинарники в S3 |
|---|---|---|
| dev | `nubes-dev` | `nubes-terraform-registry/tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes/0.0.1/` |
| test | `nubes-test` | `.../nubes-test/nubes/0.0.1/` |
| prod | `nubes` | `.../nubes/nubes/0.0.1/` |
| dev | `nubes-dev` | `terraform-registry/tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes/0.0.1/` |
| test | `nubes-test` | `terraform-registry/tf-registry.containerk8s.services.ngcloud.ru/nubes-test/nubes/0.0.1/` |
| prod | `nubes` | `terraform-registry/tf-registry.containerk8s.services.ngcloud.ru/nubes/nubes/0.0.1/` |
## Предусловия — ПРОВЕРЕНО, всё готово
- Go 1.23.1, docker 29.1.3, `mc`, GPG (`secrets/private_key.asc`, `public_key.asc`).
- Токены API: `secrets/{dev,test,prod}.token` на месте.
- API-эндпоинты доступны (HTTP 403 без токена — ожидаемо, токен передаёт 01).
- `TOOLS/config/<стенд>/operation_timeouts.json` на месте.
- `registry.env`: `REGISTRY_HOSTNAME=tf-registry.containerk8s.services.ngcloud.ru`, `S3_BUCKET=nubes-terraform-registry`.
- `registry.env`: `REGISTRY_HOSTNAME=tf-registry.containerk8s.services.ngcloud.ru`, `S3_BUCKET=terraform-registry`.
## Чего НЕ делать
- НЕ запускать `04_build_and_publish_docs.sh` (документация не нужна сейчас).
+1 -1
View File
@@ -2,7 +2,7 @@ terraform {
required_providers {
nubes = {
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes/nubes"
version = "2.1.26"
version = "1.0.0"
}
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ terraform {
required_providers {
nubes = {
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes/nubes"
version = "2.1.12"
version = "1.0.0"
}
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ terraform {
required_providers {
nubes = {
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes/nubes"
version = "2.1.10"
version = "1.0.0"
}
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ terraform {
required_providers {
nubes = {
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-test/nubes"
version = "5.0.5"
version = "3.0.0"
}
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ terraform {
required_providers {
nubes = {
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-test/nubes" # реестр провайдера (test)
version = "5.1.16" # версия провайдера Nubes
version = "3.0.0" # версия провайдера Nubes
}
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ terraform {
required_providers {
nubes = {
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-test/nubes"
version = "5.0.61"
version = "3.0.0"
}
}
}
+1 -1
View File
@@ -41,7 +41,7 @@ terraform destroy
## Провайдер
- **Источник**: `tf-registry.containerk8s.services.ngcloud.ru/nubes-test/nubes`
- **Версия**: `5.0.57`
- **Версия**: `3.0.0`
- **API**: `https://lk-api-gateway-test.ngcloud.ru/api/v1/svc`
## Структура параметров
+1 -1
View File
@@ -2,7 +2,7 @@ terraform {
required_providers {
nubes = {
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-test/nubes"
version = "5.0.64"
version = "3.0.0"
}
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ terraform {
required_providers {
nubes = {
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-test/nubes"
version = "5.0.5"
version = "3.0.0"
}
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ terraform {
required_providers {
nubes = {
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-test/nubes"
version = "5.1.7"
version = "3.0.0"
}
}
}
+2 -2
View File
@@ -14,7 +14,7 @@ cp terraform.tfvars.example terraform.tfvars
# api_token — Nubes API токен (TEST)
# s3_user_uid — UUID S3 Object Storage
# 4. Инициализация (скачает провайдер v5.0.75 из реестра)
# 4. Инициализация (скачает провайдер v3.0.0 из реестра)
terraform init
# 5. Проверка
@@ -37,5 +37,5 @@ terraform destroy
## Провайдер
- **Источник**: `tf-registry.containerk8s.services.ngcloud.ru/nubes-test/nubes`
- **Версия**: `5.0.75`
- **Версия**: `3.0.0`
- **Registry**: `https://tf-registry.containerk8s.services.ngcloud.ru`
+1 -1
View File
@@ -2,7 +2,7 @@ terraform {
required_providers {
nubes = {
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-test/nubes"
version = "5.0.64"
version = "3.0.0"
}
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ terraform {
required_providers {
nubes = {
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-test/nubes"
version = "5.0.68"
version = "3.0.0"
}
}
}
+1
View File
@@ -41,6 +41,7 @@ type OperationSpec struct {
ID int `yaml:"id"`
Kind string `yaml:"kind"`
Action string `yaml:"action"`
Modifier string `yaml:"modifier,omitempty"`
Subresource string `yaml:"subresource,omitempty"`
Man string `yaml:"man,omitempty"`
Params []ParamSpec `yaml:"params"`
@@ -24,11 +24,12 @@ import (
"resource-generator/internal/types"
)
// LoadSpecs загружает все YAML-спеки из директории и строит GenResource/GenSubresource/GenAction.
func LoadSpecs(dir string) ([]types.GenResource, []types.GenSubresource, []types.GenAction, error) {
// LoadSpecs загружает все YAML-спеки из директории и строит модели всех ресурсов.
func LoadSpecs(dir string) ([]types.GenResource, []types.GenSubresource, []types.GenAction, []types.GenModifier, error) {
var services []types.GenResource
var subs []types.GenSubresource
var actions []types.GenAction
var modifiers []types.GenModifier
domainServiceIDsSet := map[int]struct{}{}
walkErr := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
@@ -56,6 +57,29 @@ func LoadSpecs(dir string) ([]types.GenResource, []types.GenSubresource, []types
modifyParams := []types.Param{}
supportsSuspendDestroy := false
for _, op := range spec.Operations {
if op.Kind == "modifier" {
name := strings.TrimSpace(op.Modifier)
if name == "" {
name = strings.TrimSpace(op.Action)
}
modifier := types.GenModifier{
ServiceName: spec.Name,
ServiceID: spec.ServiceID,
ModifierName: name,
OperationName: op.Action,
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)
continue
}
if op.Kind != "instance" {
continue
}
@@ -192,7 +216,7 @@ func LoadSpecs(dir string) ([]types.GenResource, []types.GenSubresource, []types
})
if walkErr != nil {
return nil, nil, nil, walkErr
return nil, nil, nil, nil, walkErr
}
domainServiceIDs := make([]int, 0, len(domainServiceIDsSet))
@@ -221,8 +245,14 @@ func LoadSpecs(dir string) ([]types.GenResource, []types.GenSubresource, []types
}
return actions[i].ServiceName < actions[j].ServiceName
})
sort.Slice(modifiers, func(i, j int) bool {
if modifiers[i].ServiceName == modifiers[j].ServiceName {
return modifiers[i].ModifierName < modifiers[j].ModifierName
}
return modifiers[i].ServiceName < modifiers[j].ServiceName
})
return services, subs, actions, nil
return services, subs, actions, modifiers, nil
}
// ConvertParams конвертирует []ParamSpec → []Param с нормализацией типов.
@@ -313,6 +343,7 @@ var KnownKinds = map[string]bool{
"instance": true,
"subresource": true,
"action": true,
"modifier": true,
}
// ValidateSpec проверяет YAML-спек на обязательные поля и неизвестные kinds.
@@ -329,7 +360,7 @@ func ValidateSpec(path string, spec *types.ServiceSpec) error {
return fmt.Errorf("operation[%d] %q: missing required field: kind", i, op.Name)
}
if !KnownKinds[op.Kind] {
return fmt.Errorf("operation[%d] %q: unknown kind %q (valid: instance, subresource, action)", i, op.Name, op.Kind)
return fmt.Errorf("operation[%d] %q: unknown kind %q (valid: instance, subresource, action, modifier)", i, op.Name, op.Kind)
}
if op.Action == "" {
return fmt.Errorf("operation[%d] %q (kind=%s): missing required field: action", i, op.Name, op.Kind)
@@ -337,6 +368,9 @@ func ValidateSpec(path string, spec *types.ServiceSpec) error {
if op.Kind == "subresource" && op.Subresource == "" {
return fmt.Errorf("operation[%d] %q (kind=subresource): missing required field: subresource", i, op.Name)
}
if op.Kind == "modifier" && strings.TrimSpace(op.Action) == "" {
return fmt.Errorf("operation[%d] %q (kind=modifier): missing required field: action", i, op.Name)
}
}
return nil
}
@@ -0,0 +1,162 @@
package templates
const Modifier = `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"
{{- if .NeedsBoolDefault }}
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
{{- end }}
{{- if .NeedsInt64Default }}
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
{{- end }}
{{- if .NeedsStringDefault }}
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
{{- end }}
{{- if .NeedsJsonPlanMod }}
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
{{- end }}
"github.com/hashicorp/terraform-plugin-framework/types"
)
// Code generated by TOOLS/resource-generator. DO NOT EDIT.
// Modifier: {{.ServiceName}}.{{.ModifierName}}
var _ resource.Resource = &{{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Resource{}
type {{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Resource struct {
client *core.UniversalClient
}
type {{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Model struct {
ID types.String ` + "`" + `tfsdk:"id"` + "`" + `
{{ToCamel .ServiceName}}ID types.String ` + "`" + `tfsdk:"{{ToSnake .ServiceName}}_id"` + "`" + `
OperationTimeout types.String ` + "`" + `tfsdk:"operation_timeout"` + "`" + `
LogLevel types.String ` + "`" + `tfsdk:"log_level"` + "`" + `
{{- range .SchemaParams }}
{{ToCamel .Code}} {{ParamType .}} ` + "`" + `tfsdk:"{{ToSnake .Code}}"` + "`" + `
{{- end }}
}
func New{{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Resource() resource.Resource {
return &{{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Resource{}
}
func (r *{{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_{{.ServiceName}}_{{.ModifierName}}"
}
func (r *{{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
attrs := map[string]schema.Attribute{
"id": schema.StringAttribute{Computed: true},
"{{ToSnake .ServiceName}}_id": schema.StringAttribute{Required: true},
"operation_timeout": schema.StringAttribute{Optional: true},
"log_level": schema.StringAttribute{Optional: true},
{{- range .SchemaParams }}
"{{ToSnake .Code}}": schema.{{if eq (ParamType .) "types.Bool"}}Bool{{else if eq (ParamType .) "types.Int64"}}Int64{{else}}String{{end}}Attribute{
{{- if and .Required (eq (ParamDefaultExpr .) "") }}Required: true,{{else}}Optional: true,{{end}}
{{- if ne (ParamDefaultExpr .) "" }}Computed: true, Default: {{ParamDefaultExpr .}},{{- end }}
{{- if ne (ParamDescription .) "" }}MarkdownDescription: {{ParamDescription .}},{{end}}
{{- if .Sensitive }}Sensitive: true,{{end}}
{{- if .IsJson }}PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()},{{end}}
},
{{- end }}
}
resp.Schema = schema.Schema{Attributes: attrs}
}
func (r *{{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
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{
{{- range .SchemaParams }}
"{{.Code}}": {{ParamFormat . (printf "plan.%s" (ToCamel .Code))}},
{{- end }}
})
operationTimeout := ""
if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { operationTimeout = plan.OperationTimeout.ValueString() }
if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) }
if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "{{.OperationName}}", params, operationTimeout); err != nil {
resp.Diagnostics.AddError("Ошибка клиента", err.Error()); return
}
plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "{{.OperationName}}", "{{.ModifierName}}"))
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}
func (r *{{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state {{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Model
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.{{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 }}
})
operationTimeout := ""
if !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}}"))
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}
func (r *{{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
// No-op: no confirmed inverse payload exists for this modifier.
}
func (r *{{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Resource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil { return }
client, ok := req.ProviderData.(*core.UniversalClient)
if !ok { resp.Diagnostics.AddError("Error", "Invalid client type"); return }
r.client = client
}
`
@@ -145,3 +145,22 @@ type GenAction struct {
NeedsJsonPlanMod bool
NeedsStringsImport bool
}
// GenModifier — отдельный ресурс для отложенной parent-level modify операции.
// Delete намеренно не содержит rollback: API-контракт обратного payload не подтверждён.
type GenModifier struct {
ServiceName string
ServiceID int
ModifierName string
OperationName string
Params []Param
SchemaParams []Param
UsesBool bool
UsesInt64 bool
UsesString bool
HasDefaults bool
NeedsBoolDefault bool
NeedsInt64Default bool
NeedsStringDefault bool
NeedsJsonPlanMod bool
}
@@ -135,8 +135,38 @@ func WriteActionResource(outDir string, act types.GenAction) error {
return os.WriteFile(filePath, formatted, 0644)
}
// WriteModifierResource генерирует отдельный ресурс для parent-level modify.
func WriteModifierResource(outDir string, modifier types.GenModifier) error {
name := fmt.Sprintf("%s_%s", modifier.ServiceName, modifier.ModifierName)
fileName := fmt.Sprintf("%d_%s_modifier.go", modifier.ServiceID, name)
filePath := filepath.Join(outDir, fileName)
tpl, err := template.New("modifier").Funcs(template.FuncMap{
"ToCamel": helpers.ToCamel,
"ToSnake": helpers.ToSnake,
"ParamType": helpers.ParamType,
"ParamDefaultExpr": helpers.ParamDefaultExpr,
"ParamFormat": helpers.ParamFormat,
"ParamDescription": helpers.ParamDescription,
"bt": func() string { return "`" },
}).Parse(templates.Modifier)
if err != nil {
return err
}
var buf bytes.Buffer
if err := tpl.Execute(&buf, modifier); err != nil {
return err
}
formatted, err := helpers.FormatSourceOrWarn(filePath, buf.Bytes())
if err != nil {
return err
}
return os.WriteFile(filePath, formatted, 0644)
}
// WriteRegistry генерирует registry.go со списком всех ресурсов.
func WriteRegistry(outDir string, services []types.GenResource, subs []types.GenSubresource, actions []types.GenAction) error {
func WriteRegistry(outDir string, services []types.GenResource, subs []types.GenSubresource, actions []types.GenAction, modifiers []types.GenModifier) error {
var buf bytes.Buffer
buf.WriteString("package resources_gen\n\n")
buf.WriteString("import \"github.com/hashicorp/terraform-plugin-framework/resource\"\n\n")
@@ -152,6 +182,9 @@ func WriteRegistry(outDir string, services []types.GenResource, subs []types.Gen
for _, act := range actions {
buf.WriteString(fmt.Sprintf("\t\tNew%[1]sResource,\n", helpers.ToCamel(act.ServiceName+"_"+act.ActionName)))
}
for _, modifier := range modifiers {
buf.WriteString(fmt.Sprintf("\t\tNew%[1]sResource,\n", helpers.ToCamel(modifier.ServiceName+"_"+modifier.ModifierName)))
}
buf.WriteString("\t}\n")
buf.WriteString("}\n")
+8 -2
View File
@@ -48,7 +48,7 @@ func run() error {
return fmt.Errorf("NUBES_RESOURCES_GEN_DIR is required — must point to generated/{stand}/go/")
}
instanceResources, subresources, actions, err := loader.LoadSpecs(resourcesDir)
instanceResources, subresources, actions, modifiers, err := loader.LoadSpecs(resourcesDir)
if err != nil {
return err
}
@@ -72,6 +72,12 @@ func run() error {
writeErrs = append(writeErrs, serviceWriteErr{kind: "action", name: name, err: err})
}
}
for _, modifier := range modifiers {
if err := writers.WriteModifierResource(outDir, modifier); err != nil {
name := fmt.Sprintf("%s_%s", modifier.ServiceName, modifier.ModifierName)
writeErrs = append(writeErrs, serviceWriteErr{kind: "modifier", name: name, err: err})
}
}
if len(writeErrs) > 0 {
var b strings.Builder
@@ -82,7 +88,7 @@ func run() error {
return errors.New(strings.TrimSpace(b.String()))
}
if err := writers.WriteRegistry(outDir, instanceResources, subresources, actions); err != nil {
if err := writers.WriteRegistry(outDir, instanceResources, subresources, actions, modifiers); err != nil {
return err
}
+1 -1
View File
@@ -141,7 +141,7 @@ rm -f "$FAILURES_FILE"
# Auto-rebuild yaml-generator if sources are newer than binary
BIN="${ROOT_DIR}/TOOLS/bin/yaml-generator"
SRC="${ROOT_DIR}/TOOLS/yaml-generator/"
if [[ ! -x "$BIN" ]] || [[ "$SRC" -nt "$BIN" ]]; then
if [[ ! -x "$BIN" ]] || find "$SRC" -type f -newer "$BIN" -print -quit | grep -q .; then
echo "Building yaml-generator..."
(cd "${ROOT_DIR}/TOOLS/yaml-generator" && go build -o "$BIN" .)
fi
@@ -93,9 +93,20 @@ echo "Generating resources from unified YAML specs..."
TMP_GEN_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_GEN_DIR"' EXIT
RESOURCE_GENERATOR_BIN="${ROOT_DIR}/TOOLS/bin/resource-generator"
RESOURCE_GENERATOR_SRC="${ROOT_DIR}/TOOLS/resource-generator"
DOCS_GENERATOR_BIN="${ROOT_DIR}/TOOLS/bin/docs-generator"
DOCS_GENERATOR_SRC="${ROOT_DIR}/TOOLS/docs-generator"
if [[ ! -x "$RESOURCE_GENERATOR_BIN" ]] || find "$RESOURCE_GENERATOR_SRC" -type f -newer "$RESOURCE_GENERATOR_BIN" -print -quit | grep -q .; then
(cd "$RESOURCE_GENERATOR_SRC" && go build -o "$RESOURCE_GENERATOR_BIN" .)
fi
if [[ ! -x "$DOCS_GENERATOR_BIN" ]] || find "$DOCS_GENERATOR_SRC" -type f -newer "$DOCS_GENERATOR_BIN" -print -quit | grep -q .; then
(cd "$DOCS_GENERATOR_SRC" && go build -o "$DOCS_GENERATOR_BIN" .)
fi
NUBES_RESOURCES_DIR="$RESOURCES_YAML_DIR" \
NUBES_RESOURCES_GEN_DIR="$TMP_GEN_DIR" \
${ROOT_DIR}/TOOLS/bin/resource-generator
"$RESOURCE_GENERATOR_BIN"
mkdir -p "$GO_OUTPUT_DIR"
rm -rf "${GO_OUTPUT_DIR}"/*
@@ -113,7 +124,7 @@ if [[ "$DOCS_API_ENDPOINT" != *"/index.cfm"* ]] && [[ "$DOCS_API_ENDPOINT" != *"
DOCS_API_ENDPOINT="${DOCS_API_ENDPOINT%/}/index.cfm"
fi
${ROOT_DIR}/TOOLS/bin/docs-generator \
"$DOCS_GENERATOR_BIN" \
-resources "$RESOURCES_YAML_DIR" \
-docs "$DOCS_DIR" \
-services "$SERVICES_LIST_PATH" \
+11 -2
View File
@@ -38,6 +38,9 @@ if [[ -z "$PROFILE_DIR" ]]; then
exit 2
fi
"${ROOT_DIR}/TOOLS/scripts/01_generate_yamls.sh" --profile "$PROFILE_DIR"
"${ROOT_DIR}/TOOLS/scripts/02_generate_resources_and_docs_v2.sh" --profile "$PROFILE_DIR"
resolve_root_path() {
local path_value="$1"
if [[ -z "$path_value" ]]; then
@@ -51,7 +54,13 @@ resolve_root_path() {
echo "${ROOT_DIR}/${path_value}"
}
S3CFG_REGISTRY="${S3CFG_REGISTRY:-${ROOT_DIR}/secrets/.s3cfg_registry}"
if [[ -z "${S3CFG_REGISTRY:-}" ]]; then
if [[ -f "${ROOT_DIR}/secrets/.s3cfg_provider" ]]; then
S3CFG_REGISTRY="${ROOT_DIR}/secrets/.s3cfg_provider"
else
S3CFG_REGISTRY="${ROOT_DIR}/secrets/.s3cfg_registry"
fi
fi
TIMEOUTS_SOURCE="${TIMEOUTS_SOURCE:-${PROFILE_DIR}/operation_timeouts.json}"
PROFILE_RESOURCES_YAML_DIR="${ROOT_DIR}/generated/$(basename "$PROFILE_DIR")/resources_yaml"
PROFILE_GENERATED_GO_DIR="${ROOT_DIR}/generated/$(basename "$PROFILE_DIR")/go"
@@ -153,7 +162,7 @@ validate_registry_completeness() {
tmp_registered="$(mktemp)"
tmp_missing="$(mktemp)"
find "$resources_gen_dir" -maxdepth 1 -type f -name '*_resource.go' -print0 \
find "$resources_gen_dir" -maxdepth 1 -type f \( -name '*_resource.go' -o -name '*_modifier.go' -o -name '*_action.go' \) -print0 \
| xargs -0 grep -hE 'func[[:space:]]+New[[:alnum:]_]+Resource[[:space:]]*\(' \
| sed -E 's/.*(New[[:alnum:]_]+Resource).*/\1/' \
| sort -u > "$tmp_expected"
+10
View File
@@ -71,6 +71,16 @@ func main() {
if err != nil {
panic(err)
}
for idx := range ops {
if strings.EqualFold(ops[idx].Action, "modify") && svc.ID == 19 {
ops[idx].Kind = "modifier"
ops[idx].Modifier = "ip_space"
}
if strings.EqualFold(ops[idx].Action, "modify") && svc.ID == 22 {
ops[idx].Kind = "modifier"
ops[idx].Modifier = "network"
}
}
specYAML := types.ServiceSpec{
Name: name,
+1 -1
View File
@@ -5,7 +5,7 @@
| Стенд | Namespace | Версия | Дата заливки |
|---|---|---|---|
| PROD | `nubes` | `1.0.0` | 2026-09-03 | (новая нумерация) |
| DEV | `nubes-dev` | `2.0.0` | 2026-09-03 | (новая нумерация) |
| DEV | `nubes-dev` | `2.0.1` | 2026-09-20 | (добавлены parent modify/modifier ресурсы) |
| TEST | `nubes-test` | `3.0.0` | 2026-09-03 | (новая нумерация) |
## Как проверить
+1 -1
View File
@@ -248,7 +248,7 @@ resource "nubes_nodejs" "app3" {
Ниже полный пример `resources.tf` для RabbitMQ + Lucee UI + NodeJS воркера.
Комментарий: UI Lucee отправляет CRUD‑запросы в RabbitMQ, а применение изменений в Postgres выполняет отдельный воркер на NodeJS.
Сервис Postgres должен быть запущен заранее. В данном примере используется Postgres из раздела https://tf-registry.containerk8s.services.ngcloud.ru/docs/nubes/nubes/2.1.7/30_registry/guides/getting-started/#lucee-postgress
Сервис Postgres должен быть запущен заранее. В данном примере используется Postgres из раздела https://tf-docs.nodejsk8s.dev.nubes.ru/nubes/30_registry/guides/getting-started/#lucee-postgress
```hcl title="resources.tf"
# RabbitMQ кластер для демо.
@@ -0,0 +1,138 @@
# Оркестрация взаимозависимых ресурсов и пошаговых модификаций (на примере Штурвал)
## 1. Контекст и проблематика
### Исходная последовательность развертывания
Для развертывания инстанса сервиса **Штурвал** требуется подготовить сетевую и виртуальную инфраструктуру, состоящую из трёх взаимозависимых компонентов:
1. `vcOrg/create` — создание виртуальной организации (vCD Org).
2. `vcVdc/create` — создание виртуального дата-центра (vDC) внутри организации.
3. `vcNsxt/create` — создание сетевого шлюза NSX-T (включение AVI, выделение 4 Service Engine).
4. `vcOrg/modify` — модификация организации (добавление 3 внешних IP-адресов).
5. `vcNsxt/modify` — повторная модификация NSX-T (включение SNAT, привязка выделенного `ipSpace` из `vcOrg`).
6. `Штурвал/create` — создание кластера сервиса «Штурвал».
### В чём архитектурная сложность для Terraform
В стандартной декларативной модели Terraform каждый ресурс управляется монолитно: один блок `resource` соответствует полному жизненному циклу одной сущности (Create -> Read -> Update -> Delete).
В описанном сценарии возникает **чередующаяся (interleaved) зависимость**:
* `vcOrg` должен существовать до `vcVdc` и `vcNsxt`.
* Но добавление IP-адресов в `vcOrg` (шаг 4) и настройка SNAT в `vcNsxt` (шаг 5) должны выполняться **после** создания базового `vcNsxt` (шаг 3).
* Штурвал (шаг 6) требует, чтобы и IP-адреса, и SNAT уже были применены.
Если пытаться упаковать шаги 1 и 4 в один ресурс `cloud_vc_org`, а шаги 3 и 5 — в один `cloud_vc_nsxt`, возникает тупик в графе зависимостей Terraform (Directed Acyclic Graph, DAG), либо API вернет ошибку из-за несвоевременного вызова параметров.
---
## 2. Архитектурное решение: Паттерн отдельных ресурсов модификации (Subresource / Action Pattern)
Канонический подход в экосистеме Terraform (аналогично `aws_security_group` + `aws_security_group_rule`, `aws_vpc` + `aws_route`) — **декомпозиция отложенных действий и привязок в отдельные управляемые ресурсы провайдера**.
### Структура ресурсов
1. **Базовые ресурсы жизненного цикла (Core Instances):**
* `cloud_vc_org` — создает и держит базу организации.
* `cloud_vc_vdc` — создает VDC внутри Org.
* `cloud_vc_nsxt` — создает NSX-T шлюз (AVI, 4 SE).
2. **Ресурсы отложенной конфигурации / модификаций (Action / Subresources):**
* `cloud_vc_org_ip_allocation` (или `cloud_vc_org_modify_ip`) — управляет пулом выделенных IP-адресов организации.
* `cloud_vc_nsxt_snat` (или `cloud_vc_nsxt_modify_snat`) — управляет правилом SNAT и связкой с `ip_space`.
3. **Целевой сервис:**
* `cloud_shturval` — разворачивает кластер Штурвал.
### Пример манифеста HCL
```hcl
# 1. Создание организации
resource "cloud_vc_org" "org" {
name = "demo-org"
}
# 2. Создание VDC
resource "cloud_vc_vdc" "vdc" {
name = "demo-vdc"
org_id = cloud_vc_org.org.id
}
# 3. Создание NSX-T (включение AVI и 4 Service Engine)
resource "cloud_vc_nsxt" "nsxt" {
name = "demo-nsxt"
vdc_id = cloud_vc_vdc.vdc.id
enable_avi = true
service_engines = 4
}
# 4. Модификация vcOrg: добавление 3 IP после готовности NSX-T
resource "cloud_vc_org_ip_allocation" "org_ips" {
org_id = cloud_vc_org.org.id
ip_count = 3
# Явная зависимость гарантирует выполнение после создания NSX-T
depends_on = [cloud_vc_nsxt.nsxt]
}
# 5. Модификация vcNsxt: включение SNAT с ipSpace из vcOrg
resource "cloud_vc_nsxt_snat" "snat" {
nsxt_id = cloud_vc_nsxt.nsxt.id
ip_space = cloud_vc_org_ip_allocation.org_ips.ip_space_id
enabled = true
}
# 6. Создание сервиса Штурвал
resource "cloud_shturval" "cluster" {
name = "demo-shturval"
vdc_id = cloud_vc_vdc.vdc.id
# Зависит от полной готовности сетевой связки
depends_on = [
cloud_vc_nsxt_snat.snat,
cloud_vc_org_ip_allocation.org_ips
]
}
```
Terraform самостоятельно строит идеальный граф исполнения:
```mermaid
graph TD
A[cloud_vc_org] --> B[cloud_vc_vdc]
B --> C[cloud_vc_nsxt]
C --> D[cloud_vc_org_ip_allocation]
D --> E[cloud_vc_nsxt_snat]
E --> F[cloud_shturval]
```
---
## 3. Интеграция в провайдер
### Реализация через генератор провайдера
Согласно политике репозитория (Immutability Policy), код конкретных ресурсов не правится вручную, а генерируется:
1. В схему генератора добавляются описания новых сущностей:
* Тип `action` или `subresource` для вызова эндпоинтов модификации.
* Контракты входных/выходных атрибутов (`org_id`, `ip_count`, `ip_space_id`, `nsxt_id`, `enabled`).
2. Кодогенератор генерирует стандартные CRUD-структуры Terraform Plugin Framework / SDK.
### Жизненный цикл ресурсов модификации
* **Create**:
- Вызывает соответствующий API-метод (`POST /api/v1/vcOrg/{id}/modify` или `/api/v1/vcNsxt/{id}/modify`).
- Дожидается применения задачи (task tracking / polling).
- Сохраняет идентификатор операции или полученный `ip_space_id` в Terraform State.
* **Read**:
- Запрашивает текущее состояние родительского ресурса через GET API.
- Проверяет, выделены ли IP / активен ли SNAT.
* **Update**:
- Если меняется количество IP или настройки SNAT — отправляет повторный запрос на модификацию.
* **Delete (terraform destroy)**:
- При уничтожении инфраструктуры порядок разворачивается в обратную сторону.
- Сначала удаляется `cloud_shturval`.
- Затем `cloud_vc_nsxt_snat` отключает SNAT.
- Затем `cloud_vc_org_ip_allocation` освобождает выделенные IP.
- И только затем удаляются базовые `vcNsxt`, `vcVdc` и `vcOrg`.
---
## 4. Альтернативные подходы
1. **Smart Provider (комбинированный Create)**:
- Если API позволяет вызывать шаги последовательно внутри одного HTTP-сеанса бэкенда, провайдер мог бы скрыть это внутри `Create` ресурса `cloud_shturval`.
- *Минус*: теряется гибкость и прозрачность статусов; сбой на промежуточном этапе оставляет "зависшие" ресурсы в облаке без записи в tfstate.
2. **Модули Terraform (Module Wrapper)**:
- Описанная выше структура ресурсов упаковывается в официальный Terraform-модуль `terraform-nubes-shturval`, скрывая сложность связей от конечного пользователя и предоставляя простой интерфейс ввода параметров.
@@ -0,0 +1,237 @@
# Архитектурная концепция: Modifier-ресурсы (Идеология, правила и интеграция в Terraform Provider)
## 1. Введение и архитектурный контекст
### 1.1. Проблема: Чередующиеся зависимости (Interleaved Lifecycle)
В классической декларативной модели Terraform каждый ресурс управляется монолитно: один блок `resource` соответствует полному жизненному циклу одной сущности (Create -> Read -> Update -> Delete).
Однако при комплексном развертывании инфраструктуры у облачного провайдера (например, цепочка для сервиса **Штурвал** `k8s_sthutrval_cluster`) возникает жесткая **чередующаяся зависимость**:
1. `vcOrg/create` — создание тенанта (Организации).
2. `vcVdc/create` — создание виртуального датацентра внутри Организации.
3. `vcNsxt/create` — создание базового сетевого шлюза (Edge Gateway) с включением AVI ALB и 4 Service Engine.
4. `vcOrg/modify` — выделение пула из 3 внешних IP-адресов в Организации (требует, чтобы NSX-T уже существовал).
5. `vcNsxt/modify` — включение правила SNAT на шлюзе с привязкой `ipSpace`, созданного на шаге 4 (требует наличия свободных IP).
6. `k8sSthutrvalCluster/create` — развертывание кластера Штурвал (требует настроенного SNAT, AVI и свободных IP).
Попытка «зашить» шаги 4 и 5 внутрь основных ресурсов `vc_org` и `vc_nsxt` приводит к тупику в графе зависимостей Terraform (DAG) или к ошибкам API из-за несвоевременного вызова параметров.
### 1.2. Решение: Класс Modifier-ресурсов
Для разрешения таких зависимостей в архитектуру провайдера вводится специальный класс сущностей — **Modifier-ресурсы (Модификаторы)**.
* **Instance-ресурс (базовый сервис)** — отвечает за владение и жизненный цикл инстанса в облаке (`POST /create`, `GET /state`, `DELETE /delete`).
* **Modifier-ресурс (модификатор)** — отвечает за выполнение отложенной операции конфигурирования/связывания над уже созданным инстансом (`POST /modify`), являясь самостоятельным блоком в графе Terraform.
---
## 2. Идеология Terraform: Почему это каноничный подход
Разделение базовой сущности и отложенных настроек/связей на отдельные ресурсы — это официальный архитектурный паттерн Terraform (**Resource Association / Separate Resource Pattern**), используемый во всех провайдерах первого эшелона:
* **AWS**: `aws_security_group` (базовый контейнер) + `aws_security_group_rule` (отдельные правила привязки).
* **AWS**: `aws_vpc` + `aws_route_table_association` / `aws_vpn_gateway_attachment`.
* **GCP**: `google_project` + `google_project_iam_binding`.
### Преимущества подхода:
1. **Естественный граф зависимостей (DAG)**: Terraform выстраивает порядок шагов исключительно между блоками `resource`. Вынос модификаций в отдельные ресурсы позволяет вклинивать промежуточные сервисы между созданием родителя и его донастройкой.
2. **Симметричный и безопасный `destroy`**: При удалении стека Terraform автоматически разворачивает порядок:
* Сначала удаляется `k8s_sthutrval_cluster`.
* Затем Modifier шлюза отключает SNAT.
* Затем Modifier организации освобождает выделенные IP.
* И только потом удаляются базовые шлюз, VDC и организация.
3. **Предсказуемый `plan` и локализация сбоев**: Любая ошибка настройки локализуется в блоке модификатора, не повреждая стейт базового инстанса.
---
## 3. Правила определения входных данных Modifier-ресурса
Входные данные Modifier-ресурса определяются строго детерминированно на основе официальной YAML-спецификации сервиса из API (`operations` -> `name: modify`).
### Правило 1: Якорь привязки (`instance_id` / `<service>_id`)
Каждый модификатор обязан содержать ровно один обязательный атрибут привязки:
* Имя: `instance_id` (или семантическое имя, например `org_id`, `nsxt_id`).
* Тип: `string` (UUID).
* В манифесте `.tf` значение передаётся как ссылка на атрибут родительского ресурса:
```hcl
org_id = nubes_vc_org.main.id
```
Это гарантирует, что Terraform выполнит модификатор **строго после** создания родителя.
### Правило 2: Строгая функциональная группа параметров
Операция `modify` в API может содержать множество разнородных параметров. Модификатор инкапсулирует **только одну целевую функциональную задачу**:
* **Для модификатора IP организации (`vc_org_ip_modifier`)**:
* Входные параметры берутся из секции `modify` YAML `vc_org`: массив `vIPConfigure` (`name`, `count`).
* **Для модификатора SNAT шлюза (`vc_nsxt_snat_modifier`)**:
* Входные параметры берутся из секции `modify` YAML `vc_nsxt`: `ipSpaceName`, `needEnableAVI`, `virtualServicesCount`, `routedNetConfiguration`.
Все параметры операции `modify`, не относящиеся к данной задаче, в схему конкретного модификатора **не включаются**.
### Правило 3: Наследование типов и валидаций из YAML
Схема атрибутов модификатора строится по существующей универсальной таблице типов провайдера:
* Обязательность (`required`), значения по умолчанию (`default`), регулярные выражения (`regex`) и диапазоны значений наследуются напрямую из спецификации параметров YAML.
### Правило 4: Экспорт вычисляемых атрибутов (Computed Outputs)
Если модификатор формирует сущность, необходимую последующим шагам, он экспортирует её как `Computed`:
* `vc_org_ip_modifier` экспортирует `ip_space_name`.
* Модификатор шлюза может сослаться на него напрямую:
```hcl
ip_space_name = nubes_vc_org_ip_modifier.ips.ip_space_name
```
---
## 4. Жизненный цикл Modifier-ресурса в провайдере (CRUD)
| Метод Terraform | Вызов API облака | Поведение |
|---|---|---|
| **Create** | `POST /api/v1/svc/{service_id}/{instance_id}/modify` | Отправляет payload с целевыми параметрами модификации. Запускает polling задачи до статуса успешного завершения. Сохраняет ID и параметры в State. |
| **Read** | `GET /api/v1/svc/{service_id}/{instance_id}` | Читает текущее состояние родительского инстанса. Извлекает значения целевых параметров (например, текущие IP или статус SNAT) и сверяет с State. |
| **Update** | `POST /api/v1/svc/{service_id}/{instance_id}/modify` | Вызывается при изменении атрибутов модификатора в `.tf` файле. Отправляет обновлённый payload и ожидает завершения задачи. |
| **Delete** | `POST /api/v1/svc/{service_id}/{instance_id}/modify` | **Откат настройки**: отправляет запрос на деактивацию конкретного функционала (отключение SNAT, обнуление/освобождение пула IP), не удаляя сам родительский инстанс. |
---
## 5. Схема интеграции в конвейер провайдера
Провайдер сохраняет архитектурную чистоту и принцип неизменяемости кода конкретных сервисов (**Immutability Policy**):
```
[ API Облака ]
TOOLS/scripts/01_generate_yamls.sh
[ generated/<stand>/resources_yaml/ ]
(Спецификации стандартных сервисов)
┌───────────────────┴───────────────────┐
▼ ▼
[ Универсальный Генератор ] [ Модуль Модификаторов ]
(Генерирует стандартные (Описывает схему и CRUD
*_resource.go сервисов) для Modifier-ресурсов)
│ │
└───────────────────┬───────────────────┘
[ Точка сборки: provider.go ]
(Регистрация всех ресурсов в
едином списке Resources(ctx))
TOOLS/scripts/03_build_...
[ Единый бинарный провайдер Nubes ]
```
### Шаги интеграции:
1. **Генерация стандартных ресурсов**: Универсальный генератор штатно обрабатывает YAML-спецификации сервисов, создавая основные ресурсы инстансов.
2. **Добавление кода модификаторов**:
* Файлы модификаторов реализуют интерфейс `resource.Resource` (Terraform Plugin Framework) и размещаются в кодовой базе провайдера.
* Они используют общее ядро клиента (`provider/core/`) для отправки запросов и трекинга асинхронных операций.
3. **Регистрация в провайдере**:
* В функции `Resources(ctx)` провайдера фабричные методы модификаторов (например, `NewVcOrgIpModifierResource`, `NewVcNnxtSnatModifierResource`) добавляются в общий срез доступных ресурсов наряду со стандартными ресурсами сервисов.
4. **Сборка**:
* Провайдер компилируется в один исполняемый файл. Для пользователя Terraform новые ресурсы доступны нативно: `nubes_vc_org_ip_modifier`, `nubes_vc_nsxt_snat_modifier`.
---
## 6. Пример сквозного использования в HCL
Итоговый пользовательский сценарий развертывания выглядит чисто, декларативно и прозрачно:
```hcl
# 1. Создание Организации
resource "nubes_vc_org" "org" {
organization_type = "iaas"
resource_realm = "sandbox.nubes.ru"
}
# 2. Создание VDC
resource "nubes_vc_vdc" "vdc" {
organization_uid = nubes_vc_org.org.id
network_provider = "default"
provider_vdc = "fast-2.8"
cpu_allocated = 80
mem_allocated = 200
storage_config = [
{
name = "fast"
size = 2000
}
]
}
# 3. Создание базового Edge NSX-T (включение AVI и 4 SE)
resource "nubes_vc_nsxt" "edge" {
vdc_type = "vdc"
vdc_uid = nubes_vc_vdc.vdc.id
need_enable_avi = true
virtual_services_count = 4
routed_net_configuration = {
ip_addr_pool = "10.10.102.0/24"
main_dns = "8.8.8.8"
second_dns = "8.8.4.4"
}
}
# 4. Модификатор Org: выделение 3 IP (выполняется после Edge)
resource "nubes_vc_org_ip_modifier" "org_ips" {
org_id = nubes_vc_org.org.id
vip_configure = [
{
name = "shturval-ip-space"
count = 3
}
]
# Явная зависимость гарантирует готовность Edge
depends_on = [nubes_vc_nsxt.edge]
}
# 5. Модификатор Edge: включение SNAT с ipSpace из шага 4
resource "nubes_vc_nsxt_snat_modifier" "edge_snat" {
nsxt_id = nubes_vc_nsxt.edge.id
ip_space_name = nubes_vc_org_ip_modifier.org_ips.vip_configure[0].name
need_enable_avi = true
virtual_services_count = 4
routed_net_configuration = {
ip_addr_pool = "10.10.102.0/24"
main_dns = "8.8.8.8"
second_dns = "8.8.4.4"
}
}
# 6. Развертывание кластера Штурвал
resource "nubes_k8s_sthutrval_cluster" "cluster" {
startup_configuration = {
vdc_uid = nubes_vc_vdc.vdc.id
nsxt_uid = nubes_vc_nsxt.edge.id
cluster_name = "k8s-prod-cluster"
}
control_plane_configuration = {
count = 1
sizing_policy = "standard-cp"
sizing_disk = 50
}
worker_configuration = [
{
count = 2
sizing_policy = "standard-worker"
sizing_disk = 50
label_deck = true
}
]
# Требует полной готовности сетевой связки и свободных IP
depends_on = [
nubes_vc_nsxt_snat_modifier.edge_snat,
nubes_vc_org_ip_modifier.org_ips
]
}
```
@@ -0,0 +1,86 @@
# Спецификация цепочки развертывания: vcOrg -> vcVdc -> vcNsxt -> k8sSthutrvalCluster (DEV Stand)
Документ описывает точные параметры и операции сервисов DEV-стенда из `generated/dev/resources_yaml/`, необходимые для оркестрации цепочки развертывания кластера Штурвал (`k8s_sthutrval_cluster`, ID 150).
---
## 1. Сводная таблица шагов
| Шаг | Действие | Сервис (ID) | Операция | Ключевые параметры |
|---|---|---|---|---|
| 1 | `vcOrg/create` | `vc_org` (19) | `create` (136) | `resourceRealm`, `organizationType = "iaas"`, `orgSuffix` |
| 2 | `vcVdc/create` | `vc_vdc` (21) | `create` (9) | `organizationUid` (ссылка на Org), `providerVdc`, `networkProvider`, `storageConfig`, `cpuAllocated`, `memAllocated` |
| 3 | `vcNsxt/create` | `vc_nsxt` (22) | `create` (10) | `vdcType = "vdc"`, `vdcUid` (ссылка на VDC), `needEnableAVI = true`, `virtualServicesCount = 4`, `routedNetConfiguration` |
| 4 | `vcOrg/modify` | `vc_org` (19) | `modify` (207) | `vIPConfigure`: `name` (ipSpace), `count = 3` |
| 5 | `vcNsxt/modify` | `vc_nsxt` (22) | `modify` (111) | `ipSpaceName` (имя из шага 4), `needEnableAVI = true`, `virtualServicesCount = 4`, `routedNetConfiguration` |
| 6 | `k8sSthutrvalCluster/create` | `k8s_sthutrval_cluster` (150) | `create` (108) | `startupConfiguration`: `vdcUid`, `nsxtUid`, `clusterName`; `controlPlaneConfiguration`; `workerConfiguration` |
---
## 2. Детальная спецификация параметров из YAML DEV
### Шаг 1: `vc_org` (ID 19) — `create` (id: 136)
*Источник: `generated/dev/resources_yaml/19_vc_org.yaml`*
* `resourceRealm` (`string`, required, default: `sandbox.nubes.ru`) — целевое облако.
* `organizationType` (`string`, required, default: `iaas`, values: `iaas`, `saas`) — тип тенанта (`iaas` для доступа в Keycloak).
* `orgSuffix` (`string`, optional, regex: `^[0-9a-z]+$`, 3–10 символов) — суффикс организации.
### Шаг 2: `vc_vdc` (ID 21) — `create` (id: 9)
*Источник: `generated/dev/resources_yaml/21_vc_vdc.yaml`*
* `organizationUid` (`uuid`, required, ref: 19) — UUID созданной организации `vc_org`.
* `networkProvider` (`string`, required) — сетевой провайдер платформы.
* `providerVdc` (`string`, required) — пул ресурсов Cloud Director.
* `storageConfig` (`array-map-fixed`, required):
* `name` (`string`, required) — имя storage-политики.
* `size` (`integer > 0`, required, default: `2000`) — размер хранилища в ГБ.
* `cpuGuaranteed` (`integer >= 0`, required, values: `0`, `50`, `80`, default: `0`).
* `cpuAllocated` (`integer > 0`, required, default: `80`).
* `memAllocated` (`integer > 0`, required, default: `200`).
### Шаг 3: `vc_nsxt` (ID 22) — `create` (id: 10)
*Источник: `generated/dev/resources_yaml/22_vc_nsxt.yaml`*
* `vdcType` (`string`, required, default: `vdc`, values: `vdc`, `vdcGroup`).
* `vdcUid` (`string`, required при `vdcType == "vdc"`, ref: 21) — UUID инстанса `vc_vdc`.
* `needEnableAVI` (`boolean`, required, default: `false`) — **значение: `true`** (активация AVI Load Balancer).
* `virtualServicesCount` (`integer > 0`, 1..4, default: `1`) — **значение: `4`** (Service Engine / резерв VS).
* `routedNetConfiguration` (`map-fixed`, required):
* `ipAddrPool` (`string`, default: `10.10.102.0/24`) — CIDR routed-сети.
* `mainDns` (`string`, default: `8.8.8.8`).
* `secondDns` (`string`, default: `8.8.4.4`).
### Шаг 4: `vc_org` (ID 19) — `modify` (id: 207)
*Источник: `generated/dev/resources_yaml/19_vc_org.yaml`*
* `vIPConfigure` (`array-map-fixed`, required) — добавление внешних IP:
* `name` (`string`, required) — имя пула / ipSpace.
* `count` (`integer > 0`, required) — **значение: `3`**.
* *Условие API*: выполняется строго после создания VDC и Edge Gateway.
### Шаг 5: `vc_nsxt` (ID 22) — `modify` (id: 111)
*Источник: `generated/dev/resources_yaml/22_vc_nsxt.yaml`*
* `ipSpaceName` (`string`, optional) — **имя ipSpace**, заданное на шаге 4 (`vIPConfigure[].name`). Включает SNAT.
* `needEnableAVI` (`boolean`, optional) — `true`.
* `virtualServicesCount` (`integer > 0`, 1..4, optional) — `4`.
* `routedNetConfiguration` (`map-fixed`, required):
* `ipAddrPool`, `mainDns`, `secondDns`.
* *Условие API*: создание правила SNAT требует наличия свободных IP в организации.
### Шаг 6: `k8s_sthutrval_cluster` (ID 150) — `create` (id: 108)
*Источник: `generated/dev/resources_yaml/150_k8s_sthutrval_cluster.yaml`*
* `startupConfiguration` (`map-fixed`, required):
* `vdcUid` (`string`, required) — UUID инстанса `vc_vdc`.
* `nsxtUid` (`string`, required) — UUID инстанса `vc_nsxt` (после настройки SNAT).
* `clusterName` (`string`, required, regex: `(?=^.{1,63}$)^[a-z0-9]([a-z0-9-]*[a-z0-9])?$`).
* Флаги расширений (`boolean`, defaults: `true`): `exIngress`, `exLogging`, `exMonitoring`, `exVip`, `exNamedCsi`, `exLocalCsi`, `exUpdate`.
* `controlPlaneConfiguration` (`map-fixed`, required):
* `count` (`integer > 0`, values: `1`, `3`, `5`, default: `1`).
* `sizingPolicy` (`string`, required).
* `sizingDisk` (`integer > 0`, required, default: `50`).
* `workerConfiguration` (`array-map-fixed`, required):
* `count` (`integer > 0`, required, default: `2`).
* `sizingPolicy` (`string`, required).
* `sizingDisk` (`integer > 0`, required, default: `50`).
* `labelDeck` (`boolean`, required, default: `true`).
* `autoscale` (`boolean`, optional, default: `false`).
* `autoscaleMin` (`integer > 0`, optional, default: `2`).
* `autoscaleMax` (`integer > 0`, optional, default: `3`).
* *Условие API*: перед разворачиванием кластера в пуле должно быть не менее 2 свободных невыделенных IP.
@@ -0,0 +1,105 @@
# Архитектурный паттерн Terraform: Ресурсы привязок и модификаций (Resource Association Pattern)
## 1. Канонический стандарт Terraform
Разделение базовой сущности и её отложенных настроек/модификаций на самостоятельные ресурсы в Terraform является индустриальным стандартом (**Resource Association / Separate Resource Pattern**), рекомендованным HashiCorp и повсеместно используемым в провайдерах первого эшелона (AWS, Google Cloud, Azure, OpenStack).
### Примеры из мировой практики:
* **AWS Security Groups**:
* Базовый ресурс: `aws_security_group` (создание пустой группы).
* Ресурс настройки: `aws_security_group_rule` (отдельное правило ingress/egress).
* *Причина*: разрыв взаимных и циклических зависимостей, когда правила одной группы ссылаются на другую.
* **AWS VPC & Routing**:
* Базовые ресурсы: `aws_vpc`, `aws_route_table`, `aws_subnet`.
* Ресурсы привязок: `aws_route_table_association`, `aws_vpn_gateway_attachment`.
* **IAM (GCP / AWS)**:
* Базовые сущности: `aws_iam_user`, `aws_iam_role`.
* Ресурсы привязок прав: `aws_iam_user_policy_attachment`, `google_project_iam_binding`.
---
## 2. Почему идеология Terraform требует именно отдельных ресурсов
### 1. Управление графом зависимостей (DAG — Directed Acyclic Graph)
Terraform строит граф вычислений и определяет строгий порядок выполнения исключительно на уровне **декларативных блоков `resource`**.
* Если операция (например, добавление внешних IP в `vcOrg` или активация SNAT в `vcNsxt`) «спрятана» внутри одного монолитного ресурса, движок Terraform не может вклинить между этапами создание промежуточных объектов (`vcVdc`, базовый `vcNsxt`).
* Выделение модификации в отдельный ресурс даёт Terraform возможность явно связать зависимости:
```
vcOrg (создание)
└── vcVdc (создание)
└── vcNsxt (базовое создание)
└── vcOrg_ip_allocation (модификация Org, зависит от nsxt)
└── vcNsxt_snat (модификация Edge, зависит от ip_allocation)
└── k8s_sthutrval_cluster (зависит от snat)
```
### 2. Симметричный и безопасный `terraform destroy`
В монолитном подходе удаление инфраструктуры часто приводит к сбоям: родительский ресурс пытается удалиться раньше дочерних привязок.
В паттерне отдельных ресурсов Terraform автоматически обращает граф вспять:
1. Удаляется кластер `k8s_sthutrval_cluster`.
2. Ресурс `vcNsxt_snat` отключает SNAT на шлюзе.
3. Ресурс `vcOrg_ip_allocation` освобождает выделенные IP-адреса.
4. Удаляются базовые `vcNsxt`, `vcVdc` и `vcOrg`.
### 3. Предсказуемость плана и изоляция сбоев
* Любые изменения видны пользователю в `terraform plan` как точечные действия над конкретными ресурсами.
* Если падает сетевая модификация, ошибка локализуется в конкретном блоке ресурса привязки, а инфраструктура в Terraform State не переходит в поврежденное («зависшее») состояние.
---
## 3. Точки изменений в пайплайне генерации провайдера Nubes
Архитектура провайдера строго следует **Immutability Policy**: код конкретных ресурсов генерируется автоматически из универсальных шаблонов.
Изменения для поддержки данного паттерна вносятся строго в универсальные слои генератора:
```
┌────────────────────────────────────────────────────────────────────────┐
│ 1. TOOLS/yaml-generator/ │
│ Выделение операций modify/настроек в схеме YAML: │
│ kind: subresource / kind: association_resource │
└───────────────────────────────────┬────────────────────────────────────┘
│ (генерация YAML)
┌────────────────────────────────────────────────────────────────────────┐
│ 2. generated/<stand>/resources_yaml/*.yaml │
│ Декларативное описание схемы привязок и их параметров │
└───────────────────────────────────┬────────────────────────────────────┘
│ (вход для генератора кода)
┌────────────────────────────────────────────────────────────────────────┐
│ 3. TOOLS/resource-generator/ │
│ - templates/: универсальные шаблоны для association-ресурсов │
│ - Генерация Create (вызов modify), Read (GET инстанса), │
│ Delete (откат настройки) │
│ - Автоматическая регистрация новых ресурсов в provider.go │
└───────────────────────────────────┬────────────────────────────────────┘
│ (компиляция)
┌────────────────────────────────────────────────────────────────────────┐
│ 4. provider/core/ │
│ Универсальный CRUD-слой для ожидания тасок модификации (polling) │
└────────────────────────────────────────────────────────────────────────┘
```
### 1. `TOOLS/yaml-generator/`
* Модификации, содержащие отложенные сетевые/квотные параметры (`vIPConfigure`, `ipSpaceName/snat`), размечаются как отдельные дочерние сущности (ассоциации) родительского сервиса.
* Формируются контракты параметров: ссылка на родителя (`instance_id`), изменяемые параметры, возвращаемые идентификаторы.
### 2. `TOOLS/resource-generator/`
* Добавляется универсальный кодогенератор ресурсов-модификаторов (association/attachment resources).
* Логика CRUD:
* **Create**: отправка запроса `POST /api/v1/svc/{service_id}/{instance_id}/modify`.
* **Read**: запрос текущего состояния родителя `GET /api/v1/svc/{service_id}/{instance_id}` и извлечение привязанных настроек.
* **Update**: повторный `modify` при изменении полей.
* **Delete**: запрос `modify` с возвратом к дефолтному состоянию (отключение SNAT / освобождение пула IP).
* Ресурсы регистрируются в едином перечне провайдера.
### 3. `provider/core/`
* Универсальное ядро уже содержит абстракции работы с API и polling-задач. Проверяется корректность обработки асинхронных операций `modify` до их полного перехода в статус готовности.
### Скрипты конвейера остаются неизменными:
* `01_generate_yamls.sh`
* `02_generate_resources_and_docs_v2.sh`
* `03_build_and_upload_provider.sh`
Порядок сборки и публикации не меняется.
+19
View File
@@ -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_<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)
+89
View File
@@ -0,0 +1,89 @@
# Резюме сессии: Диагностика K8s (Штурвал), VDC и архитектура провайдера (2026-09-20)
## 1. Инфраструктурный контекст
- **ВМ 213 (jump-host / dev)**:
- SSH: `ssh vps` (`5.172.178.213`, user `naeel`, key `~/.ssh/naeel_vm_id_ed25519`).
- kubectl контекст по умолчанию: `tazetdinovn@gmail.com@naeel-test-3`.
- **Кластер `naeel-test-3`**:
- API: `https://185.247.187.146:6443`.
- Узлы: 6 нод (3 control-plane, 3 workers), версия v1.34.1 / платформа Штурвал 2.12.1.
- **Кластер `devclustername`**:
- API: `https://185.247.187.226:6443`.
- Статус: порт 6443 на Edge доступен, но TLS сбрасывается (`connection reset by peer`) — виртуальные машины кластера находятся в `suspend` / выключены.
---
## 2. Что было сделано и починено в кластере `naeel-test-3`
### Проблема:
В веб-интерфейсе Штурвала кластер висел в статусе **«Работает с ошибками» (⚠️ 2/4 по NodeConfigItems)**.
### Причина:
1. На активном воркере `naeel-test-3-workers-5p8w7-vxzch` висело ожидание применения конфигураций (`RebootPending` с типом `drainonly`).
2. Очередь на применение/drain была заблокирована (`SlotsOccupied`), так как в CRD `nodeconfigs.node.shturval.tech` осталась старая удалённая нода `naeel-test-3-workers-5p8w7-nqkr2` с зависшим флагом `rebootallowed: true`.
### Решение:
1. Вычищены все фантомные объекты `nodeconfigs`, которых уже нет среди реальных K8s-нод (сняты блокирующие finalizers).
2. Слот освободился: контроллер `shturval-node-config` корректно выполнил `drainonly` на воркере `vxzch` (под `pythonk8s` переехал на соседний воркер `wwqj2`).
3. Применились оставшиеся `NodeConfigItem` (`generic-init-config`, `all-to-nubes-registry`).
4. **Текущий статус**:
- Все 6 нод в статусе `Ready`.
- Все 6 `nodeconfigs` в статусе `READY: true`.
- Все 4 `nodeconfigitems` в статусе `ready: true` (**4/4, статус кластера зелёный**).
- Под `pythonk8s` (`drhider.pythonk8s.dev.nubes.ru`, ns `20a75175-a58c-49cb-b8fa-e86367b1a8dc`) поднят и работает (1/1 Running).
- Потребление ресурсов: среднее ~22m CPU и ~103 MiB RAM на под; суммарно на весь 6-узловой кластер ~1.6 CPU и ~7.5 GiB RAM (минимальный фоновый простой).
---
## 3. Блокировка операций в личном кабинете облака (Suspend / Modify)
### Симптом:
При попытке выполнить операцию `suspend` кластера в UI облака возникает ошибка:
`Concurrent operations are not supported (job status: cannot obtain job result) There is a started operation on this instance`
### Диагностика через API Gateway (`lk-api-gateway-dev.ngcloud.ru`):
- Инстанс кластера: `e78a40b9-7de1-4c88-af7a-ad7f7efaa7c2`.
- Зависшая операция: `modify` (`opUid: 2fb7dd59-732a-4bb9-add2-6bddd7329f72`).
- Причина зависания: оркестратор CFS поймал таймаут (`Timeout has been exceeded`), выполнил откат, записал лог ошибки, но **не проставил `dtFinish` и `isSuccessful` в БД**.
- Статус операции в API остался незакрытым, из-за чего API Gateway блокирует любые новые операции над инстансом.
- **Внимание**: это баг бэкенда платформы облака (CFS/оркестратора). Средствами `kubectl` внутри кластера это не лечится — требуется сброс статуса операции на стороне API платформы или через поддержку.
- Ошибка в истории операций: `Не удалось включить кластер. Ошибка: Error not found from 'parseTerraformError'` — вызвана тем, что общий обработчик бэкенда облака по ошибке прогнал текст через парсер ошибок Terraform. Внутри K8s Terraform не используется.
---
## 4. Архитектурные правила по VDC и Terraform-провайдеру
### Почему возникает ошибка дубликата VDC:
`VDC с именем 'WZ03709-saas-snb1-i24-vcpu50' уже существует внутри организации 'WZ03709-saas'`
- Имя VDC генерируется бэкендом детерминированно: `{org}-{type}-{segment}-{cpu}-vcpu{reservation}`.
- В одном сегменте организации существует максимум два класса VDC:
- `vcpu50` (50% гарантия vCPU — для dev/test, оверселлинг, дешевле);
- `vcpu80` (80% гарантия vCPU — для prod/баз данных, жесткая фиксация ресурсов).
- Создавать третий VDC с тем же процентом SLA в рамках одной организации невозможно (конфликт уникальности имен в vCloud) и бессмысленно: изоляция проектов внутри VDC делается через **vApp**, **сети (Org Networks)** и правила фаервола. При нехватке ресурсов VDC масштабируется через `modify`.
- **Канон Terraform**: при работе с уже созданными VDC в манифесте указывать:
```hcl
adopt_existing_on_create = true
```
(согласно `docs/60_strategy/provider_philosophy.md`).
### Зачем нужны дочерние ресурсы-действия (Action / Sub-resources):
Цепочка развертывания vCloud/NSX-T имеет циклические зависимости:
1. `vcOrg -> create`
2. `vcVdc -> create`
3. `vcNsxt -> create` (Edge Gateway)
4. `vcOrg -> modify` (выделение белых IP в организацию, так как появился Edge)
5. `vcNsxt -> modify` (настройка SNAT под конкретный выделенный IP)
6. `k8sShturval -> create` (нодам нужен интернет через SNAT)
Чтобы пользователю не приходилось делать несколько ручных прогонов `terraform apply` с ручным редактированием `.tf` файлов между шагами, в провайдере создаются **дочерние ресурсы-действия**.
- Под капотом провайдера эти дочерние ресурсы транслируются в точечные вызовы **`modify`** над родительскими объектами.
- Это стандартная практика в Terraform (аналог `aws_security_group_rule` для `aws_security_group`).
---
## 5. Что делать дальше в новом чате
1. Если продолжаем работу с провайдером Nubes:
- Проверить статус зависшей операции `2fb7dd59-732a-4bb9-add2-6bddd7329f72` в API Gateway.
- Разрабатывать/тестировать логику дочерних ресурсов (action/sub-resources) и `modify`-пайплайна по стандарту `docs/60_strategy/provider_philosophy.md`.
2. Если требуется проверить второй кластер (`devclustername` / `185.247.187.226`):
- Убедиться, что кластер выведен из suspend в веб-интерфейсе (чтобы поднялся API server на порту 6443).
+62 -2
View File
@@ -14,8 +14,68 @@
## 2. Публикация провайдера в Registry
- Артефакты: zip, SHA256SUMS, SHA256SUMS.sig
- Подпись: для .sig использовать бинарную detached подпись
- Хранилище: S3 bucket terraform-registry
- Префикс: docs/<namespace>/<name>/<version>/ для документации, отдельный префикс для бинарников по правилам registry-сервера
- Хранилище бинарников: S3 bucket `nubes-terraform-registry`
- Хранилище документации: S3 bucket `terraform-registry` (public)
- Префикс: `docs/<namespace>/<name>/` для документации, `<host>/<namespace>/<name>/<version>/` для бинарников
### Канонический provider release workflow
Рабочий registry для provider-бинарников использует:
- Registry hostname: `tf-registry.containerk8s.services.ngcloud.ru`;
- S3 endpoint: `https://s3.msk-1.ngcloud.ru`;
- S3 bucket: `nubes-terraform-registry`;
- DEV namespace: `nubes-dev`;
- Provider name: `nubes`.
Для DEV version `2.0.1` полный путь бинарников:
```text
nubes-terraform-registry/tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes/2.0.1/
```
Публикация выполняется из корня репозитория одной командой:
```bash
cd /home/naeel/TF/tf_provider
REQUEST_DELAY=0.05 ATTEMPTS=3 \
./TOOLS/scripts/03_build_and_upload_provider.sh \
--profile TOOLS/config/dev 2.0.1
```
`03_build_and_upload_provider.sh` перед сборкой автоматически запускает `01` и `02`, поэтому отдельно запускать генераторы для обычного release не требуется. В результате создаются:
- `generated/dev/resources_yaml/` — актуальные YAML из DEV API;
- `generated/dev/go/` — generated Go, включая `*_modifier.go` и `registry.go`;
- `generated/dev/docs/` — generated docs;
- `generated/dev/provider_build/` — три ZIP, `SHA256SUMS` и `SHA256SUMS.sig`.
Перед upload скрипт проверяет, что каждый constructor из resource/modifier/action-файлов присутствует в `registry.go`, затем собирает provider для `linux/amd64`, `windows/amd64` и `darwin/amd64`.
### Разделение S3-хранилищ и Credentials
В архитектуре используются **два разных бакета S3**:
1. **Бинарники провайдеров:**
- Бакет: `nubes-terraform-registry`
- Права на заливку: аккаунт `1112_terraform:super` (`secrets/.s3cfg_provider`, алиас `prod-s3`)
- Чтение: реестр читает через встроенный аккаунт `1112_terraform:reader` (`6DDP5...`)
2. **MkDocs-документация:**
- Бакет: `terraform-registry` (публичный)
- Права на заливку: аккаунт `1325` (`secrets/.s3cfg_registry`, алиас `registry`)
Нельзя путать эти бакеты: заливка бинарников в `terraform-registry` делает версию невидимой для реестра, а попытка залить бинарники ключом документации вызывает ошибку прав доступа (`Insufficient permissions`).
### Контроль успешной публикации
Успех подтверждается только после всех трёх условий:
1. В выводе есть `Done. Version <version> uploaded.` и exit code `0`.
2. В `generated/<stand>/provider_build/` присутствуют ZIP, `SHA256SUMS` и `.sig`.
3. Объекты видны в точном S3 prefix через `mc ls prod-s3/nubes-terraform-registry/...`.
4. Запрос `curl https://tf-registry.containerk8s.services.ngcloud.ru/v1/providers/<namespace>/<name>/versions` возвращает новую версию в списке.
Если публикация прервалась во время `01`, повторный запуск безопасен: он только читает service specs из API и перезаписывает generated YAML. Terraform apply, modify и delete для публикации не запускаются.
### Важное про GPG ключи
- Приватный ключ должен быть стабильным между релизами.