diff --git a/universal_rebuild/internal/resources_gen/100_openwhisk_resource.go b/universal_rebuild/internal/resources_gen/100_openwhisk_resource.go index a0c10df..a89d3a4 100644 --- a/universal_rebuild/internal/resources_gen/100_openwhisk_resource.go +++ b/universal_rebuild/internal/resources_gen/100_openwhisk_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -10,6 +11,8 @@ import ( "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" ) @@ -29,7 +32,7 @@ type OpenwhiskModel struct { ID types.String `tfsdk:"id"` ResourceName types.String `tfsdk:"resource_name"` OperationTimeout types.String `tfsdk:"operation_timeout"` - ResourceRealm types.String `tfsdk:"resource_realm"` + 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"` @@ -53,10 +56,10 @@ func (r *OpenwhiskResource) Metadata(ctx context.Context, req resource.MetadataR func (r *OpenwhiskResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, "resource_name": schema.StringAttribute{Required: true}, "operation_timeout": schema.StringAttribute{Optional: true}, - "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Ресурсная платформа Не изменяется после создания."}, + "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)}, @@ -108,26 +111,22 @@ func (r *OpenwhiskResource) ModifyPlan(ctx context.Context, req resource.ModifyP return } } - if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { - if 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() { - if plan.Username.ValueString() != state.Username.ValueString() { + // 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() || config.ResourceRealm.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + if config.Username.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "username is required.") return } - if config.Username.IsNull() || config.Username.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "username is required.") + if config.Username.IsUnknown() { return } @@ -139,11 +138,12 @@ func (r *OpenwhiskResource) ModifyPlan(ctx context.Context, req resource.ModifyP adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() } params := map[int]string{ - 64: resources_core.FormatString(config.Username), - 863: resources_core.FormatString(config.ResourceRealm), + 64: resources_core.FormatString(config.Username), } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} - resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 100, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -152,27 +152,27 @@ func (r *OpenwhiskResource) Create(ctx context.Context, req resource.CreateReque 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() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 100, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ - 64: resources_core.FormatString(data.Username), - 863: resources_core.FormatString(data.ResourceRealm), + 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()) @@ -190,7 +190,6 @@ func (r *OpenwhiskResource) Create(ctx context.Context, req resource.CreateReque {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...) @@ -232,7 +231,6 @@ func (r *OpenwhiskResource) Read(ctx context.Context, req resource.ReadRequest, {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...) @@ -283,6 +281,9 @@ func (r *OpenwhiskResource) Update(ctx context.Context, req resource.UpdateReque if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -299,7 +300,6 @@ func (r *OpenwhiskResource) Update(ctx context.Context, req resource.UpdateReque {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...) @@ -319,16 +319,15 @@ func (r *OpenwhiskResource) Delete(ctx context.Context, req resource.DeleteReque if state.ID.IsNull() || state.ID.IsUnknown() { return } - - deleteMode := "state_only" - if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { - deleteMode = "suspend" - } + 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 diff --git a/universal_rebuild/internal/resources_gen/110_dnszone_resource.go b/universal_rebuild/internal/resources_gen/110_dnszone_resource.go index 2b92e9f..20201dd 100644 --- a/universal_rebuild/internal/resources_gen/110_dnszone_resource.go +++ b/universal_rebuild/internal/resources_gen/110_dnszone_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -10,7 +11,9 @@ import ( "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" ) @@ -30,6 +33,7 @@ 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"` @@ -61,9 +65,10 @@ func (r *DnszoneResource) Metadata(ctx context.Context, req resource.MetadataReq func (r *DnszoneResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "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: "Платформа для развертывания Не изменяется после создания."}, "zone_email": schema.StringAttribute{Required: true, MarkdownDescription: "Контактный e-mail Не изменяется после создания."}, "zone_kind": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("NATIVE"), MarkdownDescription: "Тип зоны Не изменяется после создания."}, @@ -124,73 +129,109 @@ func (r *DnszoneResource) ModifyPlan(ctx context.Context, req resource.ModifyPla } } if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { - if plan.ResourceRealm.ValueString() != state.ResourceRealm.ValueString() { + // 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() { - if plan.ZoneEmail.ValueString() != state.ZoneEmail.ValueString() { + // 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() { - if plan.ZoneKind.ValueString() != state.ZoneKind.ValueString() { + // 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() { - if plan.ZoneMastersForSlave.ValueString() != state.ZoneMastersForSlave.ValueString() { + // 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() { - if plan.ZoneName.ValueString() != state.ZoneName.ValueString() { + // 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() { - if plan.ZoneTimeToExpire.ValueString() != state.ZoneTimeToExpire.ValueString() { + // 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() { - if plan.ZoneTimeToLive.ValueString() != state.ZoneTimeToLive.ValueString() { + // 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() { - if plan.ZoneTimeToRefresh.ValueString() != state.ZoneTimeToRefresh.ValueString() { + // 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() { - if plan.ZoneTimeToRetry.ValueString() != state.ZoneTimeToRetry.ValueString() { + // 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.ResourceRealm.IsNull() || config.ResourceRealm.IsUnknown() { + if config.ResourceRealm.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") return } - if config.ZoneEmail.IsNull() || config.ZoneEmail.IsUnknown() { + if config.ResourceRealm.IsUnknown() { + return + } + if config.ZoneEmail.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "zone_email is required.") return } - if config.ZoneName.IsNull() || config.ZoneName.IsUnknown() { + 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 @@ -210,8 +251,10 @@ func (r *DnszoneResource) ModifyPlan(ctx context.Context, req resource.ModifyPla 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.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 110, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -234,7 +277,9 @@ func (r *DnszoneResource) Create(ctx context.Context, req resource.CreateRequest } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 110, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ 66: resources_core.FormatString(data.ZoneEmail), @@ -252,6 +297,9 @@ func (r *DnszoneResource) Create(ctx context.Context, req resource.CreateRequest 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()) @@ -376,6 +424,9 @@ func (r *DnszoneResource) Update(ctx context.Context, req resource.UpdateRequest if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -419,16 +470,15 @@ func (r *DnszoneResource) Delete(ctx context.Context, req resource.DeleteRequest if state.ID.IsNull() || state.ID.IsUnknown() { return } - - deleteMode := "state_only" - if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { - deleteMode = "suspend" - } + 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 diff --git a/universal_rebuild/internal/resources_gen/111_dnsrecord_reconcile_action.go b/universal_rebuild/internal/resources_gen/111_dnsrecord_reconcile_action.go new file mode 100644 index 0000000..ef6e7ab --- /dev/null +++ b/universal_rebuild/internal/resources_gen/111_dnsrecord_reconcile_action.go @@ -0,0 +1,136 @@ +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/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: dnsrecord +// Action: reconcile + +var _ resource.Resource = &DnsrecordReconcileResource{} + +// DnsrecordReconcileResource triggers a one-time action. +type DnsrecordReconcileResource struct { + client *core.UniversalClient +} + +type DnsrecordReconcileModel struct { + ID types.String `tfsdk:"id"` + DnsrecordID types.String `tfsdk:"dnsrecord_id"` + RunID types.String `tfsdk:"run_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` +} + +func NewDnsrecordReconcileResource() resource.Resource { + return &DnsrecordReconcileResource{} +} + +func (r *DnsrecordReconcileResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dnsrecord_reconcile" +} + +func (r *DnsrecordReconcileResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true}, + "dnsrecord_id": schema.StringAttribute{Required: true}, + "run_id": schema.StringAttribute{Required: true}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "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 *DnsrecordReconcileResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan DnsrecordReconcileModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.DnsrecordID.ValueString()) + runID := strings.TrimSpace(plan.RunID.ValueString()) + if instanceUID == "" || runID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра или run_id") + return + } + + params := 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, "reconcile", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "reconcile", runID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *DnsrecordReconcileResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state DnsrecordReconcileModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *DnsrecordReconcileResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan DnsrecordReconcileModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.DnsrecordID.ValueString()) + runID := strings.TrimSpace(plan.RunID.ValueString()) + if instanceUID == "" || runID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра или run_id") + return + } + + params := map[string]string{} + + if err := resources_core.RunOperationByCode(ctx, r.client, instanceUID, "reconcile", params); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "reconcile", runID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *DnsrecordReconcileResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // No-op: removing the resource does not trigger a remote action. +} + +func (r *DnsrecordReconcileResource) 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/universal_rebuild/internal/resources_gen/111_dnsrecord_resource.go b/universal_rebuild/internal/resources_gen/111_dnsrecord_resource.go index 81d5f8f..03e9fa6 100644 --- a/universal_rebuild/internal/resources_gen/111_dnsrecord_resource.go +++ b/universal_rebuild/internal/resources_gen/111_dnsrecord_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -11,6 +12,8 @@ import ( "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" ) @@ -30,6 +33,7 @@ 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"` @@ -57,14 +61,15 @@ func (r *DnsrecordResource) Metadata(ctx context.Context, req resource.MetadataR func (r *DnsrecordResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "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{Required: true, MarkdownDescription: "UUID экземпляра услуги 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}, @@ -109,22 +114,11 @@ func (r *DnsrecordResource) ModifyPlan(ctx context.Context, req resource.ModifyP if resp.Diagnostics.HasError() { return } - planChanged := false - if !plan.ZoneUid.IsNull() && !plan.ZoneUid.IsUnknown() { - resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 110, plan.ZoneUid.ValueString()) - if err != nil { - resp.Diagnostics.AddWarning("Failed to resolve zone_uid", err.Error()) - } else if resolvedZoneUid != "" && resolvedZoneUid != plan.ZoneUid.ValueString() { - plan.ZoneUid = types.StringValue(resolvedZoneUid) - planChanged = true - } - } - if planChanged { - resp.Diagnostics.Append(resp.Plan.Set(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 задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") @@ -132,7 +126,10 @@ func (r *DnsrecordResource) ModifyPlan(ctx context.Context, req resource.ModifyP } } if !plan.RecordName.IsNull() && !plan.RecordName.IsUnknown() && !state.RecordName.IsNull() && !state.RecordName.IsUnknown() { - if plan.RecordName.ValueString() != state.RecordName.ValueString() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.RecordName.ValueString(), state.RecordName.ValueString()) { resp.Diagnostics.AddError("Нельзя изменить record_name", "Параметр задается при создании и не может быть изменен.") return } @@ -144,35 +141,53 @@ func (r *DnsrecordResource) ModifyPlan(ctx context.Context, req resource.ModifyP } } if !plan.RecordType.IsNull() && !plan.RecordType.IsUnknown() && !state.RecordType.IsNull() && !state.RecordType.IsUnknown() { - if plan.RecordType.ValueString() != state.RecordType.ValueString() { + // 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() { - if plan.ZoneUid.ValueString() != state.ZoneUid.ValueString() { + // 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() || config.RecordInput.IsUnknown() { + if config.RecordInput.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "record_input is required.") return } - if config.RecordName.IsNull() || config.RecordName.IsUnknown() { + if config.RecordInput.IsUnknown() { + return + } + if config.RecordName.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "record_name is required.") return } - if config.RecordType.IsNull() || config.RecordType.IsUnknown() { + if config.RecordName.IsUnknown() { + return + } + if config.RecordType.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "record_type is required.") return } - if config.ZoneUid.IsNull() || config.ZoneUid.IsUnknown() { + 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 @@ -182,7 +197,7 @@ func (r *DnsrecordResource) ModifyPlan(ctx context.Context, req resource.ModifyP adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() } if !config.ZoneUid.IsNull() && !config.ZoneUid.IsUnknown() { - resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 110, config.ZoneUid.ValueString()) + 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() { @@ -196,8 +211,10 @@ func (r *DnsrecordResource) ModifyPlan(ctx context.Context, req resource.ModifyP 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.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 111, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -222,8 +239,26 @@ func (r *DnsrecordResource) Create(ctx context.Context, req resource.CreateReque 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, 110, data.ZoneUid.ValueString()) + resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 109, data.ZoneUid.ValueString()) if err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -234,7 +269,9 @@ func (r *DnsrecordResource) Create(ctx context.Context, req resource.CreateReque } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 111, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ 75: resources_core.FormatString(data.ZoneUid), @@ -248,6 +285,9 @@ func (r *DnsrecordResource) Create(ctx context.Context, req resource.CreateReque 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()) @@ -276,13 +316,23 @@ func (r *DnsrecordResource) Create(ctx context.Context, req resource.CreateReque return } if !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() { - resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 110, state.ZoneUid.ValueString()) + 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)...) } @@ -329,13 +379,23 @@ func (r *DnsrecordResource) Read(ctx context.Context, req resource.ReadRequest, return } if !newState.ZoneUid.IsNull() && !newState.ZoneUid.IsUnknown() { - resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 110, newState.ZoneUid.ValueString()) + 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)...) } @@ -363,7 +423,7 @@ func (r *DnsrecordResource) Update(ctx context.Context, req resource.UpdateReque 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 plan.RecordInput.ValueString() != state.RecordInput.ValueString() { + if !strings.EqualFold(plan.RecordInput.ValueString(), state.RecordInput.ValueString()) { hasServiceParamChanges = true } } @@ -391,6 +451,9 @@ func (r *DnsrecordResource) Update(ctx context.Context, req resource.UpdateReque if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -418,13 +481,22 @@ func (r *DnsrecordResource) Update(ctx context.Context, req resource.UpdateReque return } if !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() { - resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 110, state.ZoneUid.ValueString()) + 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)...) } @@ -438,16 +510,15 @@ func (r *DnsrecordResource) Delete(ctx context.Context, req resource.DeleteReque if state.ID.IsNull() || state.ID.IsUnknown() { return } - - deleteMode := "state_only" - if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { - deleteMode = "suspend" - } + 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 diff --git a/universal_rebuild/internal/resources_gen/112_tenant_resource.go b/universal_rebuild/internal/resources_gen/112_tenant_resource.go index 9ad6532..50b51a9 100644 --- a/universal_rebuild/internal/resources_gen/112_tenant_resource.go +++ b/universal_rebuild/internal/resources_gen/112_tenant_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -10,6 +11,8 @@ import ( "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" ) @@ -29,6 +32,7 @@ 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"` @@ -53,9 +57,10 @@ func (r *TenantResource) Metadata(ctx context.Context, req resource.MetadataRequ func (r *TenantResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "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{Required: true, MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, @@ -109,27 +114,39 @@ func (r *TenantResource) ModifyPlan(ctx context.Context, req resource.ModifyPlan } } if !plan.GrafanaOrgName.IsNull() && !plan.GrafanaOrgName.IsUnknown() && !state.GrafanaOrgName.IsNull() && !state.GrafanaOrgName.IsUnknown() { - if plan.GrafanaOrgName.ValueString() != state.GrafanaOrgName.ValueString() { + // 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() { - if plan.ResourceRealm.ValueString() != state.ResourceRealm.ValueString() { + // 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() || config.GrafanaOrgName.IsUnknown() { + if config.GrafanaOrgName.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "grafana_org_name is required.") return } - if config.ResourceRealm.IsNull() || config.ResourceRealm.IsUnknown() { + if config.GrafanaOrgName.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 @@ -142,8 +159,10 @@ func (r *TenantResource) ModifyPlan(ctx context.Context, req resource.ModifyPlan 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.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 112, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -162,7 +181,9 @@ func (r *TenantResource) Create(ctx context.Context, req resource.CreateRequest, } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 112, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ 239: resources_core.FormatString(data.GrafanaOrgName), @@ -173,6 +194,9 @@ func (r *TenantResource) Create(ctx context.Context, req resource.CreateRequest, 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()) @@ -283,6 +307,9 @@ func (r *TenantResource) Update(ctx context.Context, req resource.UpdateRequest, if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -319,16 +346,15 @@ func (r *TenantResource) Delete(ctx context.Context, req resource.DeleteRequest, if state.ID.IsNull() || state.ID.IsUnknown() { return } - - deleteMode := "state_only" - if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { - deleteMode = "suspend" - } + 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 diff --git a/universal_rebuild/internal/resources_gen/113_vc_complex_resource.go b/universal_rebuild/internal/resources_gen/113_vc_complex_resource.go index fad5cf5..dd143a4 100644 --- a/universal_rebuild/internal/resources_gen/113_vc_complex_resource.go +++ b/universal_rebuild/internal/resources_gen/113_vc_complex_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -11,7 +12,9 @@ import ( "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" ) @@ -31,6 +34,7 @@ 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"` @@ -68,9 +72,10 @@ func (r *VcComplexResource) Metadata(ctx context.Context, req resource.MetadataR func (r *VcComplexResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "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 Не изменяется после создания."}, @@ -80,7 +85,7 @@ func (r *VcComplexResource) Schema(ctx context.Context, req resource.SchemaReque "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{Required: true, 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: "Описывается как массив объектов, где описывается имя политики и её размер Не изменяется после создания."}, + "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: "Не изменяется после создания."}, @@ -137,7 +142,10 @@ func (r *VcComplexResource) ModifyPlan(ctx context.Context, req resource.ModifyP } } if !plan.CpuAllocated.IsNull() && !plan.CpuAllocated.IsUnknown() && !state.CpuAllocated.IsNull() && !state.CpuAllocated.IsUnknown() { - if plan.CpuAllocated.ValueString() != state.CpuAllocated.ValueString() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.CpuAllocated.ValueString(), state.CpuAllocated.ValueString()) { resp.Diagnostics.AddError("Нельзя изменить cpu_allocated", "Параметр задается при создании и не может быть изменен.") return } @@ -149,7 +157,10 @@ func (r *VcComplexResource) ModifyPlan(ctx context.Context, req resource.ModifyP } } if !plan.IpSpaceName.IsNull() && !plan.IpSpaceName.IsUnknown() && !state.IpSpaceName.IsNull() && !state.IpSpaceName.IsUnknown() { - if plan.IpSpaceName.ValueString() != state.IpSpaceName.ValueString() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.IpSpaceName.ValueString(), state.IpSpaceName.ValueString()) { resp.Diagnostics.AddError("Нельзя изменить ip_space_name", "Параметр задается при создании и не может быть изменен.") return } @@ -179,19 +190,28 @@ func (r *VcComplexResource) ModifyPlan(ctx context.Context, req resource.ModifyP } } if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { - if plan.ResourceRealm.ValueString() != state.ResourceRealm.ValueString() { + // 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() { - if plan.SegroupName.ValueString() != state.SegroupName.ValueString() { + // 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() { - if plan.StorageConfig.ValueString() != state.StorageConfig.ValueString() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.StorageConfig.ValueString(), state.StorageConfig.ValueString()) { resp.Diagnostics.AddError("Нельзя изменить storage_config", "Параметр задается при создании и не может быть изменен.") return } @@ -203,39 +223,57 @@ func (r *VcComplexResource) ModifyPlan(ctx context.Context, req resource.ModifyP } } if !plan.VappName.IsNull() && !plan.VappName.IsUnknown() && !state.VappName.IsNull() && !state.VappName.IsUnknown() { - if plan.VappName.ValueString() != state.VappName.ValueString() { + // 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() { - if plan.VdcNetworkPool.ValueString() != state.VdcNetworkPool.ValueString() { + // 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() { - if plan.VdcProviderGateway.ValueString() != state.VdcProviderGateway.ValueString() { + // 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() { - if plan.VirtualServicesCount.ValueString() != state.VirtualServicesCount.ValueString() { + // 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.ResourceRealm.IsNull() || config.ResourceRealm.IsUnknown() { + if config.ResourceRealm.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") return } - if config.VappName.IsNull() || config.VappName.IsUnknown() { + if config.ResourceRealm.IsUnknown() { + 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 @@ -261,8 +299,10 @@ func (r *VcComplexResource) ModifyPlan(ctx context.Context, req resource.ModifyP 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.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 113, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -281,7 +321,9 @@ func (r *VcComplexResource) Create(ctx context.Context, req resource.CreateReque } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 113, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ 332: resources_core.FormatString(data.ResourceRealm), @@ -305,6 +347,9 @@ func (r *VcComplexResource) Create(ctx context.Context, req resource.CreateReque 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()) @@ -441,6 +486,9 @@ func (r *VcComplexResource) Update(ctx context.Context, req resource.UpdateReque if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -490,16 +538,15 @@ func (r *VcComplexResource) Delete(ctx context.Context, req resource.DeleteReque if state.ID.IsNull() || state.ID.IsUnknown() { return } - - deleteMode := "state_only" - if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { - deleteMode = "suspend" - } + 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 diff --git a/universal_rebuild/internal/resources_gen/114_gitea_complex_resource.go b/universal_rebuild/internal/resources_gen/114_gitea_complex_resource.go index c5d4586..d9b7146 100644 --- a/universal_rebuild/internal/resources_gen/114_gitea_complex_resource.go +++ b/universal_rebuild/internal/resources_gen/114_gitea_complex_resource.go @@ -10,6 +10,8 @@ import ( "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" ) @@ -29,6 +31,7 @@ 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"` @@ -51,9 +54,10 @@ func (r *GiteaComplexResource) Metadata(ctx context.Context, req resource.Metada func (r *GiteaComplexResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "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}, @@ -115,8 +119,10 @@ func (r *GiteaComplexResource) ModifyPlan(ctx context.Context, req resource.Modi 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.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 114, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -127,7 +133,9 @@ func (r *GiteaComplexResource) Create(ctx context.Context, req resource.CreateRe } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 114, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{} @@ -135,6 +143,9 @@ func (r *GiteaComplexResource) Create(ctx context.Context, req resource.CreateRe 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()) @@ -239,6 +250,9 @@ func (r *GiteaComplexResource) Update(ctx context.Context, req resource.UpdateRe if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -272,16 +286,15 @@ func (r *GiteaComplexResource) Delete(ctx context.Context, req resource.DeleteRe if state.ID.IsNull() || state.ID.IsUnknown() { return } - - deleteMode := "state_only" - if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { - deleteMode = "suspend" - } + 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 diff --git a/universal_rebuild/internal/resources_gen/115_mariadb_database_resource.go b/universal_rebuild/internal/resources_gen/115_mariadb_database_resource.go new file mode 100644 index 0000000..59dd751 --- /dev/null +++ b/universal_rebuild/internal/resources_gen/115_mariadb_database_resource.go @@ -0,0 +1,347 @@ +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(false)}, + "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-"), 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 known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект подтверждён в state_out, выполняется усыновление") + 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 { + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект отсутствует и будет пропущен: "+targetValue) + return + } + resp.Diagnostics.AddError("Подресурс не найден", "Объект отсутствует: "+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/universal_rebuild/internal/resources_gen/115_mariadb_reconcile_action.go b/universal_rebuild/internal/resources_gen/115_mariadb_reconcile_action.go new file mode 100644 index 0000000..5829d16 --- /dev/null +++ b/universal_rebuild/internal/resources_gen/115_mariadb_reconcile_action.go @@ -0,0 +1,136 @@ +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/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: mariadb +// Action: reconcile + +var _ resource.Resource = &MariadbReconcileResource{} + +// MariadbReconcileResource triggers a one-time action. +type MariadbReconcileResource struct { + client *core.UniversalClient +} + +type MariadbReconcileModel struct { + ID types.String `tfsdk:"id"` + MariadbID types.String `tfsdk:"mariadb_id"` + RunID types.String `tfsdk:"run_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` +} + +func NewMariadbReconcileResource() resource.Resource { + return &MariadbReconcileResource{} +} + +func (r *MariadbReconcileResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_mariadb_reconcile" +} + +func (r *MariadbReconcileResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true}, + "mariadb_id": schema.StringAttribute{Required: true}, + "run_id": schema.StringAttribute{Required: true}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "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 *MariadbReconcileResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan MariadbReconcileModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.MariadbID.ValueString()) + runID := strings.TrimSpace(plan.RunID.ValueString()) + if instanceUID == "" || runID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра или run_id") + return + } + + params := 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, "reconcile", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "reconcile", runID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *MariadbReconcileResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state MariadbReconcileModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *MariadbReconcileResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan MariadbReconcileModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.MariadbID.ValueString()) + runID := strings.TrimSpace(plan.RunID.ValueString()) + if instanceUID == "" || runID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра или run_id") + return + } + + params := map[string]string{} + + if err := resources_core.RunOperationByCode(ctx, r.client, instanceUID, "reconcile", params); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "reconcile", runID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *MariadbReconcileResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // No-op: removing the resource does not trigger a remote action. +} + +func (r *MariadbReconcileResource) 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/universal_rebuild/internal/resources_gen/115_mariadb_resource.go b/universal_rebuild/internal/resources_gen/115_mariadb_resource.go index 31dda26..f082ffc 100644 --- a/universal_rebuild/internal/resources_gen/115_mariadb_resource.go +++ b/universal_rebuild/internal/resources_gen/115_mariadb_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -10,8 +11,8 @@ import ( "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/stringdefault" + "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" ) @@ -28,33 +29,26 @@ type MariadbResource struct { } type MariadbModel struct { - ID types.String `tfsdk:"id"` - ResourceName types.String `tfsdk:"resource_name"` - OperationTimeout types.String `tfsdk:"operation_timeout"` - AppVersion types.String `tfsdk:"app_version"` - AutoScale types.Bool `tfsdk:"auto_scale"` - AutoScalePercentage types.Int64 `tfsdk:"auto_scale_percentage"` - AutoScaleQuotaGb types.Int64 `tfsdk:"auto_scale_quota_gb"` - AutoScaleTechWindow types.Int64 `tfsdk:"auto_scale_tech_window"` - ExtBACKUPSCHEDULE types.String `tfsdk:"ext__b_a_c_k_u_p__s_c_h_e_d_u_l_e"` - 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"` - 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"` + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration types.String `tfsdk:"access_configuration"` + AutoscaleConfiguration types.String `tfsdk:"autoscale_configuration"` + BackupConfiguration types.String `tfsdk:"backup_configuration"` + ClusterConfiguration types.String `tfsdk:"cluster_configuration"` + MariadbConfiguration types.String `tfsdk:"mariadb_configuration"` + StartupConfiguration types.String `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 { @@ -67,33 +61,26 @@ func (r *MariadbResource) Metadata(ctx context.Context, req resource.MetadataReq func (r *MariadbResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, - "resource_name": schema.StringAttribute{Required: true}, - "operation_timeout": schema.StringAttribute{Optional: true}, - "app_version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("9.4.0"), MarkdownDescription: "Версия MySQL Не изменяется после создания."}, - "auto_scale": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает автоскейлинг PV. При включении параметра необходимо настроить autoScalePercentage и autoScaleTechWindow. Алерт при срабатывании вызывает modify у текущей услуги"}, - "auto_scale_percentage": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Требует включения autoScale. Позволяет указать в процентах расширение от текущего макс объема"}, - "auto_scale_quota_gb": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Квота для autoScale"}, - "auto_scale_tech_window": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "Требует включения autoScale. Окно обновления (пока не реализовано). Указывается час в виде Integer (0 / 5 / 12 / 23)"}, - "ext__b_a_c_k_u_p__s_c_h_e_d_u_l_e": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("0 * * * *"), MarkdownDescription: "Не обязательный. Время запуска бекапа"}, - "ip_space_name_master": schema.StringAttribute{Optional: true, MarkdownDescription: "Имя ipSpace для публикации VIP master. Не может быть пустым, если включен needExternalAddressMaster"}, - "need_external_address_master": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Выделяет внешний (белый) IP для доступа к master"}, - "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), 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(1024), MarkdownDescription: "Квота памяти пода (MBytes)"}, - "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, - "s3_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Экземпляр (учетная запись) S3 для резервного копирования. Резервное копирование обязательно Не изменяется после создания."}, - "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}, + "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.StringAttribute{Required: true}, + "autoscale_configuration": schema.StringAttribute{Required: true}, + "backup_configuration": schema.StringAttribute{Required: true}, + "cluster_configuration": schema.StringAttribute{Required: true}, + "mariadb_configuration": schema.StringAttribute{Required: true}, + "startup_configuration": 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} @@ -128,54 +115,63 @@ func (r *MariadbResource) ModifyPlan(ctx context.Context, req resource.ModifyPla if resp.Diagnostics.HasError() { return } - planChanged := false - if !plan.S3Uid.IsNull() && !plan.S3Uid.IsUnknown() { - resolvedS3Uid, err := r.client.ResolveRefSvcParamValue(ctx, 12, plan.S3Uid.ValueString()) - if err != nil { - resp.Diagnostics.AddWarning("Failed to resolve s3_uid", err.Error()) - } else if resolvedS3Uid != "" && resolvedS3Uid != plan.S3Uid.ValueString() { - plan.S3Uid = types.StringValue(resolvedS3Uid) - planChanged = true - } - } - if planChanged { - resp.Diagnostics.Append(resp.Plan.Set(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() { - if plan.AppVersion.ValueString() != state.AppVersion.ValueString() { - resp.Diagnostics.AddError("Нельзя изменить app_version", "Параметр задается при создании и не может быть изменен.") - return - } - } - if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { - if 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() { - if plan.S3Uid.ValueString() != state.S3Uid.ValueString() { - resp.Diagnostics.AddError("Нельзя изменить s3_uid", "Параметр задается при создании и не может быть изменен.") + if !plan.StartupConfiguration.IsNull() && !plan.StartupConfiguration.IsUnknown() && !state.StartupConfiguration.IsNull() && !state.StartupConfiguration.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.StartupConfiguration.ValueString(), state.StartupConfiguration.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить startup_configuration", "Параметр задается при создании и не может быть изменен.") return } } return } - if config.ResourceRealm.IsNull() || config.ResourceRealm.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + if config.AccessConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "access_configuration is required.") return } - if config.S3Uid.IsNull() || config.S3Uid.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "s3_uid is required.") + if config.AccessConfiguration.IsUnknown() { + return + } + if config.AutoscaleConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "autoscale_configuration is required.") + return + } + if config.AutoscaleConfiguration.IsUnknown() { + return + } + if config.BackupConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "backup_configuration is required.") + return + } + if config.BackupConfiguration.IsUnknown() { + return + } + if config.ClusterConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "cluster_configuration is required.") + return + } + if config.ClusterConfiguration.IsUnknown() { + return + } + if config.MariadbConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "mariadb_configuration is required.") + return + } + if config.MariadbConfiguration.IsUnknown() { + return + } + if config.StartupConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "startup_configuration is required.") + return + } + if config.StartupConfiguration.IsUnknown() { return } @@ -186,32 +182,18 @@ func (r *MariadbResource) ModifyPlan(ctx context.Context, req resource.ModifyPla 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{ - 422: resources_core.FormatString(config.ResourceRealm), - 424: resources_core.FormatInt64(config.ResourceCPU), - 425: resources_core.FormatInt64(config.ResourceMemory), - 426: resources_core.FormatInt64(config.ResourceDisk), - 427: resources_core.FormatInt64(config.ResourceInstances), - 428: resources_core.FormatBool(config.NeedExternalAddressMaster), - 429: resources_core.FormatString(config.IpSpaceNameMaster), - 430: resources_core.FormatString(config.ExtBACKUPSCHEDULE), - 431: resources_core.FormatString(config.AppVersion), - 432: resources_core.FormatBool(config.AutoScale), - 433: resources_core.FormatInt64(config.AutoScalePercentage), - 434: resources_core.FormatInt64(config.AutoScaleTechWindow), - 435: resources_core.FormatInt64(config.AutoScaleQuotaGb), - 459: resources_core.FormatString(config.S3Uid), + 877: resources_core.FormatString(config.StartupConfiguration), + 878: resources_core.FormatString(config.ClusterConfiguration), + 879: resources_core.FormatString(config.AccessConfiguration), + 880: resources_core.FormatString(config.MariadbConfiguration), + 881: resources_core.FormatString(config.BackupConfiguration), + 882: resources_core.FormatString(config.AutoscaleConfiguration), } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} - resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 115, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -220,39 +202,52 @@ func (r *MariadbResource) Create(ctx context.Context, req resource.CreateRequest if resp.Diagnostics.HasError() { return } - if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + if data.AccessConfiguration.IsNull() || data.AccessConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "access_configuration is required.") return } - if data.S3Uid.IsNull() || data.S3Uid.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "s3_uid is required.") + if data.AutoscaleConfiguration.IsNull() || data.AutoscaleConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "autoscale_configuration is required.") + return + } + if data.BackupConfiguration.IsNull() || data.BackupConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "backup_configuration is required.") + return + } + if data.ClusterConfiguration.IsNull() || data.ClusterConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "cluster_configuration is required.") + return + } + if data.MariadbConfiguration.IsNull() || data.MariadbConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "mariadb_configuration is required.") + return + } + if data.StartupConfiguration.IsNull() || data.StartupConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "startup_configuration is required.") return } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 115, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ - 422: resources_core.FormatString(data.ResourceRealm), - 424: resources_core.FormatInt64(data.ResourceCPU), - 425: resources_core.FormatInt64(data.ResourceMemory), - 426: resources_core.FormatInt64(data.ResourceDisk), - 427: resources_core.FormatInt64(data.ResourceInstances), - 428: resources_core.FormatBool(data.NeedExternalAddressMaster), - 429: resources_core.FormatString(data.IpSpaceNameMaster), - 430: resources_core.FormatString(data.ExtBACKUPSCHEDULE), - 431: resources_core.FormatString(data.AppVersion), - 432: resources_core.FormatBool(data.AutoScale), - 433: resources_core.FormatInt64(data.AutoScalePercentage), - 434: resources_core.FormatInt64(data.AutoScaleTechWindow), - 435: resources_core.FormatInt64(data.AutoScaleQuotaGb), - 459: resources_core.FormatString(data.S3Uid), + 877: resources_core.FormatString(data.StartupConfiguration), + 878: resources_core.FormatString(data.ClusterConfiguration), + 879: resources_core.FormatString(data.AccessConfiguration), + 880: resources_core.FormatString(data.MariadbConfiguration), + 881: resources_core.FormatString(data.BackupConfiguration), + 882: resources_core.FormatString(data.AutoscaleConfiguration), } 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()) @@ -270,19 +265,12 @@ func (r *MariadbResource) Create(ctx context.Context, req resource.CreateRequest {Code: "vault_user_path"}, {Code: "vault_fields"}, }, []resources_core.InputField{ - {Code: "appVersion", Field: "AppVersion", Type: "string"}, - {Code: "autoScale", Field: "AutoScale", Type: "bool"}, - {Code: "autoScalePercentage", Field: "AutoScalePercentage", Type: "int64"}, - {Code: "autoScaleQuotaGb", Field: "AutoScaleQuotaGb", Type: "int64"}, - {Code: "autoScaleTechWindow", Field: "AutoScaleTechWindow", Type: "int64"}, - {Code: "ext_BACKUP_SCHEDULE", Field: "ExtBACKUPSCHEDULE", Type: "string"}, - {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"}, + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "string"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "string"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "string"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "string"}, + {Code: "mariadbConfiguration", Field: "MariadbConfiguration", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "string"}, }) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -323,19 +311,12 @@ func (r *MariadbResource) Read(ctx context.Context, req resource.ReadRequest, re {Code: "vault_user_path"}, {Code: "vault_fields"}, }, []resources_core.InputField{ - {Code: "appVersion", Field: "AppVersion", Type: "string"}, - {Code: "autoScale", Field: "AutoScale", Type: "bool"}, - {Code: "autoScalePercentage", Field: "AutoScalePercentage", Type: "int64"}, - {Code: "autoScaleQuotaGb", Field: "AutoScaleQuotaGb", Type: "int64"}, - {Code: "autoScaleTechWindow", Field: "AutoScaleTechWindow", Type: "int64"}, - {Code: "ext_BACKUP_SCHEDULE", Field: "ExtBACKUPSCHEDULE", Type: "string"}, - {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"}, + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "string"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "string"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "string"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "string"}, + {Code: "mariadbConfiguration", Field: "MariadbConfiguration", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "string"}, }) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -365,100 +346,46 @@ func (r *MariadbResource) Update(ctx context.Context, req resource.UpdateRequest hasServiceParamChanges := false if !hasServiceParamChanges { - if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() { + if plan.ClusterConfiguration.IsNull() != state.ClusterConfiguration.IsNull() || plan.ClusterConfiguration.IsUnknown() != state.ClusterConfiguration.IsUnknown() { hasServiceParamChanges = true - } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { - if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + } else if !plan.ClusterConfiguration.IsNull() && !plan.ClusterConfiguration.IsUnknown() && !state.ClusterConfiguration.IsNull() && !state.ClusterConfiguration.IsUnknown() { + if !strings.EqualFold(plan.ClusterConfiguration.ValueString(), state.ClusterConfiguration.ValueString()) { hasServiceParamChanges = true } } } if !hasServiceParamChanges { - if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() { + if plan.AccessConfiguration.IsNull() != state.AccessConfiguration.IsNull() || plan.AccessConfiguration.IsUnknown() != state.AccessConfiguration.IsUnknown() { hasServiceParamChanges = true - } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() { - if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() { + } else if !plan.AccessConfiguration.IsNull() && !plan.AccessConfiguration.IsUnknown() && !state.AccessConfiguration.IsNull() && !state.AccessConfiguration.IsUnknown() { + if !strings.EqualFold(plan.AccessConfiguration.ValueString(), state.AccessConfiguration.ValueString()) { hasServiceParamChanges = true } } } if !hasServiceParamChanges { - if plan.ResourceInstances.IsNull() != state.ResourceInstances.IsNull() || plan.ResourceInstances.IsUnknown() != state.ResourceInstances.IsUnknown() { + if plan.MariadbConfiguration.IsNull() != state.MariadbConfiguration.IsNull() || plan.MariadbConfiguration.IsUnknown() != state.MariadbConfiguration.IsUnknown() { hasServiceParamChanges = true - } else if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() { - if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() { + } else if !plan.MariadbConfiguration.IsNull() && !plan.MariadbConfiguration.IsUnknown() && !state.MariadbConfiguration.IsNull() && !state.MariadbConfiguration.IsUnknown() { + if !strings.EqualFold(plan.MariadbConfiguration.ValueString(), state.MariadbConfiguration.ValueString()) { hasServiceParamChanges = true } } } if !hasServiceParamChanges { - if plan.ResourceDisk.IsNull() != state.ResourceDisk.IsNull() || plan.ResourceDisk.IsUnknown() != state.ResourceDisk.IsUnknown() { + if plan.BackupConfiguration.IsNull() != state.BackupConfiguration.IsNull() || plan.BackupConfiguration.IsUnknown() != state.BackupConfiguration.IsUnknown() { hasServiceParamChanges = true - } else if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() { - if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() { + } else if !plan.BackupConfiguration.IsNull() && !plan.BackupConfiguration.IsUnknown() && !state.BackupConfiguration.IsNull() && !state.BackupConfiguration.IsUnknown() { + if !strings.EqualFold(plan.BackupConfiguration.ValueString(), state.BackupConfiguration.ValueString()) { hasServiceParamChanges = true } } } if !hasServiceParamChanges { - if plan.NeedExternalAddressMaster.IsNull() != state.NeedExternalAddressMaster.IsNull() || plan.NeedExternalAddressMaster.IsUnknown() != state.NeedExternalAddressMaster.IsUnknown() { + if plan.AutoscaleConfiguration.IsNull() != state.AutoscaleConfiguration.IsNull() || plan.AutoscaleConfiguration.IsUnknown() != state.AutoscaleConfiguration.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 plan.IpSpaceNameMaster.ValueString() != state.IpSpaceNameMaster.ValueString() { - hasServiceParamChanges = true - } - } - } - if !hasServiceParamChanges { - if plan.ExtBACKUPSCHEDULE.IsNull() != state.ExtBACKUPSCHEDULE.IsNull() || plan.ExtBACKUPSCHEDULE.IsUnknown() != state.ExtBACKUPSCHEDULE.IsUnknown() { - hasServiceParamChanges = true - } else if !plan.ExtBACKUPSCHEDULE.IsNull() && !plan.ExtBACKUPSCHEDULE.IsUnknown() && !state.ExtBACKUPSCHEDULE.IsNull() && !state.ExtBACKUPSCHEDULE.IsUnknown() { - if plan.ExtBACKUPSCHEDULE.ValueString() != state.ExtBACKUPSCHEDULE.ValueString() { - hasServiceParamChanges = true - } - } - } - if !hasServiceParamChanges { - if plan.AutoScale.IsNull() != state.AutoScale.IsNull() || plan.AutoScale.IsUnknown() != state.AutoScale.IsUnknown() { - hasServiceParamChanges = true - } else if !plan.AutoScale.IsNull() && !plan.AutoScale.IsUnknown() && !state.AutoScale.IsNull() && !state.AutoScale.IsUnknown() { - if plan.AutoScale.ValueBool() != state.AutoScale.ValueBool() { - hasServiceParamChanges = true - } - } - } - if !hasServiceParamChanges { - if plan.AutoScalePercentage.IsNull() != state.AutoScalePercentage.IsNull() || plan.AutoScalePercentage.IsUnknown() != state.AutoScalePercentage.IsUnknown() { - hasServiceParamChanges = true - } else if !plan.AutoScalePercentage.IsNull() && !plan.AutoScalePercentage.IsUnknown() && !state.AutoScalePercentage.IsNull() && !state.AutoScalePercentage.IsUnknown() { - if plan.AutoScalePercentage.ValueInt64() != state.AutoScalePercentage.ValueInt64() { - hasServiceParamChanges = true - } - } - } - if !hasServiceParamChanges { - if plan.AutoScaleTechWindow.IsNull() != state.AutoScaleTechWindow.IsNull() || plan.AutoScaleTechWindow.IsUnknown() != state.AutoScaleTechWindow.IsUnknown() { - hasServiceParamChanges = true - } else if !plan.AutoScaleTechWindow.IsNull() && !plan.AutoScaleTechWindow.IsUnknown() && !state.AutoScaleTechWindow.IsNull() && !state.AutoScaleTechWindow.IsUnknown() { - if plan.AutoScaleTechWindow.ValueInt64() != state.AutoScaleTechWindow.ValueInt64() { - hasServiceParamChanges = true - } - } - } - if !hasServiceParamChanges { - if plan.AutoScaleQuotaGb.IsNull() != state.AutoScaleQuotaGb.IsNull() || plan.AutoScaleQuotaGb.IsUnknown() != state.AutoScaleQuotaGb.IsUnknown() { - hasServiceParamChanges = true - } else if !plan.AutoScaleQuotaGb.IsNull() && !plan.AutoScaleQuotaGb.IsUnknown() && !state.AutoScaleQuotaGb.IsNull() && !state.AutoScaleQuotaGb.IsUnknown() { - if plan.AutoScaleQuotaGb.ValueInt64() != state.AutoScaleQuotaGb.ValueInt64() { + } else if !plan.AutoscaleConfiguration.IsNull() && !plan.AutoscaleConfiguration.IsUnknown() && !state.AutoscaleConfiguration.IsNull() && !state.AutoscaleConfiguration.IsUnknown() { + if !strings.EqualFold(plan.AutoscaleConfiguration.ValueString(), state.AutoscaleConfiguration.ValueString()) { hasServiceParamChanges = true } } @@ -479,23 +406,20 @@ func (r *MariadbResource) Update(ctx context.Context, req resource.UpdateRequest } params := map[int]string{ - 436: resources_core.FormatInt64(plan.ResourceCPU), - 437: resources_core.FormatInt64(plan.ResourceMemory), - 438: resources_core.FormatInt64(plan.ResourceInstances), - 439: resources_core.FormatInt64(plan.ResourceDisk), - 440: resources_core.FormatBool(plan.NeedExternalAddressMaster), - 441: resources_core.FormatString(plan.IpSpaceNameMaster), - 442: resources_core.FormatString(plan.ExtBACKUPSCHEDULE), - 443: resources_core.FormatBool(plan.AutoScale), - 444: resources_core.FormatInt64(plan.AutoScalePercentage), - 445: resources_core.FormatInt64(plan.AutoScaleTechWindow), - 446: resources_core.FormatInt64(plan.AutoScaleQuotaGb), + 883: resources_core.FormatString(plan.ClusterConfiguration), + 884: resources_core.FormatString(plan.AccessConfiguration), + 885: resources_core.FormatString(plan.MariadbConfiguration), + 886: resources_core.FormatString(plan.BackupConfiguration), + 887: resources_core.FormatString(plan.AutoscaleConfiguration), } operationTimeout := "" if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -512,19 +436,12 @@ func (r *MariadbResource) Update(ctx context.Context, req resource.UpdateRequest {Code: "vault_user_path"}, {Code: "vault_fields"}, }, []resources_core.InputField{ - {Code: "appVersion", Field: "AppVersion", Type: "string"}, - {Code: "autoScale", Field: "AutoScale", Type: "bool"}, - {Code: "autoScalePercentage", Field: "AutoScalePercentage", Type: "int64"}, - {Code: "autoScaleQuotaGb", Field: "AutoScaleQuotaGb", Type: "int64"}, - {Code: "autoScaleTechWindow", Field: "AutoScaleTechWindow", Type: "int64"}, - {Code: "ext_BACKUP_SCHEDULE", Field: "ExtBACKUPSCHEDULE", Type: "string"}, - {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"}, + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "string"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "string"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "string"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "string"}, + {Code: "mariadbConfiguration", Field: "MariadbConfiguration", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "string"}, }) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -543,7 +460,6 @@ func (r *MariadbResource) Delete(ctx context.Context, req resource.DeleteRequest if state.ID.IsNull() || state.ID.IsUnknown() { return } - deleteMode := "state_only" if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { deleteMode = "suspend" @@ -553,6 +469,9 @@ func (r *MariadbResource) Delete(ctx context.Context, req resource.DeleteRequest 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 diff --git a/universal_rebuild/internal/resources_gen/115_mariadb_user_resource.go b/universal_rebuild/internal/resources_gen/115_mariadb_user_resource.go new file mode 100644 index 0000000..26f6858 --- /dev/null +++ b/universal_rebuild/internal/resources_gen/115_mariadb_user_resource.go @@ -0,0 +1,375 @@ +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(false)}, + "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{Required: true, MarkdownDescription: "Секрет будет записан в хранилище секретов и не будет отображаться публично Не изменяется после создания. Изменение требует пересоздания.", Sensitive: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "username": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("user"), 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 + } + if plan.Password.IsNull() || plan.Password.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "password is required.") + 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 known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект подтверждён в state_out, выполняется усыновление") + 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 { + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект отсутствует и будет пропущен: "+targetValue) + return + } + resp.Diagnostics.AddError("Подресурс не найден", "Объект отсутствует: "+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/universal_rebuild/internal/resources_gen/116_kafka_resource.go b/universal_rebuild/internal/resources_gen/116_kafka_resource.go index 9174ad1..57fc61b 100644 --- a/universal_rebuild/internal/resources_gen/116_kafka_resource.go +++ b/universal_rebuild/internal/resources_gen/116_kafka_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -10,6 +11,8 @@ import ( "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" ) @@ -29,6 +32,7 @@ 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"` IpSpaceNameMaster types.String `tfsdk:"ip_space_name_master"` NeedExternalAddressMaster types.Bool `tfsdk:"need_external_address_master"` ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` @@ -58,9 +62,10 @@ func (r *KafkaResource) Metadata(ctx context.Context, req resource.MetadataReque func (r *KafkaResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "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{Required: true, MarkdownDescription: "Пример: `2000`"}, @@ -119,33 +124,51 @@ func (r *KafkaResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanR } } if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { - if plan.ResourceRealm.ValueString() != state.ResourceRealm.ValueString() { + // 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() || config.ResourceCPU.IsUnknown() { + if config.ResourceCPU.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "resource_c_p_u is required.") return } - if config.ResourceDisk.IsNull() || config.ResourceDisk.IsUnknown() { + if config.ResourceCPU.IsUnknown() { + return + } + if config.ResourceDisk.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "resource_disk is required.") return } - if config.ResourceInstances.IsNull() || config.ResourceInstances.IsUnknown() { + if config.ResourceDisk.IsUnknown() { + return + } + if config.ResourceInstances.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "resource_instances is required.") return } - if config.ResourceMemory.IsNull() || config.ResourceMemory.IsUnknown() { + if config.ResourceInstances.IsUnknown() { + return + } + if config.ResourceMemory.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "resource_memory is required.") return } - if config.ResourceRealm.IsNull() || config.ResourceRealm.IsUnknown() { + if config.ResourceMemory.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 @@ -163,8 +186,10 @@ func (r *KafkaResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanR 466: resources_core.FormatString(config.IpSpaceNameMaster), 469: resources_core.FormatString(config.ResourceRealm), } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} - resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 116, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -195,7 +220,9 @@ func (r *KafkaResource) Create(ctx context.Context, req resource.CreateRequest, } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 116, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ 461: resources_core.FormatInt64(data.ResourceInstances), @@ -211,6 +238,9 @@ func (r *KafkaResource) Create(ctx context.Context, req resource.CreateRequest, 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()) @@ -359,7 +389,7 @@ func (r *KafkaResource) Update(ctx context.Context, req resource.UpdateRequest, 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 plan.IpSpaceNameMaster.ValueString() != state.IpSpaceNameMaster.ValueString() { + if !strings.EqualFold(plan.IpSpaceNameMaster.ValueString(), state.IpSpaceNameMaster.ValueString()) { hasServiceParamChanges = true } } @@ -392,6 +422,9 @@ func (r *KafkaResource) Update(ctx context.Context, req resource.UpdateRequest, if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -433,7 +466,6 @@ func (r *KafkaResource) Delete(ctx context.Context, req resource.DeleteRequest, if state.ID.IsNull() || state.ID.IsUnknown() { return } - deleteMode := "state_only" if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { deleteMode = "suspend" @@ -443,6 +475,9 @@ func (r *KafkaResource) Delete(ctx context.Context, req resource.DeleteRequest, 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 diff --git a/universal_rebuild/internal/resources_gen/116_kafka_topic_resource.go b/universal_rebuild/internal/resources_gen/116_kafka_topic_resource.go index 5bdf934..a1f3041 100644 --- a/universal_rebuild/internal/resources_gen/116_kafka_topic_resource.go +++ b/universal_rebuild/internal/resources_gen/116_kafka_topic_resource.go @@ -32,6 +32,7 @@ type KafkaTopicModel struct { 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"` @@ -47,7 +48,7 @@ func (r *KafkaTopicResource) Metadata(ctx context.Context, req resource.Metadata func (r *KafkaTopicResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, "kafka_id": schema.StringAttribute{ Required: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, @@ -55,6 +56,7 @@ func (r *KafkaTopicResource) Schema(ctx context.Context, req resource.SchemaRequ "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(false)}, "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: "Количество реплик не может привышать количество реплик брокера"}, @@ -136,8 +138,28 @@ func (r *KafkaTopicResource) Create(ctx context.Context, req resource.CreateRequ if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { operationTimeout = plan.OperationTimeout.ValueString() } - if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_topic", params, operationTimeout); err != nil { - if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(err) { + 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 { @@ -154,7 +176,7 @@ func (r *KafkaTopicResource) Create(ctx context.Context, req resource.CreateRequ resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") return } - resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) return } if targetValue != "" { @@ -222,6 +244,9 @@ func (r *KafkaTopicResource) Update(ctx context.Context, req resource.UpdateRequ if !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 @@ -292,6 +317,9 @@ func (r *KafkaTopicResource) Delete(ctx context.Context, req resource.DeleteRequ 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 != "" { diff --git a/universal_rebuild/internal/resources_gen/116_kafka_user_resource.go b/universal_rebuild/internal/resources_gen/116_kafka_user_resource.go index ede89d8..b0d2933 100644 --- a/universal_rebuild/internal/resources_gen/116_kafka_user_resource.go +++ b/universal_rebuild/internal/resources_gen/116_kafka_user_resource.go @@ -32,6 +32,7 @@ type KafkaUserModel struct { 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"` @@ -49,7 +50,7 @@ func (r *KafkaUserResource) Metadata(ctx context.Context, req resource.MetadataR func (r *KafkaUserResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, "kafka_id": schema.StringAttribute{ Required: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, @@ -57,6 +58,7 @@ func (r *KafkaUserResource) Schema(ctx context.Context, req resource.SchemaReque "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(false)}, "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{Required: true, MarkdownDescription: "Пример: `192.168.1.0/24` или `*`"}, "group": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `order-consumers` или `*`"}, "name_topic": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `orders-events`. Использовать маску нельзя(`orders*`), только полное название или `*`"}, @@ -150,8 +152,28 @@ func (r *KafkaUserResource) Create(ctx context.Context, req resource.CreateReque if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { operationTimeout = plan.OperationTimeout.ValueString() } - if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout); err != nil { - if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(err) { + 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 { @@ -168,7 +190,7 @@ func (r *KafkaUserResource) Create(ctx context.Context, req resource.CreateReque resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") return } - resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) return } if targetValue != "" { @@ -238,6 +260,9 @@ func (r *KafkaUserResource) Update(ctx context.Context, req resource.UpdateReque if !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 @@ -308,6 +333,9 @@ func (r *KafkaUserResource) Delete(ctx context.Context, req resource.DeleteReque 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 != "" { diff --git a/universal_rebuild/internal/resources_gen/117_nifi_resource.go b/universal_rebuild/internal/resources_gen/117_nifi_resource.go index d616f15..3303810 100644 --- a/universal_rebuild/internal/resources_gen/117_nifi_resource.go +++ b/universal_rebuild/internal/resources_gen/117_nifi_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -10,6 +11,8 @@ import ( "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" ) @@ -29,6 +32,7 @@ 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"` @@ -55,10 +59,11 @@ func (r *NifiResource) Metadata(ctx context.Context, req resource.MetadataReques func (r *NifiResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, "resource_name": schema.StringAttribute{Required: true}, "operation_timeout": schema.StringAttribute{Optional: true}, - "kafka_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Выбрать кластер из списка Не изменяется после создания."}, + "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`"}, @@ -106,22 +111,11 @@ func (r *NifiResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRe if resp.Diagnostics.HasError() { return } - planChanged := false - if !plan.KafkaUid.IsNull() && !plan.KafkaUid.IsUnknown() { - resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, plan.KafkaUid.ValueString()) - if err != nil { - resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error()) - } else if resolvedKafkaUid != "" && resolvedKafkaUid != plan.KafkaUid.ValueString() { - plan.KafkaUid = types.StringValue(resolvedKafkaUid) - planChanged = true - } - } - if planChanged { - resp.Diagnostics.Append(resp.Plan.Set(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 задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") @@ -129,35 +123,53 @@ func (r *NifiResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRe } } if !plan.KafkaUid.IsNull() && !plan.KafkaUid.IsUnknown() && !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() { - if plan.KafkaUid.ValueString() != state.KafkaUid.ValueString() { + // 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() { - if plan.NameTopic.ValueString() != state.NameTopic.ValueString() { + // 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() || config.KafkaUid.IsUnknown() { + if config.KafkaUid.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "kafka_uid is required.") return } - if config.NameTopic.IsNull() || config.NameTopic.IsUnknown() { + if config.KafkaUid.IsUnknown() { + return + } + if config.NameTopic.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "name_topic is required.") return } - if config.Partitions.IsNull() || config.Partitions.IsUnknown() { + if config.NameTopic.IsUnknown() { + return + } + if config.Partitions.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "partitions is required.") return } - if config.Replicas.IsNull() || config.Replicas.IsUnknown() { + 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 @@ -180,8 +192,10 @@ func (r *NifiResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRe 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.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 117, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -206,6 +220,24 @@ func (r *NifiResource) Create(ctx context.Context, req resource.CreateRequest, r 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 { @@ -218,7 +250,9 @@ func (r *NifiResource) Create(ctx context.Context, req resource.CreateRequest, r } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 117, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ 471: resources_core.FormatString(data.KafkaUid), @@ -231,6 +265,9 @@ func (r *NifiResource) Create(ctx context.Context, req resource.CreateRequest, r 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()) @@ -265,6 +302,16 @@ func (r *NifiResource) Create(ctx context.Context, req resource.CreateRequest, r 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)...) } @@ -317,6 +364,16 @@ func (r *NifiResource) Read(ctx context.Context, req resource.ReadRequest, resp 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)...) } @@ -382,6 +439,9 @@ func (r *NifiResource) Update(ctx context.Context, req resource.UpdateRequest, r if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -415,6 +475,15 @@ func (r *NifiResource) Update(ctx context.Context, req resource.UpdateRequest, r 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)...) } @@ -428,16 +497,15 @@ func (r *NifiResource) Delete(ctx context.Context, req resource.DeleteRequest, r if state.ID.IsNull() || state.ID.IsUnknown() { return } - - deleteMode := "state_only" - if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { - deleteMode = "suspend" - } + 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 diff --git a/universal_rebuild/internal/resources_gen/119_akhq_resource.go b/universal_rebuild/internal/resources_gen/119_akhq_resource.go index 6942179..cd1b8a6 100644 --- a/universal_rebuild/internal/resources_gen/119_akhq_resource.go +++ b/universal_rebuild/internal/resources_gen/119_akhq_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -11,6 +12,8 @@ import ( "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" ) @@ -30,6 +33,7 @@ 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"` @@ -58,11 +62,12 @@ func (r *AkhqResource) Metadata(ctx context.Context, req resource.MetadataReques func (r *AkhqResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "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{Required: true, MarkdownDescription: "Выбрать кластер из списка"}, + "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` Не изменяется после создания."}, @@ -111,22 +116,11 @@ func (r *AkhqResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRe if resp.Diagnostics.HasError() { return } - planChanged := false - if !plan.KafkaUid.IsNull() && !plan.KafkaUid.IsUnknown() { - resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, plan.KafkaUid.ValueString()) - if err != nil { - resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error()) - } else if resolvedKafkaUid != "" && resolvedKafkaUid != plan.KafkaUid.ValueString() { - plan.KafkaUid = types.StringValue(resolvedKafkaUid) - planChanged = true - } - } - if planChanged { - resp.Diagnostics.Append(resp.Plan.Set(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 задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") @@ -134,7 +128,10 @@ func (r *AkhqResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRe } } if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { - if plan.Domain.ValueString() != state.Domain.ValueString() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) { resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") return } @@ -158,25 +155,37 @@ func (r *AkhqResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRe } } if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { - if plan.ResourceRealm.ValueString() != state.ResourceRealm.ValueString() { + // 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() || config.Domain.IsUnknown() { + if config.Domain.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "domain is required.") return } - if config.KafkaUid.IsNull() || config.KafkaUid.IsUnknown() { + if config.Domain.IsUnknown() { + return + } + if config.KafkaUid.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "kafka_uid is required.") return } - if config.ResourceRealm.IsNull() || config.ResourceRealm.IsUnknown() { + 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 @@ -201,8 +210,13 @@ func (r *AkhqResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRe 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.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 119, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -223,6 +237,24 @@ func (r *AkhqResource) Create(ctx context.Context, req resource.CreateRequest, r 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 { @@ -235,7 +267,12 @@ func (r *AkhqResource) Create(ctx context.Context, req resource.CreateRequest, r } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 119, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ 550: resources_core.FormatString(data.ResourceRealm), @@ -250,6 +287,9 @@ func (r *AkhqResource) Create(ctx context.Context, req resource.CreateRequest, r 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()) @@ -286,6 +326,16 @@ func (r *AkhqResource) Create(ctx context.Context, req resource.CreateRequest, r 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)...) } @@ -340,6 +390,16 @@ func (r *AkhqResource) Read(ctx context.Context, req resource.ReadRequest, resp 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)...) } @@ -367,7 +427,7 @@ func (r *AkhqResource) Update(ctx context.Context, req resource.UpdateRequest, r 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 plan.KafkaUid.ValueString() != state.KafkaUid.ValueString() { + if !strings.EqualFold(plan.KafkaUid.ValueString(), state.KafkaUid.ValueString()) { hasServiceParamChanges = true } } @@ -395,6 +455,9 @@ func (r *AkhqResource) Update(ctx context.Context, req resource.UpdateRequest, r if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -430,6 +493,15 @@ func (r *AkhqResource) Update(ctx context.Context, req resource.UpdateRequest, r 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)...) } @@ -443,7 +515,6 @@ func (r *AkhqResource) Delete(ctx context.Context, req resource.DeleteRequest, r if state.ID.IsNull() || state.ID.IsUnknown() { return } - deleteMode := "state_only" if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { deleteMode = "suspend" @@ -453,6 +524,9 @@ func (r *AkhqResource) Delete(ctx context.Context, req resource.DeleteRequest, r 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 diff --git a/universal_rebuild/internal/resources_gen/120_clickhouse_database_resource.go b/universal_rebuild/internal/resources_gen/120_clickhouse_database_resource.go index a805419..acefa96 100644 --- a/universal_rebuild/internal/resources_gen/120_clickhouse_database_resource.go +++ b/universal_rebuild/internal/resources_gen/120_clickhouse_database_resource.go @@ -11,6 +11,7 @@ import ( "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" ) @@ -32,7 +33,8 @@ type ClickhouseDatabaseModel struct { AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` OperationTimeout types.String `tfsdk:"operation_timeout"` - DatabaseName types.String `tfsdk:"database_name"` + LogLevel types.String `tfsdk:"log_level"` + DbName types.String `tfsdk:"db_name"` } func NewClickhouseDatabaseResource() resource.Resource { @@ -45,7 +47,7 @@ func (r *ClickhouseDatabaseResource) Metadata(ctx context.Context, req resource. func (r *ClickhouseDatabaseResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, "clickhouse_id": schema.StringAttribute{ Required: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, @@ -53,7 +55,8 @@ func (r *ClickhouseDatabaseResource) Schema(ctx context.Context, req resource.Sc "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(false)}, "operation_timeout": schema.StringAttribute{Optional: true}, - "database_name": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `db01` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "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} } @@ -64,10 +67,6 @@ func (r *ClickhouseDatabaseResource) Create(ctx context.Context, req resource.Cr if resp.Diagnostics.HasError() { return } - if plan.DatabaseName.IsNull() || plan.DatabaseName.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "database_name is required.") - return - } instanceUID := strings.TrimSpace(plan.ClickhouseID.ValueString()) if instanceUID == "" { @@ -77,10 +76,10 @@ func (r *ClickhouseDatabaseResource) Create(ctx context.Context, req resource.Cr adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() idParams := map[string]string{ - "databaseName": resources_core.FormatString(plan.DatabaseName), + "dbName": resources_core.FormatString(plan.DbName), } identityCodes := []string{ - "databaseName", + "dbName", } listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", identityCodes) targetValue := strings.TrimSpace(idParams[idKey]) @@ -115,15 +114,35 @@ func (r *ClickhouseDatabaseResource) Create(ctx context.Context, req resource.Cr } params := resources_core.CompactParams(map[string]string{ - "databaseName": resources_core.FormatString(plan.DatabaseName), + "dbName": resources_core.FormatString(plan.DbName), }) operationTimeout := "" if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { operationTimeout = plan.OperationTimeout.ValueString() } - if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_database", params, operationTimeout); err != nil { - if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(err) { + 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 { @@ -140,7 +159,7 @@ func (r *ClickhouseDatabaseResource) Create(ctx context.Context, req resource.Cr resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") return } - resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) return } if targetValue != "" { @@ -197,13 +216,16 @@ func (r *ClickhouseDatabaseResource) Update(ctx context.Context, req resource.Up params := resources_core.CompactParams(map[string]string{}) idParams := map[string]string{ - "databaseName": resources_core.FormatString(plan.DatabaseName), + "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 @@ -231,10 +253,10 @@ func (r *ClickhouseDatabaseResource) Delete(ctx context.Context, req resource.De skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() idParams := map[string]string{ - "databaseName": resources_core.FormatString(state.DatabaseName), + "dbName": resources_core.FormatString(state.DbName), } identityCodes := []string{ - "databaseName", + "dbName", } listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", identityCodes) targetValue := strings.TrimSpace(idParams[idKey]) @@ -267,13 +289,16 @@ func (r *ClickhouseDatabaseResource) Delete(ctx context.Context, req resource.De } params := resources_core.CompactParams(map[string]string{ - "databaseName": resources_core.FormatString(state.DatabaseName), + "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 != "" { diff --git a/universal_rebuild/internal/resources_gen/120_clickhouse_reconcile_action.go b/universal_rebuild/internal/resources_gen/120_clickhouse_reconcile_action.go new file mode 100644 index 0000000..0688703 --- /dev/null +++ b/universal_rebuild/internal/resources_gen/120_clickhouse_reconcile_action.go @@ -0,0 +1,136 @@ +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/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: clickhouse +// Action: reconcile + +var _ resource.Resource = &ClickhouseReconcileResource{} + +// ClickhouseReconcileResource triggers a one-time action. +type ClickhouseReconcileResource struct { + client *core.UniversalClient +} + +type ClickhouseReconcileModel struct { + ID types.String `tfsdk:"id"` + ClickhouseID types.String `tfsdk:"clickhouse_id"` + RunID types.String `tfsdk:"run_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` +} + +func NewClickhouseReconcileResource() resource.Resource { + return &ClickhouseReconcileResource{} +} + +func (r *ClickhouseReconcileResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_clickhouse_reconcile" +} + +func (r *ClickhouseReconcileResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true}, + "clickhouse_id": schema.StringAttribute{Required: true}, + "run_id": schema.StringAttribute{Required: true}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "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 *ClickhouseReconcileResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan ClickhouseReconcileModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.ClickhouseID.ValueString()) + runID := strings.TrimSpace(plan.RunID.ValueString()) + if instanceUID == "" || runID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра или run_id") + return + } + + params := 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, "reconcile", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "reconcile", runID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ClickhouseReconcileResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state ClickhouseReconcileModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *ClickhouseReconcileResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan ClickhouseReconcileModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.ClickhouseID.ValueString()) + runID := strings.TrimSpace(plan.RunID.ValueString()) + if instanceUID == "" || runID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра или run_id") + return + } + + params := map[string]string{} + + if err := resources_core.RunOperationByCode(ctx, r.client, instanceUID, "reconcile", params); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "reconcile", runID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ClickhouseReconcileResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // No-op: removing the resource does not trigger a remote action. +} + +func (r *ClickhouseReconcileResource) 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/universal_rebuild/internal/resources_gen/120_clickhouse_resource.go b/universal_rebuild/internal/resources_gen/120_clickhouse_resource.go index 7fe7e3d..426fa4e 100644 --- a/universal_rebuild/internal/resources_gen/120_clickhouse_resource.go +++ b/universal_rebuild/internal/resources_gen/120_clickhouse_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -10,8 +11,8 @@ import ( "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/stringdefault" + "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" ) @@ -28,35 +29,28 @@ type ClickhouseResource struct { } type ClickhouseModel struct { - ID types.String `tfsdk:"id"` - ResourceName types.String `tfsdk:"resource_name"` - OperationTimeout types.String `tfsdk:"operation_timeout"` - AppVersion types.String `tfsdk:"app_version"` - ExtBACKUPNUMTORETAIN types.Int64 `tfsdk:"ext__b_a_c_k_u_p__n_u_m__t_o__r_e_t_a_i_n"` - ExtBACKUPSCHEDULE types.String `tfsdk:"ext__b_a_c_k_u_p__s_c_h_e_d_u_l_e"` - IpSpaceNameMaster types.String `tfsdk:"ip_space_name_master"` - NeedCreateKeeper types.Bool `tfsdk:"need_create_keeper"` - NeedExternalAddressMaster types.Bool `tfsdk:"need_external_address_master"` - ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` - ResourceCPUKeeper types.Int64 `tfsdk:"resource_c_p_u_keeper"` - ResourceDisk types.Int64 `tfsdk:"resource_disk"` - ResourceInstances types.Int64 `tfsdk:"resource_instances"` - ResourceInstancesKeeper types.Int64 `tfsdk:"resource_instances_keeper"` - ResourceInstancesShards types.Int64 `tfsdk:"resource_instances_shards"` - ResourceMemory types.Int64 `tfsdk:"resource_memory"` - ResourceMemoryKeeper types.Int64 `tfsdk:"resource_memory_keeper"` - 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"` + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration types.String `tfsdk:"access_configuration"` + AutoscaleConfiguration types.String `tfsdk:"autoscale_configuration"` + BackupConfiguration types.String `tfsdk:"backup_configuration"` + ClickhouseConfiguration types.String `tfsdk:"clickhouse_configuration"` + ClickhouseFilesConfiguration types.String `tfsdk:"clickhouse_files_configuration"` + ClickhouseKeeperConfiguration types.String `tfsdk:"clickhouse_keeper_configuration"` + ClusterConfiguration types.String `tfsdk:"cluster_configuration"` + StartupConfiguration types.String `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 { @@ -69,35 +63,28 @@ func (r *ClickhouseResource) Metadata(ctx context.Context, req resource.Metadata func (r *ClickhouseResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, - "resource_name": schema.StringAttribute{Required: true}, - "operation_timeout": schema.StringAttribute{Optional: true}, - "app_version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("25.10"), MarkdownDescription: "Версия `clickhouse-server`"}, - "ext__b_a_c_k_u_p__n_u_m__t_o__r_e_t_a_i_n": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(7), MarkdownDescription: "Пример: `7`"}, - "ext__b_a_c_k_u_p__s_c_h_e_d_u_l_e": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("0 0 * * *"), MarkdownDescription: "Принимает формат `CRON`"}, - "ip_space_name_master": schema.StringAttribute{Optional: true, MarkdownDescription: "Пример: `internet-no-antiddos-v1`"}, - "need_create_keeper": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Необходим для кластеризации ClickHouse"}, - "need_external_address_master": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Позволяет подключиться извне к ClickHouse"}, - "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1000), MarkdownDescription: "Пример: `1000`"}, - "resource_c_p_u_keeper": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(50), MarkdownDescription: "Пример: `100`"}, - "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Пример: `100`"}, - "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Пример: `3`"}, - "resource_instances_keeper": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Пример: `1`"}, - "resource_instances_shards": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Пример: `1`"}, - "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1536), MarkdownDescription: "Пример: `2048`"}, - "resource_memory_keeper": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(152), MarkdownDescription: "Пример: `256`"}, - "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes в котором будет развёрнуто приложение Не изменяется после создания."}, - "s3_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Экземпляр (учетная запись) S3 для резервного копирования. Резервное копирование обязательно Не изменяется после создания."}, - "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}, + "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.StringAttribute{Required: true}, + "autoscale_configuration": schema.StringAttribute{Required: true}, + "backup_configuration": schema.StringAttribute{Required: true}, + "clickhouse_configuration": schema.StringAttribute{Required: true}, + "clickhouse_files_configuration": schema.StringAttribute{Optional: true, MarkdownDescription: "Формат:
- path: somethings3.xml
xmlConfig: '<clickhouse>......</clickhouse>'"}, + "clickhouse_keeper_configuration": schema.StringAttribute{Required: true}, + "cluster_configuration": schema.StringAttribute{Required: true}, + "startup_configuration": 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} @@ -132,48 +119,70 @@ func (r *ClickhouseResource) ModifyPlan(ctx context.Context, req resource.Modify if resp.Diagnostics.HasError() { return } - planChanged := false - if !plan.S3Uid.IsNull() && !plan.S3Uid.IsUnknown() { - resolvedS3Uid, err := r.client.ResolveRefSvcParamValue(ctx, 12, plan.S3Uid.ValueString()) - if err != nil { - resp.Diagnostics.AddWarning("Failed to resolve s3_uid", err.Error()) - } else if resolvedS3Uid != "" && resolvedS3Uid != plan.S3Uid.ValueString() { - plan.S3Uid = types.StringValue(resolvedS3Uid) - planChanged = true - } - } - if planChanged { - resp.Diagnostics.Append(resp.Plan.Set(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() { - if 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() { - if plan.S3Uid.ValueString() != state.S3Uid.ValueString() { - resp.Diagnostics.AddError("Нельзя изменить s3_uid", "Параметр задается при создании и не может быть изменен.") + if !plan.StartupConfiguration.IsNull() && !plan.StartupConfiguration.IsUnknown() && !state.StartupConfiguration.IsNull() && !state.StartupConfiguration.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.StartupConfiguration.ValueString(), state.StartupConfiguration.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить startup_configuration", "Параметр задается при создании и не может быть изменен.") return } } return } - if config.ResourceRealm.IsNull() || config.ResourceRealm.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + if config.AccessConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "access_configuration is required.") return } - if config.S3Uid.IsNull() || config.S3Uid.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "s3_uid is required.") + if config.AccessConfiguration.IsUnknown() { + return + } + if config.AutoscaleConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "autoscale_configuration is required.") + return + } + if config.AutoscaleConfiguration.IsUnknown() { + return + } + if config.BackupConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "backup_configuration is required.") + return + } + if config.BackupConfiguration.IsUnknown() { + return + } + if config.ClickhouseConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "clickhouse_configuration is required.") + return + } + if config.ClickhouseConfiguration.IsUnknown() { + return + } + if config.ClickhouseKeeperConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "clickhouse_keeper_configuration is required.") + return + } + if config.ClickhouseKeeperConfiguration.IsUnknown() { + return + } + if config.ClusterConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "cluster_configuration is required.") + return + } + if config.ClusterConfiguration.IsUnknown() { + return + } + if config.StartupConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "startup_configuration is required.") + return + } + if config.StartupConfiguration.IsUnknown() { return } @@ -184,34 +193,20 @@ func (r *ClickhouseResource) ModifyPlan(ctx context.Context, req resource.Modify 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{ - 572: resources_core.FormatString(config.ResourceRealm), - 573: resources_core.FormatString(config.S3Uid), - 574: resources_core.FormatInt64(config.ResourceInstances), - 575: resources_core.FormatInt64(config.ResourceInstancesShards), - 576: resources_core.FormatInt64(config.ResourceCPU), - 577: resources_core.FormatInt64(config.ResourceMemory), - 578: resources_core.FormatString(config.AppVersion), - 579: resources_core.FormatInt64(config.ResourceDisk), - 580: resources_core.FormatBool(config.NeedExternalAddressMaster), - 581: resources_core.FormatString(config.IpSpaceNameMaster), - 582: resources_core.FormatBool(config.NeedCreateKeeper), - 583: resources_core.FormatInt64(config.ResourceCPUKeeper), - 584: resources_core.FormatInt64(config.ResourceMemoryKeeper), - 585: resources_core.FormatInt64(config.ResourceInstancesKeeper), - 586: resources_core.FormatString(config.ExtBACKUPSCHEDULE), - 587: resources_core.FormatInt64(config.ExtBACKUPNUMTORETAIN), + 837: resources_core.FormatString(config.StartupConfiguration), + 838: resources_core.FormatString(config.ClusterConfiguration), + 839: resources_core.FormatString(config.AccessConfiguration), + 840: resources_core.FormatString(config.ClickhouseKeeperConfiguration), + 841: resources_core.FormatString(config.ClickhouseConfiguration), + 842: resources_core.FormatString(config.BackupConfiguration), + 843: resources_core.FormatString(config.AutoscaleConfiguration), + 851: resources_core.FormatString(config.ClickhouseFilesConfiguration), } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} - resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 120, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -220,41 +215,58 @@ func (r *ClickhouseResource) Create(ctx context.Context, req resource.CreateRequ if resp.Diagnostics.HasError() { return } - if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + if data.AccessConfiguration.IsNull() || data.AccessConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "access_configuration is required.") return } - if data.S3Uid.IsNull() || data.S3Uid.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "s3_uid is required.") + if data.AutoscaleConfiguration.IsNull() || data.AutoscaleConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "autoscale_configuration is required.") + return + } + if data.BackupConfiguration.IsNull() || data.BackupConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "backup_configuration is required.") + return + } + if data.ClickhouseConfiguration.IsNull() || data.ClickhouseConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "clickhouse_configuration is required.") + return + } + if data.ClickhouseKeeperConfiguration.IsNull() || data.ClickhouseKeeperConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "clickhouse_keeper_configuration is required.") + return + } + if data.ClusterConfiguration.IsNull() || data.ClusterConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "cluster_configuration is required.") + return + } + if data.StartupConfiguration.IsNull() || data.StartupConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "startup_configuration is required.") return } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 120, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ - 572: resources_core.FormatString(data.ResourceRealm), - 573: resources_core.FormatString(data.S3Uid), - 574: resources_core.FormatInt64(data.ResourceInstances), - 575: resources_core.FormatInt64(data.ResourceInstancesShards), - 576: resources_core.FormatInt64(data.ResourceCPU), - 577: resources_core.FormatInt64(data.ResourceMemory), - 578: resources_core.FormatString(data.AppVersion), - 579: resources_core.FormatInt64(data.ResourceDisk), - 580: resources_core.FormatBool(data.NeedExternalAddressMaster), - 581: resources_core.FormatString(data.IpSpaceNameMaster), - 582: resources_core.FormatBool(data.NeedCreateKeeper), - 583: resources_core.FormatInt64(data.ResourceCPUKeeper), - 584: resources_core.FormatInt64(data.ResourceMemoryKeeper), - 585: resources_core.FormatInt64(data.ResourceInstancesKeeper), - 586: resources_core.FormatString(data.ExtBACKUPSCHEDULE), - 587: resources_core.FormatInt64(data.ExtBACKUPNUMTORETAIN), + 837: resources_core.FormatString(data.StartupConfiguration), + 838: resources_core.FormatString(data.ClusterConfiguration), + 839: resources_core.FormatString(data.AccessConfiguration), + 840: resources_core.FormatString(data.ClickhouseKeeperConfiguration), + 841: resources_core.FormatString(data.ClickhouseConfiguration), + 842: resources_core.FormatString(data.BackupConfiguration), + 843: resources_core.FormatString(data.AutoscaleConfiguration), + 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()) @@ -272,21 +284,14 @@ func (r *ClickhouseResource) Create(ctx context.Context, req resource.CreateRequ {Code: "vault_user_path"}, {Code: "vault_fields"}, }, []resources_core.InputField{ - {Code: "appVersion", Field: "AppVersion", Type: "string"}, - {Code: "ext_BACKUP_NUM_TO_RETAIN", Field: "ExtBACKUPNUMTORETAIN", Type: "int64"}, - {Code: "ext_BACKUP_SCHEDULE", Field: "ExtBACKUPSCHEDULE", Type: "string"}, - {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"}, - {Code: "needCreateKeeper", Field: "NeedCreateKeeper", Type: "bool"}, - {Code: "needExternalAddressMaster", Field: "NeedExternalAddressMaster", Type: "bool"}, - {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, - {Code: "resourceCPUKeeper", Field: "ResourceCPUKeeper", Type: "int64"}, - {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, - {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, - {Code: "resourceInstancesKeeper", Field: "ResourceInstancesKeeper", Type: "int64"}, - {Code: "resourceInstancesShards", Field: "ResourceInstancesShards", Type: "int64"}, - {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, - {Code: "resourceMemoryKeeper", Field: "ResourceMemoryKeeper", Type: "int64"}, - {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "string"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "string"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "string"}, + {Code: "clickhouseConfiguration", Field: "ClickhouseConfiguration", Type: "string"}, + {Code: "clickhouseFilesConfiguration", Field: "ClickhouseFilesConfiguration", Type: "string"}, + {Code: "clickhouseKeeperConfiguration", Field: "ClickhouseKeeperConfiguration", Type: "string"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "string"}, }) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -327,21 +332,14 @@ func (r *ClickhouseResource) Read(ctx context.Context, req resource.ReadRequest, {Code: "vault_user_path"}, {Code: "vault_fields"}, }, []resources_core.InputField{ - {Code: "appVersion", Field: "AppVersion", Type: "string"}, - {Code: "ext_BACKUP_NUM_TO_RETAIN", Field: "ExtBACKUPNUMTORETAIN", Type: "int64"}, - {Code: "ext_BACKUP_SCHEDULE", Field: "ExtBACKUPSCHEDULE", Type: "string"}, - {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"}, - {Code: "needCreateKeeper", Field: "NeedCreateKeeper", Type: "bool"}, - {Code: "needExternalAddressMaster", Field: "NeedExternalAddressMaster", Type: "bool"}, - {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, - {Code: "resourceCPUKeeper", Field: "ResourceCPUKeeper", Type: "int64"}, - {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, - {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, - {Code: "resourceInstancesKeeper", Field: "ResourceInstancesKeeper", Type: "int64"}, - {Code: "resourceInstancesShards", Field: "ResourceInstancesShards", Type: "int64"}, - {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, - {Code: "resourceMemoryKeeper", Field: "ResourceMemoryKeeper", Type: "int64"}, - {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "string"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "string"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "string"}, + {Code: "clickhouseConfiguration", Field: "ClickhouseConfiguration", Type: "string"}, + {Code: "clickhouseFilesConfiguration", Field: "ClickhouseFilesConfiguration", Type: "string"}, + {Code: "clickhouseKeeperConfiguration", Field: "ClickhouseKeeperConfiguration", Type: "string"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "string"}, }) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -371,127 +369,64 @@ func (r *ClickhouseResource) Update(ctx context.Context, req resource.UpdateRequ hasServiceParamChanges := false if !hasServiceParamChanges { - if plan.ResourceInstances.IsNull() != state.ResourceInstances.IsNull() || plan.ResourceInstances.IsUnknown() != state.ResourceInstances.IsUnknown() { + if plan.ClusterConfiguration.IsNull() != state.ClusterConfiguration.IsNull() || plan.ClusterConfiguration.IsUnknown() != state.ClusterConfiguration.IsUnknown() { hasServiceParamChanges = true - } else if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() { - if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() { + } else if !plan.ClusterConfiguration.IsNull() && !plan.ClusterConfiguration.IsUnknown() && !state.ClusterConfiguration.IsNull() && !state.ClusterConfiguration.IsUnknown() { + if !strings.EqualFold(plan.ClusterConfiguration.ValueString(), state.ClusterConfiguration.ValueString()) { hasServiceParamChanges = true } } } if !hasServiceParamChanges { - if plan.ResourceInstancesShards.IsNull() != state.ResourceInstancesShards.IsNull() || plan.ResourceInstancesShards.IsUnknown() != state.ResourceInstancesShards.IsUnknown() { + if plan.ClickhouseKeeperConfiguration.IsNull() != state.ClickhouseKeeperConfiguration.IsNull() || plan.ClickhouseKeeperConfiguration.IsUnknown() != state.ClickhouseKeeperConfiguration.IsUnknown() { hasServiceParamChanges = true - } else if !plan.ResourceInstancesShards.IsNull() && !plan.ResourceInstancesShards.IsUnknown() && !state.ResourceInstancesShards.IsNull() && !state.ResourceInstancesShards.IsUnknown() { - if plan.ResourceInstancesShards.ValueInt64() != state.ResourceInstancesShards.ValueInt64() { + } else if !plan.ClickhouseKeeperConfiguration.IsNull() && !plan.ClickhouseKeeperConfiguration.IsUnknown() && !state.ClickhouseKeeperConfiguration.IsNull() && !state.ClickhouseKeeperConfiguration.IsUnknown() { + if !strings.EqualFold(plan.ClickhouseKeeperConfiguration.ValueString(), state.ClickhouseKeeperConfiguration.ValueString()) { hasServiceParamChanges = true } } } if !hasServiceParamChanges { - if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() { + if plan.AccessConfiguration.IsNull() != state.AccessConfiguration.IsNull() || plan.AccessConfiguration.IsUnknown() != state.AccessConfiguration.IsUnknown() { hasServiceParamChanges = true - } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { - if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + } else if !plan.AccessConfiguration.IsNull() && !plan.AccessConfiguration.IsUnknown() && !state.AccessConfiguration.IsNull() && !state.AccessConfiguration.IsUnknown() { + if !strings.EqualFold(plan.AccessConfiguration.ValueString(), state.AccessConfiguration.ValueString()) { hasServiceParamChanges = true } } } if !hasServiceParamChanges { - if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() { + if plan.ClickhouseConfiguration.IsNull() != state.ClickhouseConfiguration.IsNull() || plan.ClickhouseConfiguration.IsUnknown() != state.ClickhouseConfiguration.IsUnknown() { hasServiceParamChanges = true - } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() { - if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() { + } else if !plan.ClickhouseConfiguration.IsNull() && !plan.ClickhouseConfiguration.IsUnknown() && !state.ClickhouseConfiguration.IsNull() && !state.ClickhouseConfiguration.IsUnknown() { + if !strings.EqualFold(plan.ClickhouseConfiguration.ValueString(), state.ClickhouseConfiguration.ValueString()) { hasServiceParamChanges = true } } } if !hasServiceParamChanges { - if plan.AppVersion.IsNull() != state.AppVersion.IsNull() || plan.AppVersion.IsUnknown() != state.AppVersion.IsUnknown() { + if plan.BackupConfiguration.IsNull() != state.BackupConfiguration.IsNull() || plan.BackupConfiguration.IsUnknown() != state.BackupConfiguration.IsUnknown() { hasServiceParamChanges = true - } else if !plan.AppVersion.IsNull() && !plan.AppVersion.IsUnknown() && !state.AppVersion.IsNull() && !state.AppVersion.IsUnknown() { - if plan.AppVersion.ValueString() != state.AppVersion.ValueString() { + } else if !plan.BackupConfiguration.IsNull() && !plan.BackupConfiguration.IsUnknown() && !state.BackupConfiguration.IsNull() && !state.BackupConfiguration.IsUnknown() { + if !strings.EqualFold(plan.BackupConfiguration.ValueString(), state.BackupConfiguration.ValueString()) { hasServiceParamChanges = true } } } if !hasServiceParamChanges { - if plan.ResourceDisk.IsNull() != state.ResourceDisk.IsNull() || plan.ResourceDisk.IsUnknown() != state.ResourceDisk.IsUnknown() { + if plan.AutoscaleConfiguration.IsNull() != state.AutoscaleConfiguration.IsNull() || plan.AutoscaleConfiguration.IsUnknown() != state.AutoscaleConfiguration.IsUnknown() { hasServiceParamChanges = true - } else if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() { - if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() { + } else if !plan.AutoscaleConfiguration.IsNull() && !plan.AutoscaleConfiguration.IsUnknown() && !state.AutoscaleConfiguration.IsNull() && !state.AutoscaleConfiguration.IsUnknown() { + if !strings.EqualFold(plan.AutoscaleConfiguration.ValueString(), state.AutoscaleConfiguration.ValueString()) { hasServiceParamChanges = true } } } if !hasServiceParamChanges { - if plan.NeedExternalAddressMaster.IsNull() != state.NeedExternalAddressMaster.IsNull() || plan.NeedExternalAddressMaster.IsUnknown() != state.NeedExternalAddressMaster.IsUnknown() { + if plan.ClickhouseFilesConfiguration.IsNull() != state.ClickhouseFilesConfiguration.IsNull() || plan.ClickhouseFilesConfiguration.IsUnknown() != state.ClickhouseFilesConfiguration.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 plan.IpSpaceNameMaster.ValueString() != state.IpSpaceNameMaster.ValueString() { - hasServiceParamChanges = true - } - } - } - if !hasServiceParamChanges { - if plan.NeedCreateKeeper.IsNull() != state.NeedCreateKeeper.IsNull() || plan.NeedCreateKeeper.IsUnknown() != state.NeedCreateKeeper.IsUnknown() { - hasServiceParamChanges = true - } else if !plan.NeedCreateKeeper.IsNull() && !plan.NeedCreateKeeper.IsUnknown() && !state.NeedCreateKeeper.IsNull() && !state.NeedCreateKeeper.IsUnknown() { - if plan.NeedCreateKeeper.ValueBool() != state.NeedCreateKeeper.ValueBool() { - hasServiceParamChanges = true - } - } - } - if !hasServiceParamChanges { - if plan.ResourceCPUKeeper.IsNull() != state.ResourceCPUKeeper.IsNull() || plan.ResourceCPUKeeper.IsUnknown() != state.ResourceCPUKeeper.IsUnknown() { - hasServiceParamChanges = true - } else if !plan.ResourceCPUKeeper.IsNull() && !plan.ResourceCPUKeeper.IsUnknown() && !state.ResourceCPUKeeper.IsNull() && !state.ResourceCPUKeeper.IsUnknown() { - if plan.ResourceCPUKeeper.ValueInt64() != state.ResourceCPUKeeper.ValueInt64() { - hasServiceParamChanges = true - } - } - } - if !hasServiceParamChanges { - if plan.ResourceMemoryKeeper.IsNull() != state.ResourceMemoryKeeper.IsNull() || plan.ResourceMemoryKeeper.IsUnknown() != state.ResourceMemoryKeeper.IsUnknown() { - hasServiceParamChanges = true - } else if !plan.ResourceMemoryKeeper.IsNull() && !plan.ResourceMemoryKeeper.IsUnknown() && !state.ResourceMemoryKeeper.IsNull() && !state.ResourceMemoryKeeper.IsUnknown() { - if plan.ResourceMemoryKeeper.ValueInt64() != state.ResourceMemoryKeeper.ValueInt64() { - hasServiceParamChanges = true - } - } - } - if !hasServiceParamChanges { - if plan.ResourceInstancesKeeper.IsNull() != state.ResourceInstancesKeeper.IsNull() || plan.ResourceInstancesKeeper.IsUnknown() != state.ResourceInstancesKeeper.IsUnknown() { - hasServiceParamChanges = true - } else if !plan.ResourceInstancesKeeper.IsNull() && !plan.ResourceInstancesKeeper.IsUnknown() && !state.ResourceInstancesKeeper.IsNull() && !state.ResourceInstancesKeeper.IsUnknown() { - if plan.ResourceInstancesKeeper.ValueInt64() != state.ResourceInstancesKeeper.ValueInt64() { - hasServiceParamChanges = true - } - } - } - if !hasServiceParamChanges { - if plan.ExtBACKUPSCHEDULE.IsNull() != state.ExtBACKUPSCHEDULE.IsNull() || plan.ExtBACKUPSCHEDULE.IsUnknown() != state.ExtBACKUPSCHEDULE.IsUnknown() { - hasServiceParamChanges = true - } else if !plan.ExtBACKUPSCHEDULE.IsNull() && !plan.ExtBACKUPSCHEDULE.IsUnknown() && !state.ExtBACKUPSCHEDULE.IsNull() && !state.ExtBACKUPSCHEDULE.IsUnknown() { - if plan.ExtBACKUPSCHEDULE.ValueString() != state.ExtBACKUPSCHEDULE.ValueString() { - hasServiceParamChanges = true - } - } - } - if !hasServiceParamChanges { - if plan.ExtBACKUPNUMTORETAIN.IsNull() != state.ExtBACKUPNUMTORETAIN.IsNull() || plan.ExtBACKUPNUMTORETAIN.IsUnknown() != state.ExtBACKUPNUMTORETAIN.IsUnknown() { - hasServiceParamChanges = true - } else if !plan.ExtBACKUPNUMTORETAIN.IsNull() && !plan.ExtBACKUPNUMTORETAIN.IsUnknown() && !state.ExtBACKUPNUMTORETAIN.IsNull() && !state.ExtBACKUPNUMTORETAIN.IsUnknown() { - if plan.ExtBACKUPNUMTORETAIN.ValueInt64() != state.ExtBACKUPNUMTORETAIN.ValueInt64() { + } 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 } } @@ -512,26 +447,22 @@ func (r *ClickhouseResource) Update(ctx context.Context, req resource.UpdateRequ } params := map[int]string{ - 603: resources_core.FormatInt64(plan.ResourceInstances), - 604: resources_core.FormatInt64(plan.ResourceInstancesShards), - 605: resources_core.FormatInt64(plan.ResourceCPU), - 606: resources_core.FormatInt64(plan.ResourceMemory), - 607: resources_core.FormatString(plan.AppVersion), - 608: resources_core.FormatInt64(plan.ResourceDisk), - 609: resources_core.FormatBool(plan.NeedExternalAddressMaster), - 610: resources_core.FormatString(plan.IpSpaceNameMaster), - 611: resources_core.FormatBool(plan.NeedCreateKeeper), - 612: resources_core.FormatInt64(plan.ResourceCPUKeeper), - 613: resources_core.FormatInt64(plan.ResourceMemoryKeeper), - 614: resources_core.FormatInt64(plan.ResourceInstancesKeeper), - 615: resources_core.FormatString(plan.ExtBACKUPSCHEDULE), - 616: resources_core.FormatInt64(plan.ExtBACKUPNUMTORETAIN), + 844: resources_core.FormatString(plan.ClusterConfiguration), + 845: resources_core.FormatString(plan.ClickhouseKeeperConfiguration), + 846: resources_core.FormatString(plan.AccessConfiguration), + 847: resources_core.FormatString(plan.ClickhouseConfiguration), + 848: resources_core.FormatString(plan.BackupConfiguration), + 849: resources_core.FormatString(plan.AutoscaleConfiguration), + 850: resources_core.FormatString(plan.ClickhouseFilesConfiguration), } operationTimeout := "" if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -548,21 +479,14 @@ func (r *ClickhouseResource) Update(ctx context.Context, req resource.UpdateRequ {Code: "vault_user_path"}, {Code: "vault_fields"}, }, []resources_core.InputField{ - {Code: "appVersion", Field: "AppVersion", Type: "string"}, - {Code: "ext_BACKUP_NUM_TO_RETAIN", Field: "ExtBACKUPNUMTORETAIN", Type: "int64"}, - {Code: "ext_BACKUP_SCHEDULE", Field: "ExtBACKUPSCHEDULE", Type: "string"}, - {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"}, - {Code: "needCreateKeeper", Field: "NeedCreateKeeper", Type: "bool"}, - {Code: "needExternalAddressMaster", Field: "NeedExternalAddressMaster", Type: "bool"}, - {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, - {Code: "resourceCPUKeeper", Field: "ResourceCPUKeeper", Type: "int64"}, - {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, - {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, - {Code: "resourceInstancesKeeper", Field: "ResourceInstancesKeeper", Type: "int64"}, - {Code: "resourceInstancesShards", Field: "ResourceInstancesShards", Type: "int64"}, - {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, - {Code: "resourceMemoryKeeper", Field: "ResourceMemoryKeeper", Type: "int64"}, - {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "string"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "string"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "string"}, + {Code: "clickhouseConfiguration", Field: "ClickhouseConfiguration", Type: "string"}, + {Code: "clickhouseFilesConfiguration", Field: "ClickhouseFilesConfiguration", Type: "string"}, + {Code: "clickhouseKeeperConfiguration", Field: "ClickhouseKeeperConfiguration", Type: "string"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "string"}, }) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -581,7 +505,6 @@ func (r *ClickhouseResource) Delete(ctx context.Context, req resource.DeleteRequ if state.ID.IsNull() || state.ID.IsUnknown() { return } - deleteMode := "state_only" if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { deleteMode = "suspend" @@ -591,6 +514,9 @@ func (r *ClickhouseResource) Delete(ctx context.Context, req resource.DeleteRequ 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 diff --git a/universal_rebuild/internal/resources_gen/120_clickhouse_user_resource.go b/universal_rebuild/internal/resources_gen/120_clickhouse_user_resource.go index c542544..b797482 100644 --- a/universal_rebuild/internal/resources_gen/120_clickhouse_user_resource.go +++ b/universal_rebuild/internal/resources_gen/120_clickhouse_user_resource.go @@ -33,8 +33,9 @@ type ClickhouseUserModel struct { 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"` - DatabaseName types.String `tfsdk:"database_name"` + DbName types.String `tfsdk:"db_name"` Username types.String `tfsdk:"username"` } @@ -48,7 +49,7 @@ func (r *ClickhouseUserResource) Metadata(ctx context.Context, req resource.Meta func (r *ClickhouseUserResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, "clickhouse_id": schema.StringAttribute{ Required: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, @@ -56,9 +57,10 @@ func (r *ClickhouseUserResource) Schema(ctx context.Context, req resource.Schema "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(false)}, "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()}}, - "database_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[\"database\"]"), MarkdownDescription: "Необходимо указать массив существующих баз Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, - "username": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `myusername` Не изменяется после создания. Изменение требует пересоздания.", 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} } @@ -69,10 +71,6 @@ func (r *ClickhouseUserResource) Create(ctx context.Context, req resource.Create if resp.Diagnostics.HasError() { return } - if plan.Username.IsNull() || plan.Username.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "username is required.") - return - } instanceUID := strings.TrimSpace(plan.ClickhouseID.ValueString()) if instanceUID == "" { @@ -82,13 +80,13 @@ func (r *ClickhouseUserResource) Create(ctx context.Context, req resource.Create adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() idParams := map[string]string{ - "accessHosts": resources_core.FormatString(plan.AccessHosts), - "databaseName": resources_core.FormatString(plan.DatabaseName), - "username": resources_core.FormatString(plan.Username), + "accessHosts": resources_core.FormatString(plan.AccessHosts), + "dbName": resources_core.FormatString(plan.DbName), + "username": resources_core.FormatString(plan.Username), } identityCodes := []string{ "accessHosts", - "databaseName", + "dbName", "username", } listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) @@ -124,17 +122,37 @@ func (r *ClickhouseUserResource) Create(ctx context.Context, req resource.Create } params := resources_core.CompactParams(map[string]string{ - "username": resources_core.FormatString(plan.Username), - "databaseName": resources_core.FormatString(plan.DatabaseName), - "accessHosts": resources_core.FormatString(plan.AccessHosts), + "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 err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout); err != nil { - if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(err) { + 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 { @@ -151,7 +169,7 @@ func (r *ClickhouseUserResource) Create(ctx context.Context, req resource.Create resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") return } - resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) return } if targetValue != "" { @@ -208,15 +226,18 @@ func (r *ClickhouseUserResource) Update(ctx context.Context, req resource.Update params := resources_core.CompactParams(map[string]string{}) idParams := map[string]string{ - "accessHosts": resources_core.FormatString(plan.AccessHosts), - "databaseName": resources_core.FormatString(plan.DatabaseName), - "username": resources_core.FormatString(plan.Username), + "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 @@ -244,13 +265,13 @@ func (r *ClickhouseUserResource) Delete(ctx context.Context, req resource.Delete skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() idParams := map[string]string{ - "accessHosts": resources_core.FormatString(state.AccessHosts), - "databaseName": resources_core.FormatString(state.DatabaseName), - "username": resources_core.FormatString(state.Username), + "accessHosts": resources_core.FormatString(state.AccessHosts), + "dbName": resources_core.FormatString(state.DbName), + "username": resources_core.FormatString(state.Username), } identityCodes := []string{ "accessHosts", - "databaseName", + "dbName", "username", } listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) @@ -291,6 +312,9 @@ func (r *ClickhouseUserResource) Delete(ctx context.Context, req resource.Delete 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 != "" { diff --git a/universal_rebuild/internal/resources_gen/12_s3_reconcile_action.go b/universal_rebuild/internal/resources_gen/12_s3_reconcile_action.go new file mode 100644 index 0000000..5fe33e3 --- /dev/null +++ b/universal_rebuild/internal/resources_gen/12_s3_reconcile_action.go @@ -0,0 +1,136 @@ +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/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: s3 +// Action: reconcile + +var _ resource.Resource = &S3ReconcileResource{} + +// S3ReconcileResource triggers a one-time action. +type S3ReconcileResource struct { + client *core.UniversalClient +} + +type S3ReconcileModel struct { + ID types.String `tfsdk:"id"` + S3ID types.String `tfsdk:"s3_id"` + RunID types.String `tfsdk:"run_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` +} + +func NewS3ReconcileResource() resource.Resource { + return &S3ReconcileResource{} +} + +func (r *S3ReconcileResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_s3_reconcile" +} + +func (r *S3ReconcileResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true}, + "s3_id": schema.StringAttribute{Required: true}, + "run_id": schema.StringAttribute{Required: true}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "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 *S3ReconcileResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan S3ReconcileModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.S3ID.ValueString()) + runID := strings.TrimSpace(plan.RunID.ValueString()) + if instanceUID == "" || runID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра или run_id") + return + } + + params := 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, "reconcile", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "reconcile", runID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *S3ReconcileResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state S3ReconcileModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *S3ReconcileResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan S3ReconcileModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.S3ID.ValueString()) + runID := strings.TrimSpace(plan.RunID.ValueString()) + if instanceUID == "" || runID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра или run_id") + return + } + + params := map[string]string{} + + if err := resources_core.RunOperationByCode(ctx, r.client, instanceUID, "reconcile", params); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "reconcile", runID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *S3ReconcileResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // No-op: removing the resource does not trigger a remote action. +} + +func (r *S3ReconcileResource) 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/universal_rebuild/internal/resources_gen/12_s3_resource.go b/universal_rebuild/internal/resources_gen/12_s3_resource.go index 243d720..c421eec 100644 --- a/universal_rebuild/internal/resources_gen/12_s3_resource.go +++ b/universal_rebuild/internal/resources_gen/12_s3_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -10,6 +11,8 @@ import ( "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" ) @@ -29,6 +32,7 @@ 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"` @@ -56,9 +60,10 @@ func (r *S3Resource) Metadata(ctx context.Context, req resource.MetadataRequest, func (r *S3Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "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: "Квота на бакет. Кол-во объектов. Передаём руками"}, @@ -115,27 +120,39 @@ func (r *S3Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequ } } if !plan.DisplayName.IsNull() && !plan.DisplayName.IsUnknown() && !state.DisplayName.IsNull() && !state.DisplayName.IsUnknown() { - if plan.DisplayName.ValueString() != state.DisplayName.ValueString() { + // 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() { - if plan.ResourceRealm.ValueString() != state.ResourceRealm.ValueString() { + // 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() || config.DisplayName.IsUnknown() { + if config.DisplayName.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "display_name is required.") return } - if config.ResourceRealm.IsNull() || config.ResourceRealm.IsUnknown() { + if config.DisplayName.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 @@ -148,8 +165,10 @@ func (r *S3Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequ 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.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 12, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -168,7 +187,9 @@ func (r *S3Resource) Create(ctx context.Context, req resource.CreateRequest, res } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 12, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ 163: resources_core.FormatString(data.ResourceRealm), @@ -179,6 +200,9 @@ func (r *S3Resource) Create(ctx context.Context, req resource.CreateRequest, res 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()) @@ -326,6 +350,9 @@ func (r *S3Resource) Update(ctx context.Context, req resource.UpdateRequest, res if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -365,7 +392,6 @@ func (r *S3Resource) Delete(ctx context.Context, req resource.DeleteRequest, res if state.ID.IsNull() || state.ID.IsUnknown() { return } - deleteMode := "state_only" if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { deleteMode = "suspend" @@ -375,6 +401,9 @@ func (r *S3Resource) Delete(ctx context.Context, req resource.DeleteRequest, res 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 diff --git a/universal_rebuild/internal/resources_gen/12_s3_sub_user_resource.go b/universal_rebuild/internal/resources_gen/12_s3_sub_user_resource.go index c8ac15d..54551ae 100644 --- a/universal_rebuild/internal/resources_gen/12_s3_sub_user_resource.go +++ b/universal_rebuild/internal/resources_gen/12_s3_sub_user_resource.go @@ -32,6 +32,7 @@ type S3SubUserModel struct { 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"` } @@ -46,7 +47,7 @@ func (r *S3SubUserResource) Metadata(ctx context.Context, req resource.MetadataR func (r *S3SubUserResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, "s3_id": schema.StringAttribute{ Required: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, @@ -54,6 +55,7 @@ func (r *S3SubUserResource) Schema(ctx context.Context, req resource.SchemaReque "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(false)}, "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()}}, } @@ -131,8 +133,28 @@ func (r *S3SubUserResource) Create(ctx context.Context, req resource.CreateReque if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { operationTimeout = plan.OperationTimeout.ValueString() } - if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_sub_user", params, operationTimeout); err != nil { - if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(err) { + 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 { @@ -149,7 +171,7 @@ func (r *S3SubUserResource) Create(ctx context.Context, req resource.CreateReque resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") return } - resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) return } if targetValue != "" { @@ -214,6 +236,9 @@ func (r *S3SubUserResource) Update(ctx context.Context, req resource.UpdateReque if !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 @@ -286,6 +311,9 @@ func (r *S3SubUserResource) Delete(ctx context.Context, req resource.DeleteReque 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 != "" { diff --git a/universal_rebuild/internal/resources_gen/13_s3bucket_resource.go b/universal_rebuild/internal/resources_gen/13_s3bucket_resource.go index bc48525..2dd54f8 100644 --- a/universal_rebuild/internal/resources_gen/13_s3bucket_resource.go +++ b/universal_rebuild/internal/resources_gen/13_s3bucket_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -10,7 +11,9 @@ import ( "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" ) @@ -30,6 +33,7 @@ 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"` @@ -59,16 +63,17 @@ func (r *S3bucketResource) Metadata(ctx context.Context, req resource.MetadataRe func (r *S3bucketResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "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{Required: true, MarkdownDescription: "UID Корневой услуги S3. На основе него будет создан бакет. Не изменяется после создания."}, + "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}, @@ -113,22 +118,11 @@ func (r *S3bucketResource) ModifyPlan(ctx context.Context, req resource.ModifyPl if resp.Diagnostics.HasError() { return } - planChanged := false - if !plan.S3UserUid.IsNull() && !plan.S3UserUid.IsUnknown() { - resolvedS3UserUid, err := r.client.ResolveRefSvcParamValue(ctx, 12, plan.S3UserUid.ValueString()) - if err != nil { - resp.Diagnostics.AddWarning("Failed to resolve s3_user_uid", err.Error()) - } else if resolvedS3UserUid != "" && resolvedS3UserUid != plan.S3UserUid.ValueString() { - plan.S3UserUid = types.StringValue(resolvedS3UserUid) - planChanged = true - } - } - if planChanged { - resp.Diagnostics.Append(resp.Plan.Set(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 задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") @@ -136,7 +130,10 @@ func (r *S3bucketResource) ModifyPlan(ctx context.Context, req resource.ModifyPl } } if !plan.BucketName.IsNull() && !plan.BucketName.IsUnknown() && !state.BucketName.IsNull() && !state.BucketName.IsUnknown() { - if plan.BucketName.ValueString() != state.BucketName.ValueString() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.BucketName.ValueString(), state.BucketName.ValueString()) { resp.Diagnostics.AddError("Нельзя изменить bucket_name", "Параметр задается при создании и не может быть изменен.") return } @@ -154,13 +151,19 @@ func (r *S3bucketResource) ModifyPlan(ctx context.Context, req resource.ModifyPl } } if !plan.MaxSize.IsNull() && !plan.MaxSize.IsUnknown() && !state.MaxSize.IsNull() && !state.MaxSize.IsUnknown() { - if plan.MaxSize.ValueString() != state.MaxSize.ValueString() { + // 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() { - if plan.Placement.ValueString() != state.Placement.ValueString() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Placement.ValueString(), state.Placement.ValueString()) { resp.Diagnostics.AddError("Нельзя изменить placement", "Параметр задается при создании и не может быть изменен.") return } @@ -172,21 +175,30 @@ func (r *S3bucketResource) ModifyPlan(ctx context.Context, req resource.ModifyPl } } if !plan.S3UserUid.IsNull() && !plan.S3UserUid.IsUnknown() && !state.S3UserUid.IsNull() && !state.S3UserUid.IsUnknown() { - if plan.S3UserUid.ValueString() != state.S3UserUid.ValueString() { + // 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() || config.BucketName.IsUnknown() { + if config.BucketName.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "bucket_name is required.") return } - if config.S3UserUid.IsNull() || config.S3UserUid.IsUnknown() { + 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 @@ -212,8 +224,10 @@ func (r *S3bucketResource) ModifyPlan(ctx context.Context, req resource.ModifyPl 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.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 13, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -232,7 +246,9 @@ func (r *S3bucketResource) Create(ctx context.Context, req resource.CreateReques } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 13, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ 124: resources_core.FormatString(data.S3UserUid), @@ -248,6 +264,9 @@ func (r *S3bucketResource) Create(ctx context.Context, req resource.CreateReques 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()) @@ -276,6 +295,11 @@ func (r *S3bucketResource) Create(ctx context.Context, req resource.CreateReques 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)...) } @@ -322,6 +346,11 @@ func (r *S3bucketResource) Read(ctx context.Context, req resource.ReadRequest, r 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)...) } @@ -366,6 +395,9 @@ func (r *S3bucketResource) Update(ctx context.Context, req resource.UpdateReques if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -393,6 +425,10 @@ func (r *S3bucketResource) Update(ctx context.Context, req resource.UpdateReques 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)...) } @@ -406,16 +442,15 @@ func (r *S3bucketResource) Delete(ctx context.Context, req resource.DeleteReques if state.ID.IsNull() || state.ID.IsUnknown() { return } - - deleteMode := "state_only" - if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { - deleteMode = "suspend" - } + 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 diff --git a/universal_rebuild/internal/resources_gen/149_valo_tenant_resource.go b/universal_rebuild/internal/resources_gen/149_valo_tenant_resource.go index c97e6f4..5f05928 100644 --- a/universal_rebuild/internal/resources_gen/149_valo_tenant_resource.go +++ b/universal_rebuild/internal/resources_gen/149_valo_tenant_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -11,7 +12,9 @@ import ( "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" ) @@ -31,6 +34,7 @@ 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"` @@ -57,9 +61,10 @@ func (r *ValoTenantResource) Metadata(ctx context.Context, req resource.Metadata func (r *ValoTenantResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "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{Required: true, MarkdownDescription: "Не изменяется после создания."}, "s3_storage": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Параметр указывается в `Gb`"}, @@ -115,27 +120,39 @@ func (r *ValoTenantResource) ModifyPlan(ctx context.Context, req resource.Modify } } if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { - if plan.Domain.ValueString() != state.Domain.ValueString() { + // 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() { - if plan.ResourceRealm.ValueString() != state.ResourceRealm.ValueString() { + // 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() || config.Domain.IsUnknown() { + if config.Domain.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "domain is required.") return } - if config.ResourceRealm.IsNull() || config.ResourceRealm.IsUnknown() { + if config.Domain.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 @@ -150,8 +167,13 @@ func (r *ValoTenantResource) ModifyPlan(ctx context.Context, req resource.Modify 657: resources_core.FormatString(config.ValoOptions), 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.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 149, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -170,7 +192,12 @@ func (r *ValoTenantResource) Create(ctx context.Context, req resource.CreateRequ } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 149, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ 655: resources_core.FormatString(data.ResourceRealm), @@ -183,6 +210,9 @@ func (r *ValoTenantResource) Create(ctx context.Context, req resource.CreateRequ 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()) @@ -280,7 +310,7 @@ func (r *ValoTenantResource) Update(ctx context.Context, req resource.UpdateRequ if plan.ValoOptions.IsNull() != state.ValoOptions.IsNull() || plan.ValoOptions.IsUnknown() != state.ValoOptions.IsUnknown() { hasServiceParamChanges = true } else if !plan.ValoOptions.IsNull() && !plan.ValoOptions.IsUnknown() && !state.ValoOptions.IsNull() && !state.ValoOptions.IsUnknown() { - if plan.ValoOptions.ValueString() != state.ValoOptions.ValueString() { + if !strings.EqualFold(plan.ValoOptions.ValueString(), state.ValoOptions.ValueString()) { hasServiceParamChanges = true } } @@ -318,6 +348,9 @@ func (r *ValoTenantResource) Update(ctx context.Context, req resource.UpdateRequ if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -356,7 +389,6 @@ func (r *ValoTenantResource) Delete(ctx context.Context, req resource.DeleteRequ if state.ID.IsNull() || state.ID.IsUnknown() { return } - deleteMode := "state_only" if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { deleteMode = "suspend" @@ -366,6 +398,9 @@ func (r *ValoTenantResource) Delete(ctx context.Context, req resource.DeleteRequ 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 diff --git a/universal_rebuild/internal/resources_gen/150_k8s_sthutrval_cluster_reconcile_action.go b/universal_rebuild/internal/resources_gen/150_k8s_sthutrval_cluster_reconcile_action.go index ab84648..b82d8d6 100644 --- a/universal_rebuild/internal/resources_gen/150_k8s_sthutrval_cluster_reconcile_action.go +++ b/universal_rebuild/internal/resources_gen/150_k8s_sthutrval_cluster_reconcile_action.go @@ -9,6 +9,7 @@ import ( "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/types" ) @@ -27,7 +28,9 @@ type K8sSthutrvalClusterReconcileModel struct { ID types.String `tfsdk:"id"` K8sSthutrvalClusterID types.String `tfsdk:"k8s_sthutrval_cluster_id"` RunID types.String `tfsdk:"run_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` } func NewK8sSthutrvalClusterReconcileResource() resource.Resource { @@ -43,7 +46,9 @@ func (r *K8sSthutrvalClusterReconcileResource) Schema(ctx context.Context, req r "id": schema.StringAttribute{Computed: true}, "k8s_sthutrval_cluster_id": schema.StringAttribute{Required: true}, "run_id": schema.StringAttribute{Required: true}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, "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} } @@ -68,6 +73,9 @@ func (r *K8sSthutrvalClusterReconcileResource) Create(ctx context.Context, req r if !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, "reconcile", params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return diff --git a/universal_rebuild/internal/resources_gen/150_k8s_sthutrval_cluster_resource.go b/universal_rebuild/internal/resources_gen/150_k8s_sthutrval_cluster_resource.go index 1aea186..07df93a 100644 --- a/universal_rebuild/internal/resources_gen/150_k8s_sthutrval_cluster_resource.go +++ b/universal_rebuild/internal/resources_gen/150_k8s_sthutrval_cluster_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -10,8 +11,9 @@ import ( "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" ) @@ -28,33 +30,25 @@ type K8sSthutrvalClusterResource struct { } type K8sSthutrvalClusterModel struct { - ID types.String `tfsdk:"id"` - ResourceName types.String `tfsdk:"resource_name"` - OperationTimeout types.String `tfsdk:"operation_timeout"` - AccessIpListAPI types.String `tfsdk:"access_ip_list_a_p_i"` - AccessIpListIngress types.String `tfsdk:"access_ip_list_ingress"` - AppVersion types.String `tfsdk:"app_version"` - ClusterName types.String `tfsdk:"cluster_name"` - ControlPlaneCount types.Int64 `tfsdk:"control_plane_count"` - ControlPlaneSizingDisk types.Int64 `tfsdk:"control_plane_sizing_disk"` - ControlPlaneSizingPolicy types.String `tfsdk:"control_plane_sizing_policy"` - NeedExternalAddressAPI types.Bool `tfsdk:"need_external_address_a_p_i"` - NeedExternalAddressIngress types.Bool `tfsdk:"need_external_address_ingress"` - NsxtUid types.String `tfsdk:"nsxt_uid"` - VdcUid types.String `tfsdk:"vdc_uid"` - WorkersCount types.Int64 `tfsdk:"workers_count"` - WorkersSizingDisk types.Int64 `tfsdk:"workers_sizing_disk"` - WorkersSizingPolicy types.String `tfsdk:"workers_sizing_policy"` - 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"` + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration types.String `tfsdk:"access_configuration"` + ClusterConfiguration types.String `tfsdk:"cluster_configuration"` + ControlPlaneConfiguration types.String `tfsdk:"control_plane_configuration"` + StartupConfiguration types.String `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 { @@ -67,33 +61,25 @@ func (r *K8sSthutrvalClusterResource) Metadata(ctx context.Context, req resource func (r *K8sSthutrvalClusterResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, - "resource_name": schema.StringAttribute{Required: true}, - "operation_timeout": schema.StringAttribute{Optional: true}, - "access_ip_list_a_p_i": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Access IP List для подключения к Kubernetes API. Указывается массив. Если передан пустой массив, доступ выделяется всем"}, - "access_ip_list_ingress": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Access IP List для подключения к Ingress. Указывается массив. Если передан пустой массив, доступ выделяется всем"}, - "app_version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("2.12.1"), MarkdownDescription: "Версия Штурвал (не является версией Kubernetes)"}, - "cluster_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("example1"), MarkdownDescription: "Наименование кластера Kubernetes. Имя будет использоваться в fqdn (svc.$CLUSTER_NAME) Не изменяется после создания."}, - "control_plane_count": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, - "control_plane_sizing_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(30), MarkdownDescription: "Настройка Control Plane. Disk (Gb)"}, - "control_plane_sizing_policy": schema.StringAttribute{Required: true, MarkdownDescription: "Настройка Control Plane. CPU (шт) / MEM (gb).
Наверное, должна подтягиваться из свойств ресурсной платформы Не изменяется после создания."}, - "need_external_address_a_p_i": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Выделяет внешний IP для доступа к kubernetes API. Пока нельзя выставить false Не изменяется после создания."}, - "need_external_address_ingress": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Выделяет внешний IP для доступа к Ingress Не изменяется после создания."}, - "nsxt_uid": schema.StringAttribute{Required: true, MarkdownDescription: "InstanceUid nsxt. Nsxt должен быть создан Не изменяется после создания."}, - "vdc_uid": schema.StringAttribute{Required: true, MarkdownDescription: "UI display_name for service_id=21 (mapped to UUID in core) Не изменяется после создания."}, - "workers_count": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2), MarkdownDescription: "Настройка workers. Кол-во нод"}, - "workers_sizing_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(40), MarkdownDescription: "Настройка workers. Disk (Gb)"}, - "workers_sizing_policy": schema.StringAttribute{Required: true, MarkdownDescription: "Настройка workers. CPU (шт) / MEM (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}, + "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.StringAttribute{Required: true}, + "cluster_configuration": schema.StringAttribute{Required: true}, + "control_plane_configuration": schema.StringAttribute{Required: true}, + "startup_configuration": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания."}, + "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} @@ -128,95 +114,49 @@ func (r *K8sSthutrvalClusterResource) ModifyPlan(ctx context.Context, req resour if resp.Diagnostics.HasError() { return } - planChanged := false - if !plan.NsxtUid.IsNull() && !plan.NsxtUid.IsUnknown() { - resolvedNsxtUid, err := r.client.ResolveRefSvcParamValue(ctx, 22, plan.NsxtUid.ValueString()) - if err != nil { - resp.Diagnostics.AddWarning("Failed to resolve nsxt_uid", err.Error()) - } else if resolvedNsxtUid != "" && resolvedNsxtUid != plan.NsxtUid.ValueString() { - plan.NsxtUid = types.StringValue(resolvedNsxtUid) - planChanged = true - } - } - if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() { - resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, plan.VdcUid.ValueString()) - if err != nil { - resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) - } else if resolvedVdcUid != "" && resolvedVdcUid != plan.VdcUid.ValueString() { - plan.VdcUid = types.StringValue(resolvedVdcUid) - planChanged = true - } - } - if planChanged { - resp.Diagnostics.Append(resp.Plan.Set(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.ClusterName.IsNull() && !plan.ClusterName.IsUnknown() && !state.ClusterName.IsNull() && !state.ClusterName.IsUnknown() { - if plan.ClusterName.ValueString() != state.ClusterName.ValueString() { - resp.Diagnostics.AddError("Нельзя изменить cluster_name", "Параметр задается при создании и не может быть изменен.") - return - } - } - if !plan.ControlPlaneSizingPolicy.IsNull() && !plan.ControlPlaneSizingPolicy.IsUnknown() && !state.ControlPlaneSizingPolicy.IsNull() && !state.ControlPlaneSizingPolicy.IsUnknown() { - if plan.ControlPlaneSizingPolicy.ValueString() != state.ControlPlaneSizingPolicy.ValueString() { - resp.Diagnostics.AddError("Нельзя изменить control_plane_sizing_policy", "Параметр задается при создании и не может быть изменен.") - return - } - } - if !plan.NeedExternalAddressAPI.IsNull() && !plan.NeedExternalAddressAPI.IsUnknown() && !state.NeedExternalAddressAPI.IsNull() && !state.NeedExternalAddressAPI.IsUnknown() { - if plan.NeedExternalAddressAPI.ValueBool() != state.NeedExternalAddressAPI.ValueBool() { - resp.Diagnostics.AddError("Нельзя изменить need_external_address_a_p_i", "Параметр задается при создании и не может быть изменен.") - return - } - } - if !plan.NeedExternalAddressIngress.IsNull() && !plan.NeedExternalAddressIngress.IsUnknown() && !state.NeedExternalAddressIngress.IsNull() && !state.NeedExternalAddressIngress.IsUnknown() { - if plan.NeedExternalAddressIngress.ValueBool() != state.NeedExternalAddressIngress.ValueBool() { - resp.Diagnostics.AddError("Нельзя изменить need_external_address_ingress", "Параметр задается при создании и не может быть изменен.") - return - } - } - if !plan.NsxtUid.IsNull() && !plan.NsxtUid.IsUnknown() && !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() { - if plan.NsxtUid.ValueString() != state.NsxtUid.ValueString() { - resp.Diagnostics.AddError("Нельзя изменить nsxt_uid", "Параметр задается при создании и не может быть изменен.") - return - } - } - if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { - if plan.VdcUid.ValueString() != state.VdcUid.ValueString() { - resp.Diagnostics.AddError("Нельзя изменить vdc_uid", "Параметр задается при создании и не может быть изменен.") - return - } - } - if !plan.WorkersSizingPolicy.IsNull() && !plan.WorkersSizingPolicy.IsUnknown() && !state.WorkersSizingPolicy.IsNull() && !state.WorkersSizingPolicy.IsUnknown() { - if plan.WorkersSizingPolicy.ValueString() != state.WorkersSizingPolicy.ValueString() { - resp.Diagnostics.AddError("Нельзя изменить workers_sizing_policy", "Параметр задается при создании и не может быть изменен.") + if !plan.StartupConfiguration.IsNull() && !plan.StartupConfiguration.IsUnknown() && !state.StartupConfiguration.IsNull() && !state.StartupConfiguration.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.StartupConfiguration.ValueString(), state.StartupConfiguration.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить startup_configuration", "Параметр задается при создании и не может быть изменен.") return } } return } - if config.ControlPlaneSizingPolicy.IsNull() || config.ControlPlaneSizingPolicy.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "control_plane_sizing_policy is required.") + if config.AccessConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "access_configuration is required.") return } - if config.NsxtUid.IsNull() || config.NsxtUid.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "nsxt_uid is required.") + if config.AccessConfiguration.IsUnknown() { return } - if config.VdcUid.IsNull() || config.VdcUid.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "vdc_uid is required.") + if config.ClusterConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "cluster_configuration is required.") return } - if config.WorkersSizingPolicy.IsNull() || config.WorkersSizingPolicy.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "workers_sizing_policy is required.") + if config.ClusterConfiguration.IsUnknown() { + return + } + if config.ControlPlaneConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "control_plane_configuration is required.") + return + } + if config.ControlPlaneConfiguration.IsUnknown() { + return + } + if config.StartupConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "startup_configuration is required.") + return + } + if config.StartupConfiguration.IsUnknown() { return } @@ -227,40 +167,17 @@ func (r *K8sSthutrvalClusterResource) ModifyPlan(ctx context.Context, req resour 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{ - 344: resources_core.FormatString(config.NsxtUid), - 345: resources_core.FormatString(config.ClusterName), - 346: resources_core.FormatString(config.ControlPlaneSizingPolicy), - 347: resources_core.FormatInt64(config.ControlPlaneSizingDisk), - 348: resources_core.FormatInt64(config.ControlPlaneCount), - 349: resources_core.FormatString(config.WorkersSizingPolicy), - 350: resources_core.FormatInt64(config.WorkersSizingDisk), - 351: resources_core.FormatInt64(config.WorkersCount), - 352: resources_core.FormatString(config.AppVersion), - 353: resources_core.FormatBool(config.NeedExternalAddressAPI), - 354: resources_core.FormatString(config.AccessIpListAPI), - 355: resources_core.FormatBool(config.NeedExternalAddressIngress), - 356: resources_core.FormatString(config.AccessIpListIngress), - 747: resources_core.FormatString(config.VdcUid), + 750: resources_core.FormatString(config.StartupConfiguration), + 751: resources_core.FormatString(config.ControlPlaneConfiguration), + 752: resources_core.FormatString(config.WorkerConfiguration), + 753: resources_core.FormatString(config.AccessConfiguration), + 755: resources_core.FormatString(config.ClusterConfiguration), } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} - resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 150, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -269,67 +186,43 @@ func (r *K8sSthutrvalClusterResource) Create(ctx context.Context, req resource.C if resp.Diagnostics.HasError() { return } - if data.ControlPlaneSizingPolicy.IsNull() || data.ControlPlaneSizingPolicy.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "control_plane_sizing_policy is required.") + if data.AccessConfiguration.IsNull() || data.AccessConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "access_configuration is required.") return } - if data.NsxtUid.IsNull() || data.NsxtUid.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "nsxt_uid is required.") + if data.ClusterConfiguration.IsNull() || data.ClusterConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "cluster_configuration is required.") return } - if data.VdcUid.IsNull() || data.VdcUid.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "vdc_uid is required.") + if data.ControlPlaneConfiguration.IsNull() || data.ControlPlaneConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "control_plane_configuration is required.") return } - if data.WorkersSizingPolicy.IsNull() || data.WorkersSizingPolicy.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "workers_sizing_policy is required.") + if data.StartupConfiguration.IsNull() || data.StartupConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "startup_configuration is required.") return } - 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() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 150, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ - 344: resources_core.FormatString(data.NsxtUid), - 345: resources_core.FormatString(data.ClusterName), - 346: resources_core.FormatString(data.ControlPlaneSizingPolicy), - 347: resources_core.FormatInt64(data.ControlPlaneSizingDisk), - 348: resources_core.FormatInt64(data.ControlPlaneCount), - 349: resources_core.FormatString(data.WorkersSizingPolicy), - 350: resources_core.FormatInt64(data.WorkersSizingDisk), - 351: resources_core.FormatInt64(data.WorkersCount), - 352: resources_core.FormatString(data.AppVersion), - 353: resources_core.FormatBool(data.NeedExternalAddressAPI), - 354: resources_core.FormatString(data.AccessIpListAPI), - 355: resources_core.FormatBool(data.NeedExternalAddressIngress), - 356: resources_core.FormatString(data.AccessIpListIngress), - 747: resources_core.FormatString(data.VdcUid), + 750: resources_core.FormatString(data.StartupConfiguration), + 751: resources_core.FormatString(data.ControlPlaneConfiguration), + 752: resources_core.FormatString(data.WorkerConfiguration), + 753: resources_core.FormatString(data.AccessConfiguration), + 755: resources_core.FormatString(data.ClusterConfiguration), } 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()) @@ -347,41 +240,16 @@ func (r *K8sSthutrvalClusterResource) Create(ctx context.Context, req resource.C {Code: "vault_user_path"}, {Code: "vault_fields"}, }, []resources_core.InputField{ - {Code: "accessIpListAPI", Field: "AccessIpListAPI", Type: "string"}, - {Code: "accessIpListIngress", Field: "AccessIpListIngress", Type: "string"}, - {Code: "appVersion", Field: "AppVersion", Type: "string"}, - {Code: "clusterName", Field: "ClusterName", Type: "string"}, - {Code: "controlPlaneCount", Field: "ControlPlaneCount", Type: "int64"}, - {Code: "controlPlaneSizingDisk", Field: "ControlPlaneSizingDisk", Type: "int64"}, - {Code: "controlPlaneSizingPolicy", Field: "ControlPlaneSizingPolicy", Type: "string"}, - {Code: "needExternalAddressAPI", Field: "NeedExternalAddressAPI", Type: "bool"}, - {Code: "needExternalAddressIngress", Field: "NeedExternalAddressIngress", Type: "bool"}, - {Code: "nsxtUid", Field: "NsxtUid", Type: "string"}, - {Code: "vdcUid", Field: "VdcUid", Type: "string"}, - {Code: "workersCount", Field: "WorkersCount", Type: "int64"}, - {Code: "workersSizingDisk", Field: "WorkersSizingDisk", Type: "int64"}, - {Code: "workersSizingPolicy", Field: "WorkersSizingPolicy", Type: "string"}, + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "string"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "string"}, + {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "string"}, + {Code: "workerConfiguration", Field: "WorkerConfiguration", 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) - } - } resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) } @@ -417,41 +285,16 @@ func (r *K8sSthutrvalClusterResource) Read(ctx context.Context, req resource.Rea {Code: "vault_user_path"}, {Code: "vault_fields"}, }, []resources_core.InputField{ - {Code: "accessIpListAPI", Field: "AccessIpListAPI", Type: "string"}, - {Code: "accessIpListIngress", Field: "AccessIpListIngress", Type: "string"}, - {Code: "appVersion", Field: "AppVersion", Type: "string"}, - {Code: "clusterName", Field: "ClusterName", Type: "string"}, - {Code: "controlPlaneCount", Field: "ControlPlaneCount", Type: "int64"}, - {Code: "controlPlaneSizingDisk", Field: "ControlPlaneSizingDisk", Type: "int64"}, - {Code: "controlPlaneSizingPolicy", Field: "ControlPlaneSizingPolicy", Type: "string"}, - {Code: "needExternalAddressAPI", Field: "NeedExternalAddressAPI", Type: "bool"}, - {Code: "needExternalAddressIngress", Field: "NeedExternalAddressIngress", Type: "bool"}, - {Code: "nsxtUid", Field: "NsxtUid", Type: "string"}, - {Code: "vdcUid", Field: "VdcUid", Type: "string"}, - {Code: "workersCount", Field: "WorkersCount", Type: "int64"}, - {Code: "workersSizingDisk", Field: "WorkersSizingDisk", Type: "int64"}, - {Code: "workersSizingPolicy", Field: "WorkersSizingPolicy", Type: "string"}, + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "string"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "string"}, + {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "string"}, + {Code: "workerConfiguration", Field: "WorkerConfiguration", 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) - } - } resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) } @@ -476,64 +319,37 @@ func (r *K8sSthutrvalClusterResource) Update(ctx context.Context, req resource.U hasServiceParamChanges := false if !hasServiceParamChanges { - if plan.ControlPlaneCount.IsNull() != state.ControlPlaneCount.IsNull() || plan.ControlPlaneCount.IsUnknown() != state.ControlPlaneCount.IsUnknown() { + if plan.ClusterConfiguration.IsNull() != state.ClusterConfiguration.IsNull() || plan.ClusterConfiguration.IsUnknown() != state.ClusterConfiguration.IsUnknown() { hasServiceParamChanges = true - } else if !plan.ControlPlaneCount.IsNull() && !plan.ControlPlaneCount.IsUnknown() && !state.ControlPlaneCount.IsNull() && !state.ControlPlaneCount.IsUnknown() { - if plan.ControlPlaneCount.ValueInt64() != state.ControlPlaneCount.ValueInt64() { + } else if !plan.ClusterConfiguration.IsNull() && !plan.ClusterConfiguration.IsUnknown() && !state.ClusterConfiguration.IsNull() && !state.ClusterConfiguration.IsUnknown() { + if !strings.EqualFold(plan.ClusterConfiguration.ValueString(), state.ClusterConfiguration.ValueString()) { hasServiceParamChanges = true } } } if !hasServiceParamChanges { - if plan.WorkersCount.IsNull() != state.WorkersCount.IsNull() || plan.WorkersCount.IsUnknown() != state.WorkersCount.IsUnknown() { + if plan.ControlPlaneConfiguration.IsNull() != state.ControlPlaneConfiguration.IsNull() || plan.ControlPlaneConfiguration.IsUnknown() != state.ControlPlaneConfiguration.IsUnknown() { hasServiceParamChanges = true - } else if !plan.WorkersCount.IsNull() && !plan.WorkersCount.IsUnknown() && !state.WorkersCount.IsNull() && !state.WorkersCount.IsUnknown() { - if plan.WorkersCount.ValueInt64() != state.WorkersCount.ValueInt64() { + } else if !plan.ControlPlaneConfiguration.IsNull() && !plan.ControlPlaneConfiguration.IsUnknown() && !state.ControlPlaneConfiguration.IsNull() && !state.ControlPlaneConfiguration.IsUnknown() { + if !strings.EqualFold(plan.ControlPlaneConfiguration.ValueString(), state.ControlPlaneConfiguration.ValueString()) { hasServiceParamChanges = true } } } if !hasServiceParamChanges { - if plan.AccessIpListAPI.IsNull() != state.AccessIpListAPI.IsNull() || plan.AccessIpListAPI.IsUnknown() != state.AccessIpListAPI.IsUnknown() { + if plan.WorkerConfiguration.IsNull() != state.WorkerConfiguration.IsNull() || plan.WorkerConfiguration.IsUnknown() != state.WorkerConfiguration.IsUnknown() { hasServiceParamChanges = true - } else if !plan.AccessIpListAPI.IsNull() && !plan.AccessIpListAPI.IsUnknown() && !state.AccessIpListAPI.IsNull() && !state.AccessIpListAPI.IsUnknown() { - if plan.AccessIpListAPI.ValueString() != state.AccessIpListAPI.ValueString() { + } 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 { - if plan.AccessIpListIngress.IsNull() != state.AccessIpListIngress.IsNull() || plan.AccessIpListIngress.IsUnknown() != state.AccessIpListIngress.IsUnknown() { + if plan.AccessConfiguration.IsNull() != state.AccessConfiguration.IsNull() || plan.AccessConfiguration.IsUnknown() != state.AccessConfiguration.IsUnknown() { hasServiceParamChanges = true - } else if !plan.AccessIpListIngress.IsNull() && !plan.AccessIpListIngress.IsUnknown() && !state.AccessIpListIngress.IsNull() && !state.AccessIpListIngress.IsUnknown() { - if plan.AccessIpListIngress.ValueString() != state.AccessIpListIngress.ValueString() { - 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 plan.AppVersion.ValueString() != state.AppVersion.ValueString() { - hasServiceParamChanges = true - } - } - } - if !hasServiceParamChanges { - if plan.ControlPlaneSizingDisk.IsNull() != state.ControlPlaneSizingDisk.IsNull() || plan.ControlPlaneSizingDisk.IsUnknown() != state.ControlPlaneSizingDisk.IsUnknown() { - hasServiceParamChanges = true - } else if !plan.ControlPlaneSizingDisk.IsNull() && !plan.ControlPlaneSizingDisk.IsUnknown() && !state.ControlPlaneSizingDisk.IsNull() && !state.ControlPlaneSizingDisk.IsUnknown() { - if plan.ControlPlaneSizingDisk.ValueInt64() != state.ControlPlaneSizingDisk.ValueInt64() { - hasServiceParamChanges = true - } - } - } - if !hasServiceParamChanges { - if plan.WorkersSizingDisk.IsNull() != state.WorkersSizingDisk.IsNull() || plan.WorkersSizingDisk.IsUnknown() != state.WorkersSizingDisk.IsUnknown() { - hasServiceParamChanges = true - } else if !plan.WorkersSizingDisk.IsNull() && !plan.WorkersSizingDisk.IsUnknown() && !state.WorkersSizingDisk.IsNull() && !state.WorkersSizingDisk.IsUnknown() { - if plan.WorkersSizingDisk.ValueInt64() != state.WorkersSizingDisk.ValueInt64() { + } else if !plan.AccessConfiguration.IsNull() && !plan.AccessConfiguration.IsUnknown() && !state.AccessConfiguration.IsNull() && !state.AccessConfiguration.IsUnknown() { + if !strings.EqualFold(plan.AccessConfiguration.ValueString(), state.AccessConfiguration.ValueString()) { hasServiceParamChanges = true } } @@ -554,19 +370,19 @@ func (r *K8sSthutrvalClusterResource) Update(ctx context.Context, req resource.U } params := map[int]string{ - 362: resources_core.FormatInt64(plan.ControlPlaneCount), - 363: resources_core.FormatInt64(plan.WorkersCount), - 364: resources_core.FormatString(plan.AccessIpListAPI), - 365: resources_core.FormatString(plan.AccessIpListIngress), - 563: resources_core.FormatString(plan.AppVersion), - 637: resources_core.FormatInt64(plan.ControlPlaneSizingDisk), - 638: resources_core.FormatInt64(plan.WorkersSizingDisk), + 756: resources_core.FormatString(plan.ClusterConfiguration), + 757: resources_core.FormatString(plan.ControlPlaneConfiguration), + 758: resources_core.FormatString(plan.WorkerConfiguration), + 759: resources_core.FormatString(plan.AccessConfiguration), } operationTimeout := "" if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -583,41 +399,16 @@ func (r *K8sSthutrvalClusterResource) Update(ctx context.Context, req resource.U {Code: "vault_user_path"}, {Code: "vault_fields"}, }, []resources_core.InputField{ - {Code: "accessIpListAPI", Field: "AccessIpListAPI", Type: "string"}, - {Code: "accessIpListIngress", Field: "AccessIpListIngress", Type: "string"}, - {Code: "appVersion", Field: "AppVersion", Type: "string"}, - {Code: "clusterName", Field: "ClusterName", Type: "string"}, - {Code: "controlPlaneCount", Field: "ControlPlaneCount", Type: "int64"}, - {Code: "controlPlaneSizingDisk", Field: "ControlPlaneSizingDisk", Type: "int64"}, - {Code: "controlPlaneSizingPolicy", Field: "ControlPlaneSizingPolicy", Type: "string"}, - {Code: "needExternalAddressAPI", Field: "NeedExternalAddressAPI", Type: "bool"}, - {Code: "needExternalAddressIngress", Field: "NeedExternalAddressIngress", Type: "bool"}, - {Code: "nsxtUid", Field: "NsxtUid", Type: "string"}, - {Code: "vdcUid", Field: "VdcUid", Type: "string"}, - {Code: "workersCount", Field: "WorkersCount", Type: "int64"}, - {Code: "workersSizingDisk", Field: "WorkersSizingDisk", Type: "int64"}, - {Code: "workersSizingPolicy", Field: "WorkersSizingPolicy", Type: "string"}, + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "string"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "string"}, + {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "string"}, + {Code: "workerConfiguration", Field: "WorkerConfiguration", 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) - } - } resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) } @@ -631,7 +422,6 @@ func (r *K8sSthutrvalClusterResource) Delete(ctx context.Context, req resource.D if state.ID.IsNull() || state.ID.IsUnknown() { return } - deleteMode := "state_only" if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { deleteMode = "suspend" @@ -641,6 +431,9 @@ func (r *K8sSthutrvalClusterResource) Delete(ctx context.Context, req resource.D 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 diff --git a/universal_rebuild/internal/resources_gen/150_k8s_sthutrval_cluster_user_resource.go b/universal_rebuild/internal/resources_gen/150_k8s_sthutrval_cluster_user_resource.go new file mode 100644 index 0000000..7dbdeb4 --- /dev/null +++ b/universal_rebuild/internal/resources_gen/150_k8s_sthutrval_cluster_user_resource.go @@ -0,0 +1,347 @@ +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(false)}, + "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 known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект подтверждён в state_out, выполняется усыновление") + 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 { + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект отсутствует и будет пропущен: "+targetValue) + return + } + resp.Diagnostics.AddError("Подресурс не найден", "Объект отсутствует: "+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/universal_rebuild/internal/resources_gen/19_vc_org_reconcile_action.go b/universal_rebuild/internal/resources_gen/19_vc_org_reconcile_action.go index e8a2d39..a8fd701 100644 --- a/universal_rebuild/internal/resources_gen/19_vc_org_reconcile_action.go +++ b/universal_rebuild/internal/resources_gen/19_vc_org_reconcile_action.go @@ -9,6 +9,7 @@ import ( "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/types" ) @@ -24,10 +25,12 @@ type VcOrgReconcileResource struct { } type VcOrgReconcileModel struct { - ID types.String `tfsdk:"id"` - VcOrgID types.String `tfsdk:"vc_org_id"` - RunID types.String `tfsdk:"run_id"` - OperationTimeout types.String `tfsdk:"operation_timeout"` + ID types.String `tfsdk:"id"` + VcOrgID types.String `tfsdk:"vc_org_id"` + RunID types.String `tfsdk:"run_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` } func NewVcOrgReconcileResource() resource.Resource { @@ -40,10 +43,12 @@ func (r *VcOrgReconcileResource) Metadata(ctx context.Context, req resource.Meta func (r *VcOrgReconcileResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, - "vc_org_id": schema.StringAttribute{Required: true}, - "run_id": schema.StringAttribute{Required: true}, - "operation_timeout": schema.StringAttribute{Optional: true}, + "id": schema.StringAttribute{Computed: true}, + "vc_org_id": schema.StringAttribute{Required: true}, + "run_id": schema.StringAttribute{Required: true}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "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} } @@ -68,6 +73,9 @@ func (r *VcOrgReconcileResource) Create(ctx context.Context, req resource.Create if !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, "reconcile", params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return diff --git a/universal_rebuild/internal/resources_gen/19_vc_org_resource.go b/universal_rebuild/internal/resources_gen/19_vc_org_resource.go index de654c8..15f3d2b 100644 --- a/universal_rebuild/internal/resources_gen/19_vc_org_resource.go +++ b/universal_rebuild/internal/resources_gen/19_vc_org_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -10,7 +11,9 @@ import ( "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" ) @@ -30,6 +33,7 @@ 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"` @@ -55,9 +59,10 @@ func (r *VcOrgResource) Metadata(ctx context.Context, req resource.MetadataReque func (r *VcOrgResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "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{Required: true, MarkdownDescription: "Облако в котором будет развёрнут тенант Не изменяется после создания."}, "v_i_p_configure": schema.StringAttribute{Required: true}, @@ -112,27 +117,39 @@ func (r *VcOrgResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanR } } if !plan.OrganizationType.IsNull() && !plan.OrganizationType.IsUnknown() && !state.OrganizationType.IsNull() && !state.OrganizationType.IsUnknown() { - if plan.OrganizationType.ValueString() != state.OrganizationType.ValueString() { + // 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() { - if plan.ResourceRealm.ValueString() != state.ResourceRealm.ValueString() { + // 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() || config.ResourceRealm.IsUnknown() { + if config.ResourceRealm.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") return } - if config.VIPConfigure.IsNull() || config.VIPConfigure.IsUnknown() { + if config.ResourceRealm.IsUnknown() { + 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 @@ -145,8 +162,10 @@ func (r *VcOrgResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanR 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.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 19, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -165,7 +184,9 @@ func (r *VcOrgResource) Create(ctx context.Context, req resource.CreateRequest, } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 19, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ 418: resources_core.FormatString(data.ResourceRealm), @@ -176,6 +197,9 @@ func (r *VcOrgResource) Create(ctx context.Context, req resource.CreateRequest, 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()) @@ -271,7 +295,7 @@ func (r *VcOrgResource) Update(ctx context.Context, req resource.UpdateRequest, 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 plan.VIPConfigure.ValueString() != state.VIPConfigure.ValueString() { + if !strings.EqualFold(plan.VIPConfigure.ValueString(), state.VIPConfigure.ValueString()) { hasServiceParamChanges = true } } @@ -299,6 +323,9 @@ func (r *VcOrgResource) Update(ctx context.Context, req resource.UpdateRequest, if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -336,7 +363,6 @@ func (r *VcOrgResource) Delete(ctx context.Context, req resource.DeleteRequest, if state.ID.IsNull() || state.ID.IsUnknown() { return } - deleteMode := "state_only" if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { deleteMode = "suspend" @@ -346,6 +372,9 @@ func (r *VcOrgResource) Delete(ctx context.Context, req resource.DeleteRequest, 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 diff --git a/universal_rebuild/internal/resources_gen/19_vc_org_user_resource.go b/universal_rebuild/internal/resources_gen/19_vc_org_user_resource.go new file mode 100644 index 0000000..2c8a9a0 --- /dev/null +++ b/universal_rebuild/internal/resources_gen/19_vc_org_user_resource.go @@ -0,0 +1,347 @@ +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(false)}, + "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 known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект подтверждён в state_out, выполняется усыновление") + 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 { + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект отсутствует и будет пропущен: "+targetValue) + return + } + resp.Diagnostics.AddError("Подресурс не найден", "Объект отсутствует: "+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/universal_rebuild/internal/resources_gen/1_dummy_reconcile_action.go b/universal_rebuild/internal/resources_gen/1_dummy_reconcile_action.go new file mode 100644 index 0000000..e373bc2 --- /dev/null +++ b/universal_rebuild/internal/resources_gen/1_dummy_reconcile_action.go @@ -0,0 +1,136 @@ +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/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: dummy +// Action: reconcile + +var _ resource.Resource = &DummyReconcileResource{} + +// DummyReconcileResource triggers a one-time action. +type DummyReconcileResource struct { + client *core.UniversalClient +} + +type DummyReconcileModel struct { + ID types.String `tfsdk:"id"` + DummyID types.String `tfsdk:"dummy_id"` + RunID types.String `tfsdk:"run_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` +} + +func NewDummyReconcileResource() resource.Resource { + return &DummyReconcileResource{} +} + +func (r *DummyReconcileResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dummy_reconcile" +} + +func (r *DummyReconcileResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true}, + "dummy_id": schema.StringAttribute{Required: true}, + "run_id": schema.StringAttribute{Required: true}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "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 *DummyReconcileResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan DummyReconcileModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.DummyID.ValueString()) + runID := strings.TrimSpace(plan.RunID.ValueString()) + if instanceUID == "" || runID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра или run_id") + return + } + + params := 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, "reconcile", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "reconcile", runID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *DummyReconcileResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state DummyReconcileModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *DummyReconcileResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan DummyReconcileModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.DummyID.ValueString()) + runID := strings.TrimSpace(plan.RunID.ValueString()) + if instanceUID == "" || runID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра или run_id") + return + } + + params := map[string]string{} + + if err := resources_core.RunOperationByCode(ctx, r.client, instanceUID, "reconcile", params); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "reconcile", runID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *DummyReconcileResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // No-op: removing the resource does not trigger a remote action. +} + +func (r *DummyReconcileResource) 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/universal_rebuild/internal/resources_gen/1_dummy_redeploy_action.go b/universal_rebuild/internal/resources_gen/1_dummy_redeploy_action.go index 5236732..60df6bb 100644 --- a/universal_rebuild/internal/resources_gen/1_dummy_redeploy_action.go +++ b/universal_rebuild/internal/resources_gen/1_dummy_redeploy_action.go @@ -9,6 +9,7 @@ import ( "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/types" ) @@ -24,10 +25,12 @@ type DummyRedeployResource struct { } type DummyRedeployModel struct { - ID types.String `tfsdk:"id"` - DummyID types.String `tfsdk:"dummy_id"` - RunID types.String `tfsdk:"run_id"` - OperationTimeout types.String `tfsdk:"operation_timeout"` + ID types.String `tfsdk:"id"` + DummyID types.String `tfsdk:"dummy_id"` + RunID types.String `tfsdk:"run_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` } func NewDummyRedeployResource() resource.Resource { @@ -40,10 +43,12 @@ func (r *DummyRedeployResource) Metadata(ctx context.Context, req resource.Metad func (r *DummyRedeployResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, - "dummy_id": schema.StringAttribute{Required: true}, - "run_id": schema.StringAttribute{Required: true}, - "operation_timeout": schema.StringAttribute{Optional: true}, + "id": schema.StringAttribute{Computed: true}, + "dummy_id": schema.StringAttribute{Required: true}, + "run_id": schema.StringAttribute{Required: true}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "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} } @@ -68,6 +73,9 @@ func (r *DummyRedeployResource) Create(ctx context.Context, req resource.CreateR if !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, "redeploy", params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return diff --git a/universal_rebuild/internal/resources_gen/1_dummy_resource.go b/universal_rebuild/internal/resources_gen/1_dummy_resource.go index 3f461b3..b2b4104 100644 --- a/universal_rebuild/internal/resources_gen/1_dummy_resource.go +++ b/universal_rebuild/internal/resources_gen/1_dummy_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -11,6 +12,8 @@ import ( "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" ) @@ -30,6 +33,8 @@ 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"` + Arr types.String `tfsdk:"arr"` ArrayMapFixedExample types.String `tfsdk:"array_map_fixed_example"` Bodymessage types.String `tfsdk:"bodymessage"` DurationMs types.Int64 `tfsdk:"duration_ms"` @@ -41,7 +46,6 @@ type DummyModel struct { NestedRefExample types.String `tfsdk:"nested_ref_example"` ResourceRealm types.String `tfsdk:"resource_realm"` WhereFail types.Int64 `tfsdk:"where_fail"` - YamlExample types.String `tfsdk:"yaml_example"` SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` StateParams types.Map `tfsdk:"state_params"` @@ -64,21 +68,22 @@ func (r *DummyResource) Metadata(ctx context.Context, req resource.MetadataReque func (r *DummyResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "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, MarkdownDescription: "Для тестирования интерфейса"}, + "json_example": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Для тестирования интерфейса", PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()}}, "map_example": schema.StringAttribute{Optional: true}, "map_fixed": schema.StringAttribute{Optional: true, 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"}, - "yaml_example": schema.StringAttribute{Optional: 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}, @@ -129,42 +134,67 @@ func (r *DummyResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanR 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() { - if plan.ArrayMapFixedExample.ValueString() != state.ArrayMapFixedExample.ValueString() { + // 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.MapFixed.IsNull() && !plan.MapFixed.IsUnknown() && !state.MapFixed.IsNull() && !state.MapFixed.IsUnknown() { - if plan.MapFixed.ValueString() != state.MapFixed.ValueString() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.MapFixed.ValueString(), state.MapFixed.ValueString()) { resp.Diagnostics.AddError("Нельзя изменить map_fixed", "Параметр задается при создании и не может быть изменен.") return } } if !plan.NestedRefExample.IsNull() && !plan.NestedRefExample.IsUnknown() && !state.NestedRefExample.IsNull() && !state.NestedRefExample.IsUnknown() { - if plan.NestedRefExample.ValueString() != state.NestedRefExample.ValueString() { + // 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() { - if plan.ResourceRealm.ValueString() != state.ResourceRealm.ValueString() { + // 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.YamlExample.IsNull() && !plan.YamlExample.IsUnknown() && !state.YamlExample.IsNull() && !state.YamlExample.IsUnknown() { - if plan.YamlExample.ValueString() != state.YamlExample.ValueString() { - resp.Diagnostics.AddError("Нельзя изменить yaml_example", "Параметр задается при создании и не может быть изменен.") - return - } - } return } - if config.ResourceRealm.IsNull() || config.ResourceRealm.IsUnknown() { + 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 @@ -183,12 +213,14 @@ func (r *DummyResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanR 321: resources_core.FormatString(config.MapExample), 322: resources_core.FormatString(config.JsonExample), 396: resources_core.FormatString(config.NestedRefExample), - 447: resources_core.FormatString(config.YamlExample), 647: resources_core.FormatString(config.MapFixed), 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.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 1, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -197,13 +229,19 @@ func (r *DummyResource) Create(ctx context.Context, req resource.CreateRequest, 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() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 1, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ 198: resources_core.FormatInt64(data.DurationMs), @@ -215,15 +253,18 @@ func (r *DummyResource) Create(ctx context.Context, req resource.CreateRequest, 321: resources_core.FormatString(data.MapExample), 322: resources_core.FormatString(data.JsonExample), 396: resources_core.FormatString(data.NestedRefExample), - 447: resources_core.FormatString(data.YamlExample), 647: resources_core.FormatString(data.MapFixed), 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()) @@ -241,6 +282,7 @@ func (r *DummyResource) Create(ctx context.Context, req resource.CreateRequest, {Code: "vault_user_path"}, {Code: "vault_fields"}, }, []resources_core.InputField{ + {Code: "arr", Field: "Arr", Type: "string"}, {Code: "arrayMapFixedExample", Field: "ArrayMapFixedExample", Type: "string"}, {Code: "bodymessage", Field: "Bodymessage", Type: "string"}, {Code: "durationMs", Field: "DurationMs", Type: "int64"}, @@ -252,7 +294,6 @@ func (r *DummyResource) Create(ctx context.Context, req resource.CreateRequest, {Code: "nestedRefExample", Field: "NestedRefExample", Type: "string"}, {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, {Code: "whereFail", Field: "WhereFail", Type: "int64"}, - {Code: "yamlExample", Field: "YamlExample", Type: "string"}, }) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -293,6 +334,7 @@ func (r *DummyResource) Read(ctx context.Context, req resource.ReadRequest, resp {Code: "vault_user_path"}, {Code: "vault_fields"}, }, []resources_core.InputField{ + {Code: "arr", Field: "Arr", Type: "string"}, {Code: "arrayMapFixedExample", Field: "ArrayMapFixedExample", Type: "string"}, {Code: "bodymessage", Field: "Bodymessage", Type: "string"}, {Code: "durationMs", Field: "DurationMs", Type: "int64"}, @@ -304,7 +346,6 @@ func (r *DummyResource) Read(ctx context.Context, req resource.ReadRequest, resp {Code: "nestedRefExample", Field: "NestedRefExample", Type: "string"}, {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, {Code: "whereFail", Field: "WhereFail", Type: "int64"}, - {Code: "yamlExample", Field: "YamlExample", Type: "string"}, }) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -373,7 +414,7 @@ func (r *DummyResource) Update(ctx context.Context, req resource.UpdateRequest, 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 plan.Bodymessage.ValueString() != state.Bodymessage.ValueString() { + if !strings.EqualFold(plan.Bodymessage.ValueString(), state.Bodymessage.ValueString()) { hasServiceParamChanges = true } } @@ -382,7 +423,7 @@ func (r *DummyResource) Update(ctx context.Context, req resource.UpdateRequest, if plan.MapExample.IsNull() != state.MapExample.IsNull() || plan.MapExample.IsUnknown() != state.MapExample.IsUnknown() { hasServiceParamChanges = true } else if !plan.MapExample.IsNull() && !plan.MapExample.IsUnknown() && !state.MapExample.IsNull() && !state.MapExample.IsUnknown() { - if plan.MapExample.ValueString() != state.MapExample.ValueString() { + if !strings.EqualFold(plan.MapExample.ValueString(), state.MapExample.ValueString()) { hasServiceParamChanges = true } } @@ -391,7 +432,7 @@ func (r *DummyResource) Update(ctx context.Context, req resource.UpdateRequest, 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 plan.JsonExample.ValueString() != state.JsonExample.ValueString() { + if !strings.EqualFold(plan.JsonExample.ValueString(), state.JsonExample.ValueString()) { hasServiceParamChanges = true } } @@ -425,6 +466,9 @@ func (r *DummyResource) Update(ctx context.Context, req resource.UpdateRequest, if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -441,6 +485,7 @@ func (r *DummyResource) Update(ctx context.Context, req resource.UpdateRequest, {Code: "vault_user_path"}, {Code: "vault_fields"}, }, []resources_core.InputField{ + {Code: "arr", Field: "Arr", Type: "string"}, {Code: "arrayMapFixedExample", Field: "ArrayMapFixedExample", Type: "string"}, {Code: "bodymessage", Field: "Bodymessage", Type: "string"}, {Code: "durationMs", Field: "DurationMs", Type: "int64"}, @@ -452,7 +497,6 @@ func (r *DummyResource) Update(ctx context.Context, req resource.UpdateRequest, {Code: "nestedRefExample", Field: "NestedRefExample", Type: "string"}, {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, {Code: "whereFail", Field: "WhereFail", Type: "int64"}, - {Code: "yamlExample", Field: "YamlExample", Type: "string"}, }) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -471,7 +515,6 @@ func (r *DummyResource) Delete(ctx context.Context, req resource.DeleteRequest, if state.ID.IsNull() || state.ID.IsUnknown() { return } - deleteMode := "state_only" if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { deleteMode = "suspend" @@ -481,6 +524,9 @@ func (r *DummyResource) Delete(ctx context.Context, req resource.DeleteRequest, 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 diff --git a/universal_rebuild/internal/resources_gen/20_vc_org_saas_resource.go b/universal_rebuild/internal/resources_gen/20_vc_org_saas_resource.go index 87fa0eb..319d7a4 100644 --- a/universal_rebuild/internal/resources_gen/20_vc_org_saas_resource.go +++ b/universal_rebuild/internal/resources_gen/20_vc_org_saas_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -10,6 +11,8 @@ import ( "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" ) @@ -29,6 +32,7 @@ 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"` @@ -52,9 +56,10 @@ func (r *VcOrgSaasResource) Metadata(ctx context.Context, req resource.MetadataR func (r *VcOrgSaasResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "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: "Платформа для развертывания Не изменяется после создания."}, "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)}, @@ -107,17 +112,23 @@ func (r *VcOrgSaasResource) ModifyPlan(ctx context.Context, req resource.ModifyP } } if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { - if plan.ResourceRealm.ValueString() != state.ResourceRealm.ValueString() { + // 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() || config.ResourceRealm.IsUnknown() { + 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 @@ -129,8 +140,10 @@ func (r *VcOrgSaasResource) ModifyPlan(ctx context.Context, req resource.ModifyP 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.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 20, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -145,7 +158,9 @@ func (r *VcOrgSaasResource) Create(ctx context.Context, req resource.CreateReque } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 20, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ 185: resources_core.FormatString(data.ResourceRealm), @@ -155,6 +170,9 @@ func (r *VcOrgSaasResource) Create(ctx context.Context, req resource.CreateReque 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()) @@ -263,6 +281,9 @@ func (r *VcOrgSaasResource) Update(ctx context.Context, req resource.UpdateReque if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -298,7 +319,6 @@ func (r *VcOrgSaasResource) Delete(ctx context.Context, req resource.DeleteReque if state.ID.IsNull() || state.ID.IsUnknown() { return } - deleteMode := "state_only" if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { deleteMode = "suspend" @@ -308,6 +328,9 @@ func (r *VcOrgSaasResource) Delete(ctx context.Context, req resource.DeleteReque 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 diff --git a/universal_rebuild/internal/resources_gen/21_vc_vdc_reconcile_action.go b/universal_rebuild/internal/resources_gen/21_vc_vdc_reconcile_action.go index 0725e11..29801da 100644 --- a/universal_rebuild/internal/resources_gen/21_vc_vdc_reconcile_action.go +++ b/universal_rebuild/internal/resources_gen/21_vc_vdc_reconcile_action.go @@ -9,6 +9,7 @@ import ( "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/types" ) @@ -24,10 +25,12 @@ type VcVdcReconcileResource struct { } type VcVdcReconcileModel struct { - ID types.String `tfsdk:"id"` - VcVdcID types.String `tfsdk:"vc_vdc_id"` - RunID types.String `tfsdk:"run_id"` - OperationTimeout types.String `tfsdk:"operation_timeout"` + ID types.String `tfsdk:"id"` + VcVdcID types.String `tfsdk:"vc_vdc_id"` + RunID types.String `tfsdk:"run_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` } func NewVcVdcReconcileResource() resource.Resource { @@ -40,10 +43,12 @@ func (r *VcVdcReconcileResource) Metadata(ctx context.Context, req resource.Meta func (r *VcVdcReconcileResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, - "vc_vdc_id": schema.StringAttribute{Required: true}, - "run_id": schema.StringAttribute{Required: true}, - "operation_timeout": schema.StringAttribute{Optional: true}, + "id": schema.StringAttribute{Computed: true}, + "vc_vdc_id": schema.StringAttribute{Required: true}, + "run_id": schema.StringAttribute{Required: true}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "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} } @@ -68,6 +73,9 @@ func (r *VcVdcReconcileResource) Create(ctx context.Context, req resource.Create if !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, "reconcile", params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return diff --git a/universal_rebuild/internal/resources_gen/21_vc_vdc_resource.go b/universal_rebuild/internal/resources_gen/21_vc_vdc_resource.go index b122325..3df7878 100644 --- a/universal_rebuild/internal/resources_gen/21_vc_vdc_resource.go +++ b/universal_rebuild/internal/resources_gen/21_vc_vdc_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -11,6 +12,9 @@ import ( "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" ) @@ -30,10 +34,10 @@ 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"` - MemGuaranteed types.Int64 `tfsdk:"mem_guaranteed"` NetworkProvider types.String `tfsdk:"network_provider"` OrganizationUid types.String `tfsdk:"organization_uid"` ProviderVdc types.String `tfsdk:"provider_vdc"` @@ -60,17 +64,17 @@ func (r *VcVdcResource) Metadata(ctx context.Context, req resource.MetadataReque func (r *VcVdcResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "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"}, - "mem_guaranteed": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(20), MarkdownDescription: "% резервирования Не изменяется после создания."}, "network_provider": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания."}, - "organization_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Для создания VDC предварительно необходимо создать организацию через услугу `Организация в Cloud Director` Не изменяется после создания."}, + "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{Required: true, MarkdownDescription: "Первая политика в списке указывается как "По умолчанию""}, + "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}, @@ -115,22 +119,11 @@ func (r *VcVdcResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanR if resp.Diagnostics.HasError() { return } - planChanged := false - if !plan.OrganizationUid.IsNull() && !plan.OrganizationUid.IsUnknown() { - resolvedOrganizationUid, err := r.client.ResolveRefSvcParamValue(ctx, 19, plan.OrganizationUid.ValueString()) - if err != nil { - resp.Diagnostics.AddWarning("Failed to resolve organization_uid", err.Error()) - } else if resolvedOrganizationUid != "" && resolvedOrganizationUid != plan.OrganizationUid.ValueString() { - plan.OrganizationUid = types.StringValue(resolvedOrganizationUid) - planChanged = true - } - } - if planChanged { - resp.Diagnostics.Append(resp.Plan.Set(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 задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") @@ -143,46 +136,54 @@ func (r *VcVdcResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanR 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.NetworkProvider.IsNull() && !plan.NetworkProvider.IsUnknown() && !state.NetworkProvider.IsNull() && !state.NetworkProvider.IsUnknown() { - if plan.NetworkProvider.ValueString() != state.NetworkProvider.ValueString() { + // 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() { - if plan.OrganizationUid.ValueString() != state.OrganizationUid.ValueString() { + // 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() { - if plan.ProviderVdc.ValueString() != state.ProviderVdc.ValueString() { + // 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() || config.NetworkProvider.IsUnknown() { + if config.NetworkProvider.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "network_provider is required.") return } - if config.OrganizationUid.IsNull() || config.OrganizationUid.IsUnknown() { + if config.NetworkProvider.IsUnknown() { + return + } + if config.OrganizationUid.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "organization_uid is required.") return } - if config.ProviderVdc.IsNull() || config.ProviderVdc.IsUnknown() { + if config.OrganizationUid.IsUnknown() { + return + } + if config.ProviderVdc.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "provider_vdc is required.") return } - if config.StorageConfig.IsNull() || config.StorageConfig.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "storage_config is required.") + if config.ProviderVdc.IsUnknown() { return } @@ -206,13 +207,14 @@ func (r *VcVdcResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanR 335: resources_core.FormatString(config.ProviderVdc), 361: resources_core.FormatString(config.StorageConfig), 397: resources_core.FormatInt64(config.CpuGuaranteed), - 398: resources_core.FormatInt64(config.MemGuaranteed), 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.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 21, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -233,10 +235,24 @@ func (r *VcVdcResource) Create(ctx context.Context, req resource.CreateRequest, resp.Diagnostics.AddError("Missing required attribute", "provider_vdc is required.") return } - if data.StorageConfig.IsNull() || data.StorageConfig.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "storage_config 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 { @@ -249,14 +265,15 @@ func (r *VcVdcResource) Create(ctx context.Context, req resource.CreateRequest, } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 21, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) 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), - 398: resources_core.FormatInt64(data.MemGuaranteed), 557: resources_core.FormatInt64(data.CpuAllocated), 558: resources_core.FormatInt64(data.MemAllocated), 746: resources_core.FormatString(data.NetworkProvider), @@ -266,6 +283,9 @@ func (r *VcVdcResource) Create(ctx context.Context, req resource.CreateRequest, 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()) @@ -286,7 +306,6 @@ func (r *VcVdcResource) Create(ctx context.Context, req resource.CreateRequest, {Code: "cpuAllocated", Field: "CpuAllocated", Type: "int64"}, {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"}, {Code: "memAllocated", Field: "MemAllocated", Type: "int64"}, - {Code: "memGuaranteed", Field: "MemGuaranteed", Type: "int64"}, {Code: "networkProvider", Field: "NetworkProvider", Type: "string"}, {Code: "organizationUid", Field: "OrganizationUid", Type: "string"}, {Code: "providerVdc", Field: "ProviderVdc", Type: "string"}, @@ -304,6 +323,16 @@ func (r *VcVdcResource) Create(ctx context.Context, req resource.CreateRequest, 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)...) } @@ -342,7 +371,6 @@ func (r *VcVdcResource) Read(ctx context.Context, req resource.ReadRequest, resp {Code: "cpuAllocated", Field: "CpuAllocated", Type: "int64"}, {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"}, {Code: "memAllocated", Field: "MemAllocated", Type: "int64"}, - {Code: "memGuaranteed", Field: "MemGuaranteed", Type: "int64"}, {Code: "networkProvider", Field: "NetworkProvider", Type: "string"}, {Code: "organizationUid", Field: "OrganizationUid", Type: "string"}, {Code: "providerVdc", Field: "ProviderVdc", Type: "string"}, @@ -360,6 +388,16 @@ func (r *VcVdcResource) Read(ctx context.Context, req resource.ReadRequest, resp 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)...) } @@ -405,7 +443,7 @@ func (r *VcVdcResource) Update(ctx context.Context, req resource.UpdateRequest, 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 plan.StorageConfig.ValueString() != state.StorageConfig.ValueString() { + if !strings.EqualFold(plan.StorageConfig.ValueString(), state.StorageConfig.ValueString()) { hasServiceParamChanges = true } } @@ -435,6 +473,9 @@ func (r *VcVdcResource) Update(ctx context.Context, req resource.UpdateRequest, if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -454,7 +495,6 @@ func (r *VcVdcResource) Update(ctx context.Context, req resource.UpdateRequest, {Code: "cpuAllocated", Field: "CpuAllocated", Type: "int64"}, {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"}, {Code: "memAllocated", Field: "MemAllocated", Type: "int64"}, - {Code: "memGuaranteed", Field: "MemGuaranteed", Type: "int64"}, {Code: "networkProvider", Field: "NetworkProvider", Type: "string"}, {Code: "organizationUid", Field: "OrganizationUid", Type: "string"}, {Code: "providerVdc", Field: "ProviderVdc", Type: "string"}, @@ -472,6 +512,15 @@ func (r *VcVdcResource) Update(ctx context.Context, req resource.UpdateRequest, 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)...) } @@ -485,7 +534,6 @@ func (r *VcVdcResource) Delete(ctx context.Context, req resource.DeleteRequest, if state.ID.IsNull() || state.ID.IsUnknown() { return } - deleteMode := "state_only" if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { deleteMode = "suspend" @@ -495,6 +543,9 @@ func (r *VcVdcResource) Delete(ctx context.Context, req resource.DeleteRequest, 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 diff --git a/universal_rebuild/internal/resources_gen/22_vc_nsxt_reconcile_action.go b/universal_rebuild/internal/resources_gen/22_vc_nsxt_reconcile_action.go index 32068dd..d04cbfa 100644 --- a/universal_rebuild/internal/resources_gen/22_vc_nsxt_reconcile_action.go +++ b/universal_rebuild/internal/resources_gen/22_vc_nsxt_reconcile_action.go @@ -9,6 +9,7 @@ import ( "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/types" ) @@ -24,10 +25,12 @@ type VcNsxtReconcileResource struct { } type VcNsxtReconcileModel struct { - ID types.String `tfsdk:"id"` - VcNsxtID types.String `tfsdk:"vc_nsxt_id"` - RunID types.String `tfsdk:"run_id"` - OperationTimeout types.String `tfsdk:"operation_timeout"` + ID types.String `tfsdk:"id"` + VcNsxtID types.String `tfsdk:"vc_nsxt_id"` + RunID types.String `tfsdk:"run_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` } func NewVcNsxtReconcileResource() resource.Resource { @@ -40,10 +43,12 @@ func (r *VcNsxtReconcileResource) Metadata(ctx context.Context, req resource.Met func (r *VcNsxtReconcileResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, - "vc_nsxt_id": schema.StringAttribute{Required: true}, - "run_id": schema.StringAttribute{Required: true}, - "operation_timeout": schema.StringAttribute{Optional: true}, + "id": schema.StringAttribute{Computed: true}, + "vc_nsxt_id": schema.StringAttribute{Required: true}, + "run_id": schema.StringAttribute{Required: true}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "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} } @@ -68,6 +73,9 @@ func (r *VcNsxtReconcileResource) Create(ctx context.Context, req resource.Creat if !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, "reconcile", params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return diff --git a/universal_rebuild/internal/resources_gen/22_vc_nsxt_resource.go b/universal_rebuild/internal/resources_gen/22_vc_nsxt_resource.go index c2c3dcb..3f9e654 100644 --- a/universal_rebuild/internal/resources_gen/22_vc_nsxt_resource.go +++ b/universal_rebuild/internal/resources_gen/22_vc_nsxt_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -11,7 +12,9 @@ import ( "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" ) @@ -28,26 +31,27 @@ type VcNsxtResource struct { } type VcNsxtModel struct { - ID types.String `tfsdk:"id"` - ResourceName types.String `tfsdk:"resource_name"` - OperationTimeout types.String `tfsdk:"operation_timeout"` - IpSpaceName types.String `tfsdk:"ip_space_name"` - NeedEnableAVI types.Bool `tfsdk:"need_enable_a_v_i"` - NeedExternalAddressSNAT types.Bool `tfsdk:"need_external_address_s_n_a_t"` - 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"` + 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 { @@ -60,26 +64,27 @@ func (r *VcNsxtResource) Metadata(ctx context.Context, req resource.MetadataRequ func (r *VcNsxtResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, - "resource_name": schema.StringAttribute{Required: true}, - "operation_timeout": schema.StringAttribute{Optional: true}, - "ip_space_name": schema.StringAttribute{Optional: true, MarkdownDescription: "Имя ip Space для внешнего IP

getKeyListFromStruct(getInstanceState(getValueFromStruct(getInstanceState(getValueFromStruct(getInstanceState(), "params.vdcUid")), "params.organizationUid")), "params.vIPConfigure")"}, - "need_enable_a_v_i": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включение Load Balancer"}, - "need_external_address_s_n_a_t": schema.BoolAttribute{Optional: true, MarkdownDescription: "Создать правило SNAT на подсеть `10.10.102.0/24`"}, - "vdc_group_uid": schema.StringAttribute{Optional: true, MarkdownDescription: "UUID Услуги `Группа виртуальных датацентров (groupvDC)` Не изменяется после создания."}, - "vdc_type": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("vdc"), MarkdownDescription: "Тип родительской услуги Не изменяется после создания."}, - "vdc_uid": schema.StringAttribute{Optional: 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}, + "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} @@ -114,31 +119,11 @@ func (r *VcNsxtResource) ModifyPlan(ctx context.Context, req resource.ModifyPlan if resp.Diagnostics.HasError() { return } - planChanged := false - if !plan.VdcGroupUid.IsNull() && !plan.VdcGroupUid.IsUnknown() { - resolvedVdcGroupUid, err := r.client.ResolveRefSvcParamValue(ctx, 29, plan.VdcGroupUid.ValueString()) - if err != nil { - resp.Diagnostics.AddWarning("Failed to resolve vdc_group_uid", err.Error()) - } else if resolvedVdcGroupUid != "" && resolvedVdcGroupUid != plan.VdcGroupUid.ValueString() { - plan.VdcGroupUid = types.StringValue(resolvedVdcGroupUid) - planChanged = true - } - } - if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() { - resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, plan.VdcUid.ValueString()) - if err != nil { - resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) - } else if resolvedVdcUid != "" && resolvedVdcUid != plan.VdcUid.ValueString() { - plan.VdcUid = types.StringValue(resolvedVdcUid) - planChanged = true - } - } - if planChanged { - resp.Diagnostics.Append(resp.Plan.Set(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 задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") @@ -146,19 +131,28 @@ func (r *VcNsxtResource) ModifyPlan(ctx context.Context, req resource.ModifyPlan } } if !plan.VdcGroupUid.IsNull() && !plan.VdcGroupUid.IsUnknown() && !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() { - if plan.VdcGroupUid.ValueString() != state.VdcGroupUid.ValueString() { + // 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() { - if plan.VdcType.ValueString() != state.VdcType.ValueString() { + // 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() { - if plan.VdcUid.ValueString() != state.VdcUid.ValueString() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VdcUid.ValueString(), state.VdcUid.ValueString()) { resp.Diagnostics.AddError("Нельзя изменить vdc_uid", "Параметр задается при создании и не может быть изменен.") return } @@ -195,9 +189,12 @@ func (r *VcNsxtResource) ModifyPlan(ctx context.Context, req resource.ModifyPlan 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.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 22, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -206,6 +203,42 @@ func (r *VcNsxtResource) Create(ctx context.Context, req resource.CreateRequest, 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 { @@ -228,7 +261,9 @@ func (r *VcNsxtResource) Create(ctx context.Context, req resource.CreateRequest, } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 22, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ 8: resources_core.FormatString(data.VdcUid), @@ -236,12 +271,16 @@ func (r *VcNsxtResource) Create(ctx context.Context, req resource.CreateRequest, 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()) @@ -261,7 +300,7 @@ func (r *VcNsxtResource) Create(ctx context.Context, req resource.CreateRequest, }, []resources_core.InputField{ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"}, - {Code: "needExternalAddressSNAT", Field: "NeedExternalAddressSNAT", 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"}, @@ -287,6 +326,21 @@ func (r *VcNsxtResource) Create(ctx context.Context, req resource.CreateRequest, 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)...) } @@ -324,7 +378,7 @@ func (r *VcNsxtResource) Read(ctx context.Context, req resource.ReadRequest, res }, []resources_core.InputField{ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"}, - {Code: "needExternalAddressSNAT", Field: "NeedExternalAddressSNAT", 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"}, @@ -350,6 +404,21 @@ func (r *VcNsxtResource) Read(ctx context.Context, req resource.ReadRequest, res 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)...) } @@ -392,19 +461,19 @@ func (r *VcNsxtResource) Update(ctx context.Context, req resource.UpdateRequest, } } if !hasServiceParamChanges { - if plan.NeedExternalAddressSNAT.IsNull() != state.NeedExternalAddressSNAT.IsNull() || plan.NeedExternalAddressSNAT.IsUnknown() != state.NeedExternalAddressSNAT.IsUnknown() { + if plan.IpSpaceName.IsNull() != state.IpSpaceName.IsNull() || plan.IpSpaceName.IsUnknown() != state.IpSpaceName.IsUnknown() { hasServiceParamChanges = true - } else if !plan.NeedExternalAddressSNAT.IsNull() && !plan.NeedExternalAddressSNAT.IsUnknown() && !state.NeedExternalAddressSNAT.IsNull() && !state.NeedExternalAddressSNAT.IsUnknown() { - if plan.NeedExternalAddressSNAT.ValueBool() != state.NeedExternalAddressSNAT.ValueBool() { + } 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.IpSpaceName.IsNull() != state.IpSpaceName.IsNull() || plan.IpSpaceName.IsUnknown() != state.IpSpaceName.IsUnknown() { + if plan.QosProfile.IsNull() != state.QosProfile.IsNull() || plan.QosProfile.IsUnknown() != state.QosProfile.IsUnknown() { hasServiceParamChanges = true - } else if !plan.IpSpaceName.IsNull() && !plan.IpSpaceName.IsUnknown() && !state.IpSpaceName.IsNull() && !state.IpSpaceName.IsUnknown() { - if plan.IpSpaceName.ValueString() != state.IpSpaceName.ValueString() { + } 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 } } @@ -427,14 +496,17 @@ func (r *VcNsxtResource) Update(ctx context.Context, req resource.UpdateRequest, params := map[int]string{ 368: resources_core.FormatBool(plan.NeedEnableAVI), 369: resources_core.FormatInt64(plan.VirtualServicesCount), - 371: resources_core.FormatBool(plan.NeedExternalAddressSNAT), 372: resources_core.FormatString(plan.IpSpaceName), + 856: resources_core.FormatString(plan.QosProfile), } operationTimeout := "" if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -453,7 +525,7 @@ func (r *VcNsxtResource) Update(ctx context.Context, req resource.UpdateRequest, }, []resources_core.InputField{ {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"}, - {Code: "needExternalAddressSNAT", Field: "NeedExternalAddressSNAT", 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"}, @@ -479,6 +551,20 @@ func (r *VcNsxtResource) Update(ctx context.Context, req resource.UpdateRequest, 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)...) } @@ -492,16 +578,15 @@ func (r *VcNsxtResource) Delete(ctx context.Context, req resource.DeleteRequest, if state.ID.IsNull() || state.ID.IsUnknown() { return } - - deleteMode := "state_only" - if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { - deleteMode = "suspend" - } + 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 diff --git a/universal_rebuild/internal/resources_gen/23_vc_vm_resource.go b/universal_rebuild/internal/resources_gen/23_vc_vm_resource.go index 744b786..b405e85 100644 --- a/universal_rebuild/internal/resources_gen/23_vc_vm_resource.go +++ b/universal_rebuild/internal/resources_gen/23_vc_vm_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -10,7 +11,9 @@ import ( "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" ) @@ -30,6 +33,7 @@ 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"` @@ -60,13 +64,14 @@ func (r *VcVmResource) Metadata(ctx context.Context, req resource.MetadataReques func (r *VcVmResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "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{Required: true, MarkdownDescription: "InstanceUid vApp. vApp должен быть создан Не изменяется после создания."}, + "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: "Имя ВМ. Не поддерживает спец-символы Не изменяется после создания."}, @@ -115,22 +120,11 @@ func (r *VcVmResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRe if resp.Diagnostics.HasError() { return } - planChanged := false - if !plan.VappUid.IsNull() && !plan.VappUid.IsUnknown() { - resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, plan.VappUid.ValueString()) - if err != nil { - resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error()) - } else if resolvedVappUid != "" && resolvedVappUid != plan.VappUid.ValueString() { - plan.VappUid = types.StringValue(resolvedVappUid) - planChanged = true - } - } - if planChanged { - resp.Diagnostics.Append(resp.Plan.Set(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 задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") @@ -138,39 +132,57 @@ func (r *VcVmResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRe } } if !plan.IpSpaceName.IsNull() && !plan.IpSpaceName.IsUnknown() && !state.IpSpaceName.IsNull() && !state.IpSpaceName.IsUnknown() { - if plan.IpSpaceName.ValueString() != state.IpSpaceName.ValueString() { + // 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() { - if plan.TemplateName.ValueString() != state.TemplateName.ValueString() { + // 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() { - if plan.VappUid.ValueString() != state.VappUid.ValueString() { + // 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() { - if plan.VmName.ValueString() != state.VmName.ValueString() { + // 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() || config.IpSpaceName.IsUnknown() { + if config.IpSpaceName.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.") return } - if config.VappUid.IsNull() || config.VappUid.IsUnknown() { + 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 @@ -197,8 +209,10 @@ func (r *VcVmResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRe 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.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 23, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -215,6 +229,24 @@ func (r *VcVmResource) Create(ctx context.Context, req resource.CreateRequest, r 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 { @@ -227,7 +259,9 @@ func (r *VcVmResource) Create(ctx context.Context, req resource.CreateRequest, r } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 23, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ 33: resources_core.FormatString(data.VappUid), @@ -244,6 +278,9 @@ func (r *VcVmResource) Create(ctx context.Context, req resource.CreateRequest, r 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()) @@ -282,6 +319,16 @@ func (r *VcVmResource) Create(ctx context.Context, req resource.CreateRequest, r 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)...) } @@ -338,6 +385,16 @@ func (r *VcVmResource) Read(ctx context.Context, req resource.ReadRequest, resp 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)...) } @@ -365,7 +422,7 @@ func (r *VcVmResource) Update(ctx context.Context, req resource.UpdateRequest, r 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.ValueString() != state.VmCpu.ValueString() { + if !strings.EqualFold(plan.VmCpu.ValueString(), state.VmCpu.ValueString()) { hasServiceParamChanges = true } } @@ -374,7 +431,7 @@ func (r *VcVmResource) Update(ctx context.Context, req resource.UpdateRequest, r 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.ValueString() != state.VmRam.ValueString() { + if !strings.EqualFold(plan.VmRam.ValueString(), state.VmRam.ValueString()) { hasServiceParamChanges = true } } @@ -383,7 +440,7 @@ func (r *VcVmResource) Update(ctx context.Context, req resource.UpdateRequest, r 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 plan.VmDisksize.ValueString() != state.VmDisksize.ValueString() { + if !strings.EqualFold(plan.VmDisksize.ValueString(), state.VmDisksize.ValueString()) { hasServiceParamChanges = true } } @@ -392,7 +449,7 @@ func (r *VcVmResource) Update(ctx context.Context, req resource.UpdateRequest, r 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 plan.AccessIpList.ValueString() != state.AccessIpList.ValueString() { + if !strings.EqualFold(plan.AccessIpList.ValueString(), state.AccessIpList.ValueString()) { hasServiceParamChanges = true } } @@ -423,6 +480,9 @@ func (r *VcVmResource) Update(ctx context.Context, req resource.UpdateRequest, r if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -460,6 +520,15 @@ func (r *VcVmResource) Update(ctx context.Context, req resource.UpdateRequest, r 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)...) } @@ -473,7 +542,6 @@ func (r *VcVmResource) Delete(ctx context.Context, req resource.DeleteRequest, r if state.ID.IsNull() || state.ID.IsUnknown() { return } - deleteMode := "state_only" if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { deleteMode = "suspend" @@ -483,6 +551,9 @@ func (r *VcVmResource) Delete(ctx context.Context, req resource.DeleteRequest, r 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 diff --git a/universal_rebuild/internal/resources_gen/25_vcexternalip_resource.go b/universal_rebuild/internal/resources_gen/25_vcexternalip_resource.go index c8beddf..c08274c 100644 --- a/universal_rebuild/internal/resources_gen/25_vcexternalip_resource.go +++ b/universal_rebuild/internal/resources_gen/25_vcexternalip_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -10,7 +11,9 @@ import ( "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" ) @@ -30,6 +33,7 @@ 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"` @@ -66,9 +70,10 @@ func (r *VcexternalipResource) Metadata(ctx context.Context, req resource.Metada func (r *VcexternalipResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "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: "Не изменяется после создания."}, @@ -134,75 +139,114 @@ func (r *VcexternalipResource) ModifyPlan(ctx context.Context, req resource.Modi } } if !plan.FromServiceCloudEdgeName.IsNull() && !plan.FromServiceCloudEdgeName.IsUnknown() && !state.FromServiceCloudEdgeName.IsNull() && !state.FromServiceCloudEdgeName.IsUnknown() { - if plan.FromServiceCloudEdgeName.ValueString() != state.FromServiceCloudEdgeName.ValueString() { + // 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() { - if plan.FromServiceCloudEdgeScope.ValueString() != state.FromServiceCloudEdgeScope.ValueString() { + // 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() { - if plan.FromServiceCloudOrgName.ValueString() != state.FromServiceCloudOrgName.ValueString() { + // 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() { - if plan.FromServiceCloudVdcName.ValueString() != state.FromServiceCloudVdcName.ValueString() { + // 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() { - if plan.FromServiceCloudVmwareUrl.ValueString() != state.FromServiceCloudVmwareUrl.ValueString() { + // 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() { - if plan.FromServiceNamespace.ValueString() != state.FromServiceNamespace.ValueString() { + // 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() { - if plan.FromServiceVdcGroupName.ValueString() != state.FromServiceVdcGroupName.ValueString() { + // 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() { - if plan.ResourceRealm.ValueString() != state.ResourceRealm.ValueString() { + // 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() || config.FromServiceNamespace.IsUnknown() { + if config.FromServiceNamespace.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "from_service_namespace is required.") return } - if config.InternalAddrAccess.IsNull() || config.InternalAddrAccess.IsUnknown() { + if config.FromServiceNamespace.IsUnknown() { + return + } + if config.InternalAddrAccess.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "internal_addr_access is required.") return } - if config.IpSpaceName.IsNull() || config.IpSpaceName.IsUnknown() { + if config.InternalAddrAccess.IsUnknown() { + return + } + if config.IpSpaceName.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.") return } - if config.ResourceRealm.IsNull() || config.ResourceRealm.IsUnknown() { + if config.IpSpaceName.IsUnknown() { + return + } + if config.ResourceRealm.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") return } - if config.ServiceUid.IsNull() || config.ServiceUid.IsUnknown() { + if config.ResourceRealm.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 @@ -227,8 +271,10 @@ func (r *VcexternalipResource) ModifyPlan(ctx context.Context, req resource.Modi 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.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 25, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -259,7 +305,9 @@ func (r *VcexternalipResource) Create(ctx context.Context, req resource.CreateRe } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 25, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ 140: resources_core.FormatString(data.ServiceUid), @@ -282,6 +330,9 @@ func (r *VcexternalipResource) Create(ctx context.Context, req resource.CreateRe 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()) @@ -399,7 +450,7 @@ func (r *VcexternalipResource) Update(ctx context.Context, req resource.UpdateRe 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 plan.ServiceUid.ValueString() != state.ServiceUid.ValueString() { + if !strings.EqualFold(plan.ServiceUid.ValueString(), state.ServiceUid.ValueString()) { hasServiceParamChanges = true } } @@ -408,7 +459,7 @@ func (r *VcexternalipResource) Update(ctx context.Context, req resource.UpdateRe 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 plan.IpSpaceName.ValueString() != state.IpSpaceName.ValueString() { + if !strings.EqualFold(plan.IpSpaceName.ValueString(), state.IpSpaceName.ValueString()) { hasServiceParamChanges = true } } @@ -435,7 +486,7 @@ func (r *VcexternalipResource) Update(ctx context.Context, req resource.UpdateRe 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 plan.InternalPortAccess.ValueString() != state.InternalPortAccess.ValueString() { + if !strings.EqualFold(plan.InternalPortAccess.ValueString(), state.InternalPortAccess.ValueString()) { hasServiceParamChanges = true } } @@ -444,7 +495,7 @@ func (r *VcexternalipResource) Update(ctx context.Context, req resource.UpdateRe 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 plan.InternalAddrAccess.ValueString() != state.InternalAddrAccess.ValueString() { + if !strings.EqualFold(plan.InternalAddrAccess.ValueString(), state.InternalAddrAccess.ValueString()) { hasServiceParamChanges = true } } @@ -477,6 +528,9 @@ func (r *VcexternalipResource) Update(ctx context.Context, req resource.UpdateRe if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -525,16 +579,15 @@ func (r *VcexternalipResource) Delete(ctx context.Context, req resource.DeleteRe if state.ID.IsNull() || state.ID.IsUnknown() { return } - - deleteMode := "state_only" - if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { - deleteMode = "suspend" - } + 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 diff --git a/universal_rebuild/internal/resources_gen/26_vapp_resource.go b/universal_rebuild/internal/resources_gen/26_vapp_resource.go index 35e9615..733d00c 100644 --- a/universal_rebuild/internal/resources_gen/26_vapp_resource.go +++ b/universal_rebuild/internal/resources_gen/26_vapp_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -10,6 +11,8 @@ import ( "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" ) @@ -29,6 +32,7 @@ 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"` @@ -54,12 +58,13 @@ func (r *VappResource) Metadata(ctx context.Context, req resource.MetadataReques func (r *VappResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, "resource_name": schema.StringAttribute{Required: true}, "operation_timeout": schema.StringAttribute{Optional: true}, - "nsxt_uid": schema.StringAttribute{Required: true, MarkdownDescription: "UUID Услуги `Сетевой шлюз периметра (Edge)` Не изменяется после создания."}, + "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{Required: true, MarkdownDescription: "Уникальное в рамках организации Не изменяется после создания."}, - "vdc_uid": schema.StringAttribute{Required: true, MarkdownDescription: "UUID Услуги `Виртуальный датацентр (vDC)` Не изменяется после создания."}, + "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}, @@ -104,31 +109,11 @@ func (r *VappResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRe if resp.Diagnostics.HasError() { return } - planChanged := false - if !plan.NsxtUid.IsNull() && !plan.NsxtUid.IsUnknown() { - resolvedNsxtUid, err := r.client.ResolveRefSvcParamValue(ctx, 22, plan.NsxtUid.ValueString()) - if err != nil { - resp.Diagnostics.AddWarning("Failed to resolve nsxt_uid", err.Error()) - } else if resolvedNsxtUid != "" && resolvedNsxtUid != plan.NsxtUid.ValueString() { - plan.NsxtUid = types.StringValue(resolvedNsxtUid) - planChanged = true - } - } - if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() { - resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, plan.VdcUid.ValueString()) - if err != nil { - resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) - } else if resolvedVdcUid != "" && resolvedVdcUid != plan.VdcUid.ValueString() { - plan.VdcUid = types.StringValue(resolvedVdcUid) - planChanged = true - } - } - if planChanged { - resp.Diagnostics.Append(resp.Plan.Set(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 задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") @@ -136,37 +121,55 @@ func (r *VappResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRe } } if !plan.NsxtUid.IsNull() && !plan.NsxtUid.IsUnknown() && !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() { - if plan.NsxtUid.ValueString() != state.NsxtUid.ValueString() { + // 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() { - if plan.VappName.ValueString() != state.VappName.ValueString() { + // 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() { - if plan.VdcUid.ValueString() != state.VdcUid.ValueString() { + // 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() || config.NsxtUid.IsUnknown() { + if config.NsxtUid.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "nsxt_uid is required.") return } - if config.VappName.IsNull() || config.VappName.IsUnknown() { + if config.NsxtUid.IsUnknown() { + return + } + if config.VappName.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "vapp_name is required.") return } - if config.VdcUid.IsNull() || config.VdcUid.IsUnknown() { + if config.VappName.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 @@ -196,8 +199,10 @@ func (r *VappResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRe 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.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 26, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -218,6 +223,42 @@ func (r *VappResource) Create(ctx context.Context, req resource.CreateRequest, r 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 { @@ -240,7 +281,9 @@ func (r *VappResource) Create(ctx context.Context, req resource.CreateRequest, r } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 26, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ 190: resources_core.FormatString(data.NsxtUid), @@ -252,6 +295,9 @@ func (r *VappResource) Create(ctx context.Context, req resource.CreateRequest, r 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()) @@ -293,6 +339,21 @@ func (r *VappResource) Create(ctx context.Context, req resource.CreateRequest, r 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)...) } @@ -352,6 +413,21 @@ func (r *VappResource) Read(ctx context.Context, req resource.ReadRequest, resp 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)...) } @@ -396,6 +472,9 @@ func (r *VappResource) Update(ctx context.Context, req resource.UpdateRequest, r if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -436,6 +515,20 @@ func (r *VappResource) Update(ctx context.Context, req resource.UpdateRequest, r 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)...) } @@ -449,7 +542,6 @@ func (r *VappResource) Delete(ctx context.Context, req resource.DeleteRequest, r if state.ID.IsNull() || state.ID.IsUnknown() { return } - deleteMode := "state_only" if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { deleteMode = "suspend" @@ -459,6 +551,9 @@ func (r *VappResource) Delete(ctx context.Context, req resource.DeleteRequest, r 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 diff --git a/universal_rebuild/internal/resources_gen/27_vc_vm_v2_resource.go b/universal_rebuild/internal/resources_gen/27_vc_vm_v2_resource.go index 53b57db..f8bd0a2 100644 --- a/universal_rebuild/internal/resources_gen/27_vc_vm_v2_resource.go +++ b/universal_rebuild/internal/resources_gen/27_vc_vm_v2_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -10,7 +11,9 @@ import ( "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" ) @@ -30,6 +33,7 @@ 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"` @@ -53,10 +57,11 @@ func (r *VcVmV2Resource) Metadata(ctx context.Context, req resource.MetadataRequ func (r *VcVmV2Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, "resource_name": schema.StringAttribute{Required: true}, "operation_timeout": schema.StringAttribute{Optional: true}, - "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)"}, + "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}, @@ -120,8 +125,10 @@ func (r *VcVmV2Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlan 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.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 27, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -132,7 +139,9 @@ func (r *VcVmV2Resource) Create(ctx context.Context, req resource.CreateRequest, } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 27, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ 301: resources_core.FormatString(data.ServerListJson), @@ -142,6 +151,9 @@ func (r *VcVmV2Resource) Create(ctx context.Context, req resource.CreateRequest, 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()) @@ -233,7 +245,7 @@ func (r *VcVmV2Resource) Update(ctx context.Context, req resource.UpdateRequest, 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 plan.ServerListJson.ValueString() != state.ServerListJson.ValueString() { + if !strings.EqualFold(plan.ServerListJson.ValueString(), state.ServerListJson.ValueString()) { hasServiceParamChanges = true } } @@ -261,6 +273,9 @@ func (r *VcVmV2Resource) Update(ctx context.Context, req resource.UpdateRequest, if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -296,7 +311,6 @@ func (r *VcVmV2Resource) Delete(ctx context.Context, req resource.DeleteRequest, if state.ID.IsNull() || state.ID.IsUnknown() { return } - deleteMode := "state_only" if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { deleteMode = "suspend" @@ -306,6 +320,9 @@ func (r *VcVmV2Resource) Delete(ctx context.Context, req resource.DeleteRequest, 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 diff --git a/universal_rebuild/internal/resources_gen/28_vc_vm_v3_reconcile_action.go b/universal_rebuild/internal/resources_gen/28_vc_vm_v3_reconcile_action.go new file mode 100644 index 0000000..7fbdc54 --- /dev/null +++ b/universal_rebuild/internal/resources_gen/28_vc_vm_v3_reconcile_action.go @@ -0,0 +1,136 @@ +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/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_vm_v3 +// Action: reconcile + +var _ resource.Resource = &VcVmV3ReconcileResource{} + +// VcVmV3ReconcileResource triggers a one-time action. +type VcVmV3ReconcileResource struct { + client *core.UniversalClient +} + +type VcVmV3ReconcileModel struct { + ID types.String `tfsdk:"id"` + VcVmV3ID types.String `tfsdk:"vc_vm_v3_id"` + RunID types.String `tfsdk:"run_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` +} + +func NewVcVmV3ReconcileResource() resource.Resource { + return &VcVmV3ReconcileResource{} +} + +func (r *VcVmV3ReconcileResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_vm_v3_reconcile" +} + +func (r *VcVmV3ReconcileResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true}, + "vc_vm_v3_id": schema.StringAttribute{Required: true}, + "run_id": schema.StringAttribute{Required: true}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "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 *VcVmV3ReconcileResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan VcVmV3ReconcileModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.VcVmV3ID.ValueString()) + runID := strings.TrimSpace(plan.RunID.ValueString()) + if instanceUID == "" || runID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра или run_id") + return + } + + params := 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, "reconcile", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "reconcile", runID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *VcVmV3ReconcileResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcVmV3ReconcileModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcVmV3ReconcileResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcVmV3ReconcileModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.VcVmV3ID.ValueString()) + runID := strings.TrimSpace(plan.RunID.ValueString()) + if instanceUID == "" || runID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра или run_id") + return + } + + params := map[string]string{} + + if err := resources_core.RunOperationByCode(ctx, r.client, instanceUID, "reconcile", params); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "reconcile", runID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *VcVmV3ReconcileResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // No-op: removing the resource does not trigger a remote action. +} + +func (r *VcVmV3ReconcileResource) 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/universal_rebuild/internal/resources_gen/28_vc_vm_v3_redeploy_action.go b/universal_rebuild/internal/resources_gen/28_vc_vm_v3_redeploy_action.go index f2212aa..d4f675c 100644 --- a/universal_rebuild/internal/resources_gen/28_vc_vm_v3_redeploy_action.go +++ b/universal_rebuild/internal/resources_gen/28_vc_vm_v3_redeploy_action.go @@ -9,6 +9,8 @@ import ( "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/types" ) @@ -24,21 +26,23 @@ type VcVmV3RedeployResource struct { } type VcVmV3RedeployModel struct { - ID types.String `tfsdk:"id"` - VcVmV3ID types.String `tfsdk:"vc_vm_v3_id"` - RunID types.String `tfsdk:"run_id"` - OperationTimeout types.String `tfsdk:"operation_timeout"` - VmName types.String `tfsdk:"vm_name"` - VmCpu types.Int64 `tfsdk:"vm_cpu"` - VmRam types.Int64 `tfsdk:"vm_ram"` - VmDisk types.Int64 `tfsdk:"vm_disk"` - IpSpaceName types.String `tfsdk:"ip_space_name"` - AccessIpList types.String `tfsdk:"access_ip_list"` - ImageVm types.String `tfsdk:"image_vm"` - UserLogin types.String `tfsdk:"user_login"` - UserPublicKey types.String `tfsdk:"user_public_key"` - CloudInit types.String `tfsdk:"cloud_init"` - AccessPortList types.String `tfsdk:"access_port_list"` + ID types.String `tfsdk:"id"` + VcVmV3ID types.String `tfsdk:"vc_vm_v3_id"` + RunID types.String `tfsdk:"run_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + VmName types.String `tfsdk:"vm_name"` + VmCpu types.Int64 `tfsdk:"vm_cpu"` + VmRam types.Int64 `tfsdk:"vm_ram"` + VmDisk types.Int64 `tfsdk:"vm_disk"` + IpSpaceName types.String `tfsdk:"ip_space_name"` + AccessIpList types.String `tfsdk:"access_ip_list"` + ImageVm types.String `tfsdk:"image_vm"` + UserLogin types.String `tfsdk:"user_login"` + UserPublicKey types.String `tfsdk:"user_public_key"` + CloudInit types.String `tfsdk:"cloud_init"` + AccessPortList types.String `tfsdk:"access_port_list"` } func NewVcVmV3RedeployResource() resource.Resource { @@ -51,21 +55,23 @@ func (r *VcVmV3RedeployResource) Metadata(ctx context.Context, req resource.Meta func (r *VcVmV3RedeployResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, - "vc_vm_v3_id": schema.StringAttribute{Required: true}, - "run_id": schema.StringAttribute{Required: true}, - "operation_timeout": schema.StringAttribute{Optional: true}, - "vm_name": schema.StringAttribute{Required: true}, - "vm_cpu": schema.Int64Attribute{Required: true, MarkdownDescription: "шт"}, - "vm_ram": schema.Int64Attribute{Required: true, MarkdownDescription: "Gb"}, - "vm_disk": schema.Int64Attribute{Optional: true, MarkdownDescription: "Основной диск у vm зависит от образа (см. документацию)
Если необходимо дополнительное место для vm, введите колличество Gb в эту форму"}, - "ip_space_name": schema.StringAttribute{Optional: true, MarkdownDescription: "no-needed, без внешнего ip
internet-antiddos-v1, с функцией защиты от ddos атак
internet-no-antiddos-v1, с функцией защиты от ddos атак
internet-ipv4-v1 тестовое простанство dev"}, - "access_ip_list": schema.StringAttribute{Optional: true, MarkdownDescription: "Формат массива
Каким адресам будет предоставлен доступ до вм

Требует выделенного внешнего IP
Если ничего не передано, то выставлено ["0.0.0.0/0"]"}, - "image_vm": schema.StringAttribute{Required: true}, - "user_login": schema.StringAttribute{Required: true, MarkdownDescription: "Учётка SSH"}, - "user_public_key": schema.StringAttribute{Required: true, MarkdownDescription: "Публичная часть SSH-Ключа для пользователя"}, - "cloud_init": schema.StringAttribute{Optional: true, MarkdownDescription: "Можно передать создание дополнительных пользователей.
На текущий момент не реализовано выполнения в cloud-init, установки пакетов, выполнения команд и записи файлов."}, - "access_port_list": schema.StringAttribute{Optional: true, MarkdownDescription: "По каким портам открыть доступ к ВМ.
Порты 22, 10050 проставляются автоматически
Пока доступно:
22 - SSH,
80 - HTTP,
443 - HTTPS,
10050 - ZABBIX"}, + "id": schema.StringAttribute{Computed: true}, + "vc_vm_v3_id": schema.StringAttribute{Required: true}, + "run_id": schema.StringAttribute{Required: true}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "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."}, + "vm_name": schema.StringAttribute{Required: true}, + "vm_cpu": schema.Int64Attribute{Required: true, MarkdownDescription: "шт"}, + "vm_ram": schema.Int64Attribute{Required: true, MarkdownDescription: "Gb"}, + "vm_disk": schema.Int64Attribute{Optional: true, MarkdownDescription: "Основной диск у vm зависит от образа (см. документацию)
Если необходимо дополнительное место для vm, введите колличество Gb в эту форму"}, + "ip_space_name": schema.StringAttribute{Optional: true, MarkdownDescription: "no-needed, без внешнего ip
internet-antiddos-v1, с функцией защиты от ddos атак
internet-no-antiddos-v1, с функцией защиты от ddos атак
internet-ipv4-v1 тестовое простанство dev"}, + "access_ip_list": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Формат массива
Каким адресам будет предоставлен доступ до вм

Требует выделенного внешнего IP
Если ничего не передано, то выставлено ["0.0.0.0/0"]", PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()}}, + "image_vm": schema.StringAttribute{Required: true}, + "user_login": schema.StringAttribute{Required: true, MarkdownDescription: "Учётка SSH"}, + "user_public_key": schema.StringAttribute{Required: true, MarkdownDescription: "Публичная часть SSH-Ключа для пользователя"}, + "cloud_init": schema.StringAttribute{Optional: true, MarkdownDescription: "Можно передать создание дополнительных пользователей.
На текущий момент не реализовано выполнения в cloud-init, установки пакетов, выполнения команд и записи файлов."}, + "access_port_list": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "По каким портам открыть доступ к ВМ.
Порты 22, 10050 проставляются автоматически
Пока доступно:
22 - SSH,
80 - HTTP,
443 - HTTPS,
10050 - ZABBIX", PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()}}, } resp.Schema = schema.Schema{Attributes: attrs} } @@ -126,6 +132,9 @@ func (r *VcVmV3RedeployResource) Create(ctx context.Context, req resource.Create if !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, "redeploy", params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return diff --git a/universal_rebuild/internal/resources_gen/28_vc_vm_v3_resource.go b/universal_rebuild/internal/resources_gen/28_vc_vm_v3_resource.go index fdfdd1e..7efe110 100644 --- a/universal_rebuild/internal/resources_gen/28_vc_vm_v3_resource.go +++ b/universal_rebuild/internal/resources_gen/28_vc_vm_v3_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -11,7 +12,9 @@ import ( "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" ) @@ -31,6 +34,7 @@ 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"` AccessIpList types.String `tfsdk:"access_ip_list"` AccessPortList types.String `tfsdk:"access_port_list"` CloudInit types.String `tfsdk:"cloud_init"` @@ -65,17 +69,18 @@ func (r *VcVmV3Resource) Metadata(ctx context.Context, req resource.MetadataRequ func (r *VcVmV3Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, "resource_name": schema.StringAttribute{Required: true}, "operation_timeout": schema.StringAttribute{Optional: true}, - "access_ip_list": schema.StringAttribute{Optional: true, MarkdownDescription: "Формат массива
Каким адресам будет предоставлен доступ до вм

Требует выделенного внешнего IP
Если ничего не передано, то выставлено ["0.0.0.0/0"]"}, + "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{Required: true, MarkdownDescription: "UI display_name for service_id=26 (mapped to UUID in core) Не изменяется после создания."}, + "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: "Не изменяется после создания."}, @@ -124,22 +129,11 @@ func (r *VcVmV3Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlan if resp.Diagnostics.HasError() { return } - planChanged := false - if !plan.VappUid.IsNull() && !plan.VappUid.IsUnknown() { - resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, plan.VappUid.ValueString()) - if err != nil { - resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error()) - } else if resolvedVappUid != "" && resolvedVappUid != plan.VappUid.ValueString() { - plan.VappUid = types.StringValue(resolvedVappUid) - planChanged = true - } - } - if planChanged { - resp.Diagnostics.Append(resp.Plan.Set(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 задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") @@ -147,59 +141,89 @@ func (r *VcVmV3Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlan } } if !plan.CloudInit.IsNull() && !plan.CloudInit.IsUnknown() && !state.CloudInit.IsNull() && !state.CloudInit.IsUnknown() { - if plan.CloudInit.ValueString() != state.CloudInit.ValueString() { + // 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() { - if plan.ImageVm.ValueString() != state.ImageVm.ValueString() { + // 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() { - if plan.UserLogin.ValueString() != state.UserLogin.ValueString() { + // 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() { - if plan.UserPublicKey.ValueString() != state.UserPublicKey.ValueString() { + // 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() { - if plan.VappUid.ValueString() != state.VappUid.ValueString() { + // 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() { - if plan.VmName.ValueString() != state.VmName.ValueString() { + // 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() || config.ImageVm.IsUnknown() { + if config.ImageVm.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "image_vm is required.") return } - if config.IpSpaceName.IsNull() || config.IpSpaceName.IsUnknown() { + if config.ImageVm.IsUnknown() { + return + } + if config.IpSpaceName.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.") return } - if config.UserPublicKey.IsNull() || config.UserPublicKey.IsUnknown() { + if config.IpSpaceName.IsUnknown() { + return + } + if config.UserPublicKey.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "user_public_key is required.") return } - if config.VappUid.IsNull() || config.VappUid.IsUnknown() { + 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 @@ -230,8 +254,10 @@ func (r *VcVmV3Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlan 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.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 28, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -256,6 +282,24 @@ func (r *VcVmV3Resource) Create(ctx context.Context, req resource.CreateRequest, 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 { @@ -268,7 +312,9 @@ func (r *VcVmV3Resource) Create(ctx context.Context, req resource.CreateRequest, } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 28, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ 407: resources_core.FormatString(data.VappUid), @@ -289,6 +335,9 @@ func (r *VcVmV3Resource) Create(ctx context.Context, req resource.CreateRequest, 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()) @@ -331,6 +380,16 @@ func (r *VcVmV3Resource) Create(ctx context.Context, req resource.CreateRequest, 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)...) } @@ -391,6 +450,16 @@ func (r *VcVmV3Resource) Read(ctx context.Context, req resource.ReadRequest, res 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)...) } @@ -445,7 +514,7 @@ func (r *VcVmV3Resource) Update(ctx context.Context, req resource.UpdateRequest, 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 plan.IpSpaceName.ValueString() != state.IpSpaceName.ValueString() { + if !strings.EqualFold(plan.IpSpaceName.ValueString(), state.IpSpaceName.ValueString()) { hasServiceParamChanges = true } } @@ -454,7 +523,7 @@ func (r *VcVmV3Resource) Update(ctx context.Context, req resource.UpdateRequest, 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 plan.AccessIpList.ValueString() != state.AccessIpList.ValueString() { + if !strings.EqualFold(plan.AccessIpList.ValueString(), state.AccessIpList.ValueString()) { hasServiceParamChanges = true } } @@ -463,7 +532,7 @@ func (r *VcVmV3Resource) Update(ctx context.Context, req resource.UpdateRequest, 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 plan.AccessPortList.ValueString() != state.AccessPortList.ValueString() { + if !strings.EqualFold(plan.AccessPortList.ValueString(), state.AccessPortList.ValueString()) { hasServiceParamChanges = true } } @@ -496,6 +565,9 @@ func (r *VcVmV3Resource) Update(ctx context.Context, req resource.UpdateRequest, if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -537,6 +609,15 @@ func (r *VcVmV3Resource) Update(ctx context.Context, req resource.UpdateRequest, 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)...) } @@ -550,7 +631,6 @@ func (r *VcVmV3Resource) Delete(ctx context.Context, req resource.DeleteRequest, if state.ID.IsNull() || state.ID.IsUnknown() { return } - deleteMode := "state_only" if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { deleteMode = "suspend" @@ -560,6 +640,9 @@ func (r *VcVmV3Resource) Delete(ctx context.Context, req resource.DeleteRequest, 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 diff --git a/universal_rebuild/internal/resources_gen/29_vc_vdc_group_reconcile_action.go b/universal_rebuild/internal/resources_gen/29_vc_vdc_group_reconcile_action.go index 0676cc7..e087664 100644 --- a/universal_rebuild/internal/resources_gen/29_vc_vdc_group_reconcile_action.go +++ b/universal_rebuild/internal/resources_gen/29_vc_vdc_group_reconcile_action.go @@ -9,6 +9,7 @@ import ( "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/types" ) @@ -24,10 +25,12 @@ type VcVdcGroupReconcileResource struct { } type VcVdcGroupReconcileModel struct { - ID types.String `tfsdk:"id"` - VcVdcGroupID types.String `tfsdk:"vc_vdc_group_id"` - RunID types.String `tfsdk:"run_id"` - OperationTimeout types.String `tfsdk:"operation_timeout"` + ID types.String `tfsdk:"id"` + VcVdcGroupID types.String `tfsdk:"vc_vdc_group_id"` + RunID types.String `tfsdk:"run_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` } func NewVcVdcGroupReconcileResource() resource.Resource { @@ -40,10 +43,12 @@ func (r *VcVdcGroupReconcileResource) Metadata(ctx context.Context, req resource func (r *VcVdcGroupReconcileResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, - "vc_vdc_group_id": schema.StringAttribute{Required: true}, - "run_id": schema.StringAttribute{Required: true}, - "operation_timeout": schema.StringAttribute{Optional: true}, + "id": schema.StringAttribute{Computed: true}, + "vc_vdc_group_id": schema.StringAttribute{Required: true}, + "run_id": schema.StringAttribute{Required: true}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "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} } @@ -68,6 +73,9 @@ func (r *VcVdcGroupReconcileResource) Create(ctx context.Context, req resource.C if !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, "reconcile", params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return diff --git a/universal_rebuild/internal/resources_gen/29_vc_vdc_group_resource.go b/universal_rebuild/internal/resources_gen/29_vc_vdc_group_resource.go index f8f43fa..f737ec1 100644 --- a/universal_rebuild/internal/resources_gen/29_vc_vdc_group_resource.go +++ b/universal_rebuild/internal/resources_gen/29_vc_vdc_group_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -10,6 +11,8 @@ import ( "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" ) @@ -29,6 +32,7 @@ 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"` @@ -52,10 +56,11 @@ func (r *VcVdcGroupResource) Metadata(ctx context.Context, req resource.Metadata func (r *VcVdcGroupResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, "resource_name": schema.StringAttribute{Required: true}, "operation_timeout": schema.StringAttribute{Optional: true}, - "vdc_uid": schema.StringAttribute{Required: true, MarkdownDescription: "UI display_name for service_id=21 (mapped to UUID in core) Не изменяется после создания."}, + "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}, @@ -100,22 +105,11 @@ func (r *VcVdcGroupResource) ModifyPlan(ctx context.Context, req resource.Modify if resp.Diagnostics.HasError() { return } - planChanged := false - if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() { - resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, plan.VdcUid.ValueString()) - if err != nil { - resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) - } else if resolvedVdcUid != "" && resolvedVdcUid != plan.VdcUid.ValueString() { - plan.VdcUid = types.StringValue(resolvedVdcUid) - planChanged = true - } - } - if planChanged { - resp.Diagnostics.Append(resp.Plan.Set(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 задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") @@ -123,17 +117,23 @@ func (r *VcVdcGroupResource) ModifyPlan(ctx context.Context, req resource.Modify } } if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { - if plan.VdcUid.ValueString() != state.VdcUid.ValueString() { + // 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() || config.VdcUid.IsUnknown() { + 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 @@ -153,8 +153,10 @@ func (r *VcVdcGroupResource) ModifyPlan(ctx context.Context, req resource.Modify 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.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 29, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -167,6 +169,24 @@ func (r *VcVdcGroupResource) Create(ctx context.Context, req resource.CreateRequ 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 { @@ -179,7 +199,9 @@ func (r *VcVdcGroupResource) Create(ctx context.Context, req resource.CreateRequ } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 29, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ 618: resources_core.FormatString(data.VdcUid), @@ -189,6 +211,9 @@ func (r *VcVdcGroupResource) Create(ctx context.Context, req resource.CreateRequ 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()) @@ -220,6 +245,16 @@ func (r *VcVdcGroupResource) Create(ctx context.Context, req resource.CreateRequ 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)...) } @@ -269,6 +304,16 @@ func (r *VcVdcGroupResource) Read(ctx context.Context, req resource.ReadRequest, 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)...) } @@ -313,6 +358,9 @@ func (r *VcVdcGroupResource) Update(ctx context.Context, req resource.UpdateRequ if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -343,6 +391,15 @@ func (r *VcVdcGroupResource) Update(ctx context.Context, req resource.UpdateRequ 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)...) } @@ -356,16 +413,15 @@ func (r *VcVdcGroupResource) Delete(ctx context.Context, req resource.DeleteRequ if state.ID.IsNull() || state.ID.IsUnknown() { return } - - deleteMode := "state_only" - if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { - deleteMode = "suspend" - } + 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 diff --git a/universal_rebuild/internal/resources_gen/29_vc_vdc_group_vdc_resource.go b/universal_rebuild/internal/resources_gen/29_vc_vdc_group_vdc_resource.go new file mode 100644 index 0000000..8524d4d --- /dev/null +++ b/universal_rebuild/internal/resources_gen/29_vc_vdc_group_vdc_resource.go @@ -0,0 +1,330 @@ +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(false)}, + "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 known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "vdc", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект подтверждён в state_out, выполняется усыновление") + 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 { + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект отсутствует и будет пропущен: "+targetValue) + return + } + resp.Diagnostics.AddError("Подресурс не найден", "Объект отсутствует: "+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/universal_rebuild/internal/resources_gen/2_template_reconcile_action.go b/universal_rebuild/internal/resources_gen/2_template_reconcile_action.go new file mode 100644 index 0000000..0d74ebe --- /dev/null +++ b/universal_rebuild/internal/resources_gen/2_template_reconcile_action.go @@ -0,0 +1,136 @@ +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/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: template +// Action: reconcile + +var _ resource.Resource = &TemplateReconcileResource{} + +// TemplateReconcileResource triggers a one-time action. +type TemplateReconcileResource struct { + client *core.UniversalClient +} + +type TemplateReconcileModel struct { + ID types.String `tfsdk:"id"` + TemplateID types.String `tfsdk:"template_id"` + RunID types.String `tfsdk:"run_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` +} + +func NewTemplateReconcileResource() resource.Resource { + return &TemplateReconcileResource{} +} + +func (r *TemplateReconcileResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_template_reconcile" +} + +func (r *TemplateReconcileResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true}, + "template_id": schema.StringAttribute{Required: true}, + "run_id": schema.StringAttribute{Required: true}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "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 *TemplateReconcileResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan TemplateReconcileModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.TemplateID.ValueString()) + runID := strings.TrimSpace(plan.RunID.ValueString()) + if instanceUID == "" || runID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра или run_id") + return + } + + params := 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, "reconcile", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "reconcile", runID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *TemplateReconcileResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state TemplateReconcileModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *TemplateReconcileResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan TemplateReconcileModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.TemplateID.ValueString()) + runID := strings.TrimSpace(plan.RunID.ValueString()) + if instanceUID == "" || runID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра или run_id") + return + } + + params := map[string]string{} + + if err := resources_core.RunOperationByCode(ctx, r.client, instanceUID, "reconcile", params); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "reconcile", runID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *TemplateReconcileResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // No-op: removing the resource does not trigger a remote action. +} + +func (r *TemplateReconcileResource) 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/universal_rebuild/internal/resources_gen/2_template_resource.go b/universal_rebuild/internal/resources_gen/2_template_resource.go index 9567524..2c46a54 100644 --- a/universal_rebuild/internal/resources_gen/2_template_resource.go +++ b/universal_rebuild/internal/resources_gen/2_template_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -10,8 +11,8 @@ import ( "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/stringdefault" + "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" ) @@ -31,12 +32,10 @@ type TemplateModel struct { ID types.String `tfsdk:"id"` ResourceName types.String `tfsdk:"resource_name"` OperationTimeout types.String `tfsdk:"operation_timeout"` - 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"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration types.String `tfsdk:"access_configuration"` + ClusterConfiguration types.String `tfsdk:"cluster_configuration"` + StartupConfiguration types.String `tfsdk:"startup_configuration"` SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` StateParams types.Map `tfsdk:"state_params"` @@ -59,15 +58,13 @@ func (r *TemplateResource) Metadata(ctx context.Context, req resource.MetadataRe func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, "resource_name": schema.StringAttribute{Required: true}, "operation_timeout": schema.StringAttribute{Optional: true}, - "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("web01"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."}, - "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100)}, - "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(200)}, - "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.StringAttribute{Required: true}, + "cluster_configuration": schema.StringAttribute{Required: true}, + "startup_configuration": 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}, @@ -118,22 +115,36 @@ func (r *TemplateResource) ModifyPlan(ctx context.Context, req resource.ModifyPl return } } - if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { - if plan.Domain.ValueString() != state.Domain.ValueString() { - resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") - return - } - } - if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { - if plan.ResourceRealm.ValueString() != state.ResourceRealm.ValueString() { - resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + if !plan.StartupConfiguration.IsNull() && !plan.StartupConfiguration.IsUnknown() && !state.StartupConfiguration.IsNull() && !state.StartupConfiguration.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.StartupConfiguration.ValueString(), state.StartupConfiguration.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить startup_configuration", "Параметр задается при создании и не может быть изменен.") return } } return } - if config.ResourceRealm.IsNull() || config.ResourceRealm.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + if config.AccessConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "access_configuration is required.") + return + } + if config.AccessConfiguration.IsUnknown() { + return + } + if config.ClusterConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "cluster_configuration is required.") + return + } + if config.ClusterConfiguration.IsUnknown() { + return + } + if config.StartupConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "startup_configuration is required.") + return + } + if config.StartupConfiguration.IsUnknown() { return } @@ -145,15 +156,14 @@ func (r *TemplateResource) ModifyPlan(ctx context.Context, req resource.ModifyPl adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() } params := map[int]string{ - 202: resources_core.FormatInt64(config.ResourceCPU), - 203: resources_core.FormatInt64(config.ResourceMemory), - 204: resources_core.FormatInt64(config.ResourceDisk), - 205: resources_core.FormatInt64(config.ResourceInstances), - 206: resources_core.FormatString(config.ResourceRealm), - 207: resources_core.FormatString(config.Domain), + 813: resources_core.FormatString(config.ClusterConfiguration), + 814: resources_core.FormatString(config.StartupConfiguration), + 815: resources_core.FormatString(config.AccessConfiguration), } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} - resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 2, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -162,27 +172,37 @@ func (r *TemplateResource) Create(ctx context.Context, req resource.CreateReques if resp.Diagnostics.HasError() { return } - if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + if data.AccessConfiguration.IsNull() || data.AccessConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "access_configuration is required.") + return + } + if data.ClusterConfiguration.IsNull() || data.ClusterConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "cluster_configuration is required.") + return + } + if data.StartupConfiguration.IsNull() || data.StartupConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "startup_configuration is required.") return } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 2, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ - 202: resources_core.FormatInt64(data.ResourceCPU), - 203: resources_core.FormatInt64(data.ResourceMemory), - 204: resources_core.FormatInt64(data.ResourceDisk), - 205: resources_core.FormatInt64(data.ResourceInstances), - 206: resources_core.FormatString(data.ResourceRealm), - 207: resources_core.FormatString(data.Domain), + 813: resources_core.FormatString(data.ClusterConfiguration), + 814: resources_core.FormatString(data.StartupConfiguration), + 815: resources_core.FormatString(data.AccessConfiguration), } 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()) @@ -200,12 +220,9 @@ func (r *TemplateResource) Create(ctx context.Context, req resource.CreateReques {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"}, + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "string"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "string"}, }) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -246,12 +263,9 @@ func (r *TemplateResource) Read(ctx context.Context, req resource.ReadRequest, r {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"}, + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "string"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "string"}, }) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -281,37 +295,19 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques hasServiceParamChanges := false if !hasServiceParamChanges { - if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() { + if plan.ClusterConfiguration.IsNull() != state.ClusterConfiguration.IsNull() || plan.ClusterConfiguration.IsUnknown() != state.ClusterConfiguration.IsUnknown() { hasServiceParamChanges = true - } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { - if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + } else if !plan.ClusterConfiguration.IsNull() && !plan.ClusterConfiguration.IsUnknown() && !state.ClusterConfiguration.IsNull() && !state.ClusterConfiguration.IsUnknown() { + if !strings.EqualFold(plan.ClusterConfiguration.ValueString(), state.ClusterConfiguration.ValueString()) { hasServiceParamChanges = true } } } if !hasServiceParamChanges { - if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() { + if plan.AccessConfiguration.IsNull() != state.AccessConfiguration.IsNull() || plan.AccessConfiguration.IsUnknown() != state.AccessConfiguration.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() { + } else if !plan.AccessConfiguration.IsNull() && !plan.AccessConfiguration.IsUnknown() && !state.AccessConfiguration.IsNull() && !state.AccessConfiguration.IsUnknown() { + if !strings.EqualFold(plan.AccessConfiguration.ValueString(), state.AccessConfiguration.ValueString()) { hasServiceParamChanges = true } } @@ -332,16 +328,17 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques } params := map[int]string{ - 208: resources_core.FormatInt64(plan.ResourceCPU), - 209: resources_core.FormatInt64(plan.ResourceMemory), - 210: resources_core.FormatInt64(plan.ResourceDisk), - 211: resources_core.FormatInt64(plan.ResourceInstances), + 816: resources_core.FormatString(plan.ClusterConfiguration), + 817: resources_core.FormatString(plan.AccessConfiguration), } operationTimeout := "" if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -358,12 +355,9 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques {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"}, + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "string"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "string"}, }) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -382,7 +376,6 @@ func (r *TemplateResource) Delete(ctx context.Context, req resource.DeleteReques if state.ID.IsNull() || state.ID.IsUnknown() { return } - deleteMode := "state_only" if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { deleteMode = "suspend" @@ -392,6 +385,9 @@ func (r *TemplateResource) Delete(ctx context.Context, req resource.DeleteReques 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 diff --git a/universal_rebuild/internal/resources_gen/50_nextcloud_backup_resource.go b/universal_rebuild/internal/resources_gen/50_nextcloud_backup_resource.go new file mode 100644 index 0000000..71bb43e --- /dev/null +++ b/universal_rebuild/internal/resources_gen/50_nextcloud_backup_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/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(false)}, + "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 known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект подтверждён в state_out, выполняется усыновление") + 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 { + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект отсутствует и будет пропущен: "+targetValue) + return + } + resp.Diagnostics.AddError("Подресурс не найден", "Объект отсутствует: "+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/universal_rebuild/internal/resources_gen/50_nextcloud_reconcile_action.go b/universal_rebuild/internal/resources_gen/50_nextcloud_reconcile_action.go new file mode 100644 index 0000000..b97c8e1 --- /dev/null +++ b/universal_rebuild/internal/resources_gen/50_nextcloud_reconcile_action.go @@ -0,0 +1,136 @@ +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/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: nextcloud +// Action: reconcile + +var _ resource.Resource = &NextcloudReconcileResource{} + +// NextcloudReconcileResource triggers a one-time action. +type NextcloudReconcileResource struct { + client *core.UniversalClient +} + +type NextcloudReconcileModel struct { + ID types.String `tfsdk:"id"` + NextcloudID types.String `tfsdk:"nextcloud_id"` + RunID types.String `tfsdk:"run_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` +} + +func NewNextcloudReconcileResource() resource.Resource { + return &NextcloudReconcileResource{} +} + +func (r *NextcloudReconcileResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_nextcloud_reconcile" +} + +func (r *NextcloudReconcileResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true}, + "nextcloud_id": schema.StringAttribute{Required: true}, + "run_id": schema.StringAttribute{Required: true}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "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 *NextcloudReconcileResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan NextcloudReconcileModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.NextcloudID.ValueString()) + runID := strings.TrimSpace(plan.RunID.ValueString()) + if instanceUID == "" || runID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра или run_id") + return + } + + params := 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, "reconcile", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "reconcile", runID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *NextcloudReconcileResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state NextcloudReconcileModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *NextcloudReconcileResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan NextcloudReconcileModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.NextcloudID.ValueString()) + runID := strings.TrimSpace(plan.RunID.ValueString()) + if instanceUID == "" || runID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра или run_id") + return + } + + params := map[string]string{} + + if err := resources_core.RunOperationByCode(ctx, r.client, instanceUID, "reconcile", params); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "reconcile", runID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *NextcloudReconcileResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // No-op: removing the resource does not trigger a remote action. +} + +func (r *NextcloudReconcileResource) 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/universal_rebuild/internal/resources_gen/50_nextcloud_recovery_action.go b/universal_rebuild/internal/resources_gen/50_nextcloud_recovery_action.go new file mode 100644 index 0000000..e62142d --- /dev/null +++ b/universal_rebuild/internal/resources_gen/50_nextcloud_recovery_action.go @@ -0,0 +1,170 @@ +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/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: nextcloud +// Action: recovery + +var _ resource.Resource = &NextcloudRecoveryResource{} + +// NextcloudRecoveryResource triggers a one-time action. +type NextcloudRecoveryResource struct { + client *core.UniversalClient +} + +type NextcloudRecoveryModel struct { + ID types.String `tfsdk:"id"` + NextcloudID types.String `tfsdk:"nextcloud_id"` + RunID types.String `tfsdk:"run_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + StartupConfiguration types.String `tfsdk:"startup_configuration"` + ClusterConfiguration types.String `tfsdk:"cluster_configuration"` + AccessConfiguration types.String `tfsdk:"access_configuration"` + AppConfiguration types.String `tfsdk:"app_configuration"` +} + +func NewNextcloudRecoveryResource() resource.Resource { + return &NextcloudRecoveryResource{} +} + +func (r *NextcloudRecoveryResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_nextcloud_recovery" +} + +func (r *NextcloudRecoveryResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true}, + "nextcloud_id": schema.StringAttribute{Required: true}, + "run_id": schema.StringAttribute{Required: true}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "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}, + "cluster_configuration": schema.StringAttribute{Required: true}, + "access_configuration": schema.StringAttribute{Required: true}, + "app_configuration": schema.StringAttribute{Required: true, MarkdownDescription: "Версия Nextcloud будет взята из бэкапа"}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *NextcloudRecoveryResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan NextcloudRecoveryModel + 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 + } + if plan.ClusterConfiguration.IsNull() || plan.ClusterConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "cluster_configuration is required.") + return + } + if plan.AccessConfiguration.IsNull() || plan.AccessConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "access_configuration is required.") + return + } + if plan.AppConfiguration.IsNull() || plan.AppConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "app_configuration is required.") + return + } + + instanceUID := strings.TrimSpace(plan.NextcloudID.ValueString()) + runID := strings.TrimSpace(plan.RunID.ValueString()) + if instanceUID == "" || runID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра или run_id") + return + } + + params := map[string]string{ + "startupConfiguration": resources_core.FormatString(plan.StartupConfiguration), + "clusterConfiguration": resources_core.FormatString(plan.ClusterConfiguration), + "accessConfiguration": resources_core.FormatString(plan.AccessConfiguration), + "appConfiguration": resources_core.FormatString(plan.AppConfiguration), + } + + operationTimeout := "" + if !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, "recovery", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "recovery", runID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *NextcloudRecoveryResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state NextcloudRecoveryModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *NextcloudRecoveryResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan NextcloudRecoveryModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.NextcloudID.ValueString()) + runID := strings.TrimSpace(plan.RunID.ValueString()) + if instanceUID == "" || runID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра или run_id") + return + } + + params := map[string]string{ + "startupConfiguration": resources_core.FormatString(plan.StartupConfiguration), + "clusterConfiguration": resources_core.FormatString(plan.ClusterConfiguration), + "accessConfiguration": resources_core.FormatString(plan.AccessConfiguration), + "appConfiguration": resources_core.FormatString(plan.AppConfiguration), + } + + if err := resources_core.RunOperationByCode(ctx, r.client, instanceUID, "recovery", params); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "recovery", runID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *NextcloudRecoveryResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // No-op: removing the resource does not trigger a remote action. +} + +func (r *NextcloudRecoveryResource) 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/universal_rebuild/internal/resources_gen/50_nextcloud_resource.go b/universal_rebuild/internal/resources_gen/50_nextcloud_resource.go index 5509d65..6183117 100644 --- a/universal_rebuild/internal/resources_gen/50_nextcloud_resource.go +++ b/universal_rebuild/internal/resources_gen/50_nextcloud_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -10,8 +11,8 @@ import ( "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/stringdefault" + "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" ) @@ -31,15 +32,11 @@ type NextcloudModel struct { ID types.String `tfsdk:"id"` ResourceName types.String `tfsdk:"resource_name"` OperationTimeout types.String `tfsdk:"operation_timeout"` - AppVersion types.String `tfsdk:"app_version"` - DocumentServer types.String `tfsdk:"document_server"` - Domain types.String `tfsdk:"domain"` - PsqlUid types.String `tfsdk:"psql_uid"` - RedisUid types.String `tfsdk:"redis_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"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration types.String `tfsdk:"access_configuration"` + AppConfiguration types.String `tfsdk:"app_configuration"` + ClusterConfiguration types.String `tfsdk:"cluster_configuration"` + StartupConfiguration types.String `tfsdk:"startup_configuration"` SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` StateParams types.Map `tfsdk:"state_params"` @@ -62,18 +59,14 @@ func (r *NextcloudResource) Metadata(ctx context.Context, req resource.MetadataR func (r *NextcloudResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, "resource_name": schema.StringAttribute{Required: true}, "operation_timeout": schema.StringAttribute{Optional: true}, - "app_version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("32.0.1")}, - "document_server": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("collabora"), MarkdownDescription: "Выбор сервера документов (onlyoffice)/(collabora). onlyoffice пока не доступен
Размер пода collabora:
1.8 ядра. 2Gi ОЗУ Не изменяется после создания."}, - "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("web01"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."}, - "psql_uid": schema.StringAttribute{Required: true, MarkdownDescription: "UI display_name for service_id=90 (mapped to UUID in core) Не изменяется после создания."}, - "redis_uid": schema.StringAttribute{Required: true, MarkdownDescription: "UI display_name for service_id=91 (mapped to UUID in core) Не изменяется после создания."}, - "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1000), MarkdownDescription: "Квота ядра пода (Указывается в mili). 1 ядро = 1000 Mili"}, - "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2), MarkdownDescription: "Квота диска (Указывается в GIGAbytes)
Диск не может быть меньше 2G (!)"}, - "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов
Не может пока быть больше одного, так как нужны SC под RWMany"}, - "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1024), MarkdownDescription: "Квота памяти пода (Указывается в Megabytes)"}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания."}, + "app_configuration": schema.StringAttribute{Required: true}, + "cluster_configuration": schema.StringAttribute{Required: true}, + "startup_configuration": 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}, @@ -118,69 +111,58 @@ func (r *NextcloudResource) ModifyPlan(ctx context.Context, req resource.ModifyP if resp.Diagnostics.HasError() { return } - planChanged := false - if !plan.PsqlUid.IsNull() && !plan.PsqlUid.IsUnknown() { - resolvedPsqlUid, err := r.client.ResolveRefSvcParamValue(ctx, 90, plan.PsqlUid.ValueString()) - if err != nil { - resp.Diagnostics.AddWarning("Failed to resolve psql_uid", err.Error()) - } else if resolvedPsqlUid != "" && resolvedPsqlUid != plan.PsqlUid.ValueString() { - plan.PsqlUid = types.StringValue(resolvedPsqlUid) - planChanged = true - } - } - if !plan.RedisUid.IsNull() && !plan.RedisUid.IsUnknown() { - resolvedRedisUid, err := r.client.ResolveRefSvcParamValue(ctx, 91, plan.RedisUid.ValueString()) - if err != nil { - resp.Diagnostics.AddWarning("Failed to resolve redis_uid", err.Error()) - } else if resolvedRedisUid != "" && resolvedRedisUid != plan.RedisUid.ValueString() { - plan.RedisUid = types.StringValue(resolvedRedisUid) - planChanged = true - } - } - if planChanged { - resp.Diagnostics.Append(resp.Plan.Set(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.DocumentServer.IsNull() && !plan.DocumentServer.IsUnknown() && !state.DocumentServer.IsNull() && !state.DocumentServer.IsUnknown() { - if plan.DocumentServer.ValueString() != state.DocumentServer.ValueString() { - resp.Diagnostics.AddError("Нельзя изменить document_server", "Параметр задается при создании и не может быть изменен.") + if !plan.AccessConfiguration.IsNull() && !plan.AccessConfiguration.IsUnknown() && !state.AccessConfiguration.IsNull() && !state.AccessConfiguration.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.AccessConfiguration.ValueString(), state.AccessConfiguration.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить access_configuration", "Параметр задается при создании и не может быть изменен.") return } } - if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { - if plan.Domain.ValueString() != state.Domain.ValueString() { - resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") - return - } - } - if !plan.PsqlUid.IsNull() && !plan.PsqlUid.IsUnknown() && !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() { - if plan.PsqlUid.ValueString() != state.PsqlUid.ValueString() { - resp.Diagnostics.AddError("Нельзя изменить psql_uid", "Параметр задается при создании и не может быть изменен.") - return - } - } - if !plan.RedisUid.IsNull() && !plan.RedisUid.IsUnknown() && !state.RedisUid.IsNull() && !state.RedisUid.IsUnknown() { - if plan.RedisUid.ValueString() != state.RedisUid.ValueString() { - resp.Diagnostics.AddError("Нельзя изменить redis_uid", "Параметр задается при создании и не может быть изменен.") + if !plan.StartupConfiguration.IsNull() && !plan.StartupConfiguration.IsUnknown() && !state.StartupConfiguration.IsNull() && !state.StartupConfiguration.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.StartupConfiguration.ValueString(), state.StartupConfiguration.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить startup_configuration", "Параметр задается при создании и не может быть изменен.") return } } return } - if config.PsqlUid.IsNull() || config.PsqlUid.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "psql_uid is required.") + if config.AccessConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "access_configuration is required.") return } - if config.RedisUid.IsNull() || config.RedisUid.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "redis_uid is required.") + if config.AccessConfiguration.IsUnknown() { + return + } + if config.AppConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "app_configuration is required.") + return + } + if config.AppConfiguration.IsUnknown() { + return + } + if config.ClusterConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "cluster_configuration is required.") + return + } + if config.ClusterConfiguration.IsUnknown() { + return + } + if config.StartupConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "startup_configuration is required.") + return + } + if config.StartupConfiguration.IsUnknown() { return } @@ -191,35 +173,16 @@ func (r *NextcloudResource) ModifyPlan(ctx context.Context, req resource.ModifyP 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) - } - } - if !config.RedisUid.IsNull() && !config.RedisUid.IsUnknown() { - resolvedRedisUid, err := r.client.ResolveRefSvcParamValue(ctx, 91, config.RedisUid.ValueString()) - if err != nil { - resp.Diagnostics.AddWarning("Failed to resolve redis_uid", err.Error()) - } else if resolvedRedisUid != "" && resolvedRedisUid != config.RedisUid.ValueString() { - config.RedisUid = types.StringValue(resolvedRedisUid) - } - } params := map[int]string{ - 21: resources_core.FormatString(config.Domain), - 99: resources_core.FormatInt64(config.ResourceCPU), - 100: resources_core.FormatInt64(config.ResourceMemory), - 101: resources_core.FormatInt64(config.ResourceDisk), - 192: resources_core.FormatString(config.DocumentServer), - 470: resources_core.FormatInt64(config.ResourceInstances), - 564: resources_core.FormatString(config.PsqlUid), - 569: resources_core.FormatString(config.AppVersion), - 570: resources_core.FormatString(config.RedisUid), + 1055: resources_core.FormatString(config.StartupConfiguration), + 1056: resources_core.FormatString(config.ClusterConfiguration), + 1057: resources_core.FormatString(config.AccessConfiguration), + 1058: resources_core.FormatString(config.AppConfiguration), } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} - resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 50, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -228,54 +191,42 @@ func (r *NextcloudResource) Create(ctx context.Context, req resource.CreateReque if resp.Diagnostics.HasError() { return } - if data.PsqlUid.IsNull() || data.PsqlUid.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "psql_uid is required.") + if data.AccessConfiguration.IsNull() || data.AccessConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "access_configuration is required.") return } - if data.RedisUid.IsNull() || data.RedisUid.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "redis_uid is required.") + if data.AppConfiguration.IsNull() || data.AppConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "app_configuration is required.") return } - 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) - } + if data.ClusterConfiguration.IsNull() || data.ClusterConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "cluster_configuration is required.") + return } - if !data.RedisUid.IsNull() && !data.RedisUid.IsUnknown() { - resolvedRedisUid, err := r.client.ResolveRefSvcParamValue(ctx, 91, data.RedisUid.ValueString()) - if err != nil { - resp.Diagnostics.AddError("Ошибка клиента", err.Error()) - return - } - if resolvedRedisUid != "" && resolvedRedisUid != data.RedisUid.ValueString() { - data.RedisUid = types.StringValue(resolvedRedisUid) - } + if data.StartupConfiguration.IsNull() || data.StartupConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "startup_configuration is required.") + return } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 50, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ - 21: resources_core.FormatString(data.Domain), - 99: resources_core.FormatInt64(data.ResourceCPU), - 100: resources_core.FormatInt64(data.ResourceMemory), - 101: resources_core.FormatInt64(data.ResourceDisk), - 192: resources_core.FormatString(data.DocumentServer), - 470: resources_core.FormatInt64(data.ResourceInstances), - 564: resources_core.FormatString(data.PsqlUid), - 569: resources_core.FormatString(data.AppVersion), - 570: resources_core.FormatString(data.RedisUid), + 1055: resources_core.FormatString(data.StartupConfiguration), + 1056: resources_core.FormatString(data.ClusterConfiguration), + 1057: resources_core.FormatString(data.AccessConfiguration), + 1058: resources_core.FormatString(data.AppConfiguration), } 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()) @@ -293,36 +244,15 @@ func (r *NextcloudResource) Create(ctx context.Context, req resource.CreateReque {Code: "vault_user_path"}, {Code: "vault_fields"}, }, []resources_core.InputField{ - {Code: "appVersion", Field: "AppVersion", Type: "string"}, - {Code: "documentServer", Field: "DocumentServer", Type: "string"}, - {Code: "domain", Field: "Domain", Type: "string"}, - {Code: "psqlUid", Field: "PsqlUid", Type: "string"}, - {Code: "redisUid", Field: "RedisUid", 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: "accessConfiguration", Field: "AccessConfiguration", Type: "string"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "string"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "string"}, }) 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) - } - } - if !state.RedisUid.IsNull() && !state.RedisUid.IsUnknown() { - resolvedRedisUid, err := r.client.ResolveRefSvcParamValue(ctx, 91, state.RedisUid.ValueString()) - if err != nil { - resp.Diagnostics.AddWarning("Failed to resolve redis_uid", err.Error()) - } else if resolvedRedisUid != "" && resolvedRedisUid != state.RedisUid.ValueString() { - state.RedisUid = types.StringValue(resolvedRedisUid) - } - } resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) } @@ -358,36 +288,15 @@ func (r *NextcloudResource) Read(ctx context.Context, req resource.ReadRequest, {Code: "vault_user_path"}, {Code: "vault_fields"}, }, []resources_core.InputField{ - {Code: "appVersion", Field: "AppVersion", Type: "string"}, - {Code: "documentServer", Field: "DocumentServer", Type: "string"}, - {Code: "domain", Field: "Domain", Type: "string"}, - {Code: "psqlUid", Field: "PsqlUid", Type: "string"}, - {Code: "redisUid", Field: "RedisUid", 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: "accessConfiguration", Field: "AccessConfiguration", Type: "string"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "string"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "string"}, }) 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) - } - } - if !newState.RedisUid.IsNull() && !newState.RedisUid.IsUnknown() { - resolvedRedisUid, err := r.client.ResolveRefSvcParamValue(ctx, 91, newState.RedisUid.ValueString()) - if err != nil { - resp.Diagnostics.AddWarning("Failed to resolve redis_uid", err.Error()) - } else if resolvedRedisUid != "" && resolvedRedisUid != newState.RedisUid.ValueString() { - newState.RedisUid = types.StringValue(resolvedRedisUid) - } - } resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) } @@ -412,46 +321,19 @@ func (r *NextcloudResource) Update(ctx context.Context, req resource.UpdateReque hasServiceParamChanges := false if !hasServiceParamChanges { - if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() { + if plan.ClusterConfiguration.IsNull() != state.ClusterConfiguration.IsNull() || plan.ClusterConfiguration.IsUnknown() != state.ClusterConfiguration.IsUnknown() { hasServiceParamChanges = true - } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { - if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + } else if !plan.ClusterConfiguration.IsNull() && !plan.ClusterConfiguration.IsUnknown() && !state.ClusterConfiguration.IsNull() && !state.ClusterConfiguration.IsUnknown() { + if !strings.EqualFold(plan.ClusterConfiguration.ValueString(), state.ClusterConfiguration.ValueString()) { hasServiceParamChanges = true } } } if !hasServiceParamChanges { - if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() { + if plan.AppConfiguration.IsNull() != state.AppConfiguration.IsNull() || plan.AppConfiguration.IsUnknown() != state.AppConfiguration.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.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 plan.AppVersion.ValueString() != state.AppVersion.ValueString() { + } else if !plan.AppConfiguration.IsNull() && !plan.AppConfiguration.IsUnknown() && !state.AppConfiguration.IsNull() && !state.AppConfiguration.IsUnknown() { + if !strings.EqualFold(plan.AppConfiguration.ValueString(), state.AppConfiguration.ValueString()) { hasServiceParamChanges = true } } @@ -472,17 +354,17 @@ func (r *NextcloudResource) Update(ctx context.Context, req resource.UpdateReque } params := map[int]string{ - 178: resources_core.FormatInt64(plan.ResourceCPU), - 179: resources_core.FormatInt64(plan.ResourceMemory), - 180: resources_core.FormatInt64(plan.ResourceDisk), - 547: resources_core.FormatInt64(plan.ResourceInstances), - 571: resources_core.FormatString(plan.AppVersion), + 1059: resources_core.FormatString(plan.ClusterConfiguration), + 1060: resources_core.FormatString(plan.AppConfiguration), } operationTimeout := "" if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -499,36 +381,15 @@ func (r *NextcloudResource) Update(ctx context.Context, req resource.UpdateReque {Code: "vault_user_path"}, {Code: "vault_fields"}, }, []resources_core.InputField{ - {Code: "appVersion", Field: "AppVersion", Type: "string"}, - {Code: "documentServer", Field: "DocumentServer", Type: "string"}, - {Code: "domain", Field: "Domain", Type: "string"}, - {Code: "psqlUid", Field: "PsqlUid", Type: "string"}, - {Code: "redisUid", Field: "RedisUid", 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: "accessConfiguration", Field: "AccessConfiguration", Type: "string"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "string"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "string"}, }) 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) - } - } - if !state.RedisUid.IsNull() && !state.RedisUid.IsUnknown() { - resolvedRedisUid, err := r.client.ResolveRefSvcParamValue(ctx, 91, state.RedisUid.ValueString()) - if err != nil { - resp.Diagnostics.AddWarning("Failed to resolve redis_uid", err.Error()) - } else if resolvedRedisUid != "" && resolvedRedisUid != state.RedisUid.ValueString() { - state.RedisUid = types.StringValue(resolvedRedisUid) - } - } resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) } @@ -542,7 +403,6 @@ func (r *NextcloudResource) Delete(ctx context.Context, req resource.DeleteReque if state.ID.IsNull() || state.ID.IsUnknown() { return } - deleteMode := "state_only" if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { deleteMode = "suspend" @@ -552,6 +412,9 @@ func (r *NextcloudResource) Delete(ctx context.Context, req resource.DeleteReque 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 diff --git a/universal_rebuild/internal/resources_gen/50_nextcloud_restart_action.go b/universal_rebuild/internal/resources_gen/50_nextcloud_restart_action.go index 7568ac5..d331a23 100644 --- a/universal_rebuild/internal/resources_gen/50_nextcloud_restart_action.go +++ b/universal_rebuild/internal/resources_gen/50_nextcloud_restart_action.go @@ -9,6 +9,7 @@ import ( "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/types" ) @@ -24,10 +25,12 @@ type NextcloudRestartResource struct { } type NextcloudRestartModel struct { - ID types.String `tfsdk:"id"` - NextcloudID types.String `tfsdk:"nextcloud_id"` - RunID types.String `tfsdk:"run_id"` - OperationTimeout types.String `tfsdk:"operation_timeout"` + ID types.String `tfsdk:"id"` + NextcloudID types.String `tfsdk:"nextcloud_id"` + RunID types.String `tfsdk:"run_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` } func NewNextcloudRestartResource() resource.Resource { @@ -40,10 +43,12 @@ func (r *NextcloudRestartResource) Metadata(ctx context.Context, req resource.Me func (r *NextcloudRestartResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, - "nextcloud_id": schema.StringAttribute{Required: true}, - "run_id": schema.StringAttribute{Required: true}, - "operation_timeout": schema.StringAttribute{Optional: true}, + "id": schema.StringAttribute{Computed: true}, + "nextcloud_id": schema.StringAttribute{Required: true}, + "run_id": schema.StringAttribute{Required: true}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "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} } @@ -68,6 +73,9 @@ func (r *NextcloudRestartResource) Create(ctx context.Context, req resource.Crea if !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, "restart", params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return diff --git a/universal_rebuild/internal/resources_gen/81_superset_resource.go b/universal_rebuild/internal/resources_gen/81_superset_resource.go index ab9d101..8a8010c 100644 --- a/universal_rebuild/internal/resources_gen/81_superset_resource.go +++ b/universal_rebuild/internal/resources_gen/81_superset_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -11,7 +12,9 @@ import ( "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" ) @@ -31,6 +34,7 @@ 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"` @@ -60,9 +64,10 @@ func (r *SupersetResource) Metadata(ctx context.Context, req resource.MetadataRe func (r *SupersetResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "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("web01"), 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 ядро Не изменяется после создания."}, @@ -121,13 +126,19 @@ func (r *SupersetResource) ModifyPlan(ctx context.Context, req resource.ModifyPl } } if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { - if plan.Domain.ValueString() != state.Domain.ValueString() { + // 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() { - if plan.Emails.ValueString() != state.Emails.ValueString() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Emails.ValueString(), state.Emails.ValueString()) { resp.Diagnostics.AddError("Нельзя изменить emails", "Параметр задается при создании и не может быть изменен.") return } @@ -157,17 +168,23 @@ func (r *SupersetResource) ModifyPlan(ctx context.Context, req resource.ModifyPl } } if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { - if plan.ResourceRealm.ValueString() != state.ResourceRealm.ValueString() { + // 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() || config.ResourceRealm.IsUnknown() { + 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 @@ -185,8 +202,13 @@ func (r *SupersetResource) ModifyPlan(ctx context.Context, req resource.ModifyPl 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.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 81, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -201,7 +223,12 @@ func (r *SupersetResource) Create(ctx context.Context, req resource.CreateReques } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 81, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ 14: resources_core.FormatString(data.Domain), @@ -217,6 +244,9 @@ func (r *SupersetResource) Create(ctx context.Context, req resource.CreateReques 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()) @@ -337,6 +367,9 @@ func (r *SupersetResource) Update(ctx context.Context, req resource.UpdateReques if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -378,16 +411,15 @@ func (r *SupersetResource) Delete(ctx context.Context, req resource.DeleteReques if state.ID.IsNull() || state.ID.IsUnknown() { return } - - deleteMode := "state_only" - if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { - deleteMode = "suspend" - } + 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 diff --git a/universal_rebuild/internal/resources_gen/82_harbor_resource.go b/universal_rebuild/internal/resources_gen/82_harbor_resource.go index 4e1e3e3..2fe923b 100644 --- a/universal_rebuild/internal/resources_gen/82_harbor_resource.go +++ b/universal_rebuild/internal/resources_gen/82_harbor_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -11,7 +12,9 @@ import ( "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" ) @@ -31,8 +34,8 @@ 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"` - Emails types.String `tfsdk:"emails"` ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` ResourceInstances types.Int64 `tfsdk:"resource_instances"` ResourceMemory types.Int64 `tfsdk:"resource_memory"` @@ -60,16 +63,16 @@ func (r *HarborResource) Metadata(ctx context.Context, req resource.MetadataRequ func (r *HarborResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "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("web01"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."}, - "emails": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("testuser1"), MarkdownDescription: "Учётная запись, которой будут выданы админские права. Обязана находиться в Keycloak. Можно передавать несколько через пробел Не изменяется после создания."}, "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{Required: true, MarkdownDescription: "UUID экземпляра услуги S3 Не изменяется после создания."}, + "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}, @@ -114,22 +117,11 @@ func (r *HarborResource) ModifyPlan(ctx context.Context, req resource.ModifyPlan if resp.Diagnostics.HasError() { return } - planChanged := false - if !plan.S3Uid.IsNull() && !plan.S3Uid.IsUnknown() { - resolvedS3Uid, err := r.client.ResolveRefSvcParamValue(ctx, 12, plan.S3Uid.ValueString()) - if err != nil { - resp.Diagnostics.AddWarning("Failed to resolve s3_uid", err.Error()) - } else if resolvedS3Uid != "" && resolvedS3Uid != plan.S3Uid.ValueString() { - plan.S3Uid = types.StringValue(resolvedS3Uid) - planChanged = true - } - } - if planChanged { - resp.Diagnostics.Append(resp.Plan.Set(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 задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") @@ -137,17 +129,14 @@ func (r *HarborResource) ModifyPlan(ctx context.Context, req resource.ModifyPlan } } if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { - if plan.Domain.ValueString() != state.Domain.ValueString() { + // 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() { - if 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", "Параметр задается при создании и не может быть изменен.") @@ -167,27 +156,39 @@ func (r *HarborResource) ModifyPlan(ctx context.Context, req resource.ModifyPlan } } if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { - if plan.ResourceRealm.ValueString() != state.ResourceRealm.ValueString() { + // 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() { - if plan.S3Uid.ValueString() != state.S3Uid.ValueString() { + // 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() || config.ResourceRealm.IsUnknown() { + if config.ResourceRealm.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") return } - if config.S3Uid.IsNull() || config.S3Uid.IsUnknown() { + 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 @@ -206,15 +207,19 @@ func (r *HarborResource) ModifyPlan(ctx context.Context, req resource.ModifyPlan } params := map[int]string{ 16: resources_core.FormatString(config.Domain), - 17: resources_core.FormatString(config.Emails), 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.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 82, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -233,11 +238,15 @@ func (r *HarborResource) Create(ctx context.Context, req resource.CreateRequest, } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 82, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ 16: resources_core.FormatString(data.Domain), - 17: resources_core.FormatString(data.Emails), 110: resources_core.FormatString(data.ResourceRealm), 228: resources_core.FormatInt64(data.ResourceCPU), 229: resources_core.FormatInt64(data.ResourceMemory), @@ -249,6 +258,9 @@ func (r *HarborResource) Create(ctx context.Context, req resource.CreateRequest, 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()) @@ -267,7 +279,6 @@ func (r *HarborResource) Create(ctx context.Context, req resource.CreateRequest, {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: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, @@ -277,6 +288,11 @@ func (r *HarborResource) Create(ctx context.Context, req resource.CreateRequest, 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)...) } @@ -313,7 +329,6 @@ func (r *HarborResource) Read(ctx context.Context, req resource.ReadRequest, res {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: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, @@ -323,6 +338,11 @@ func (r *HarborResource) Read(ctx context.Context, req resource.ReadRequest, res 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)...) } @@ -367,6 +387,9 @@ func (r *HarborResource) Update(ctx context.Context, req resource.UpdateRequest, if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -384,7 +407,6 @@ func (r *HarborResource) Update(ctx context.Context, req resource.UpdateRequest, {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: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, @@ -394,6 +416,10 @@ func (r *HarborResource) Update(ctx context.Context, req resource.UpdateRequest, 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)...) } @@ -407,16 +433,15 @@ func (r *HarborResource) Delete(ctx context.Context, req resource.DeleteRequest, if state.ID.IsNull() || state.ID.IsUnknown() { return } - - deleteMode := "state_only" - if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { - deleteMode = "suspend" - } + 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 diff --git a/universal_rebuild/internal/resources_gen/88_k8s_ziti_controller_resource.go b/universal_rebuild/internal/resources_gen/88_k8s_ziti_controller_resource.go index 4ff2545..a5d7a93 100644 --- a/universal_rebuild/internal/resources_gen/88_k8s_ziti_controller_resource.go +++ b/universal_rebuild/internal/resources_gen/88_k8s_ziti_controller_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -11,7 +12,9 @@ import ( "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" ) @@ -31,6 +34,7 @@ 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"` @@ -59,9 +63,10 @@ func (r *K8sZitiControllerResource) Metadata(ctx context.Context, req resource.M func (r *K8sZitiControllerResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "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("web01"), 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) Не изменяется после создания."}, @@ -119,7 +124,10 @@ func (r *K8sZitiControllerResource) ModifyPlan(ctx context.Context, req resource } } if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { - if plan.Domain.ValueString() != state.Domain.ValueString() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) { resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") return } @@ -149,17 +157,23 @@ func (r *K8sZitiControllerResource) ModifyPlan(ctx context.Context, req resource } } if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { - if plan.ResourceRealm.ValueString() != state.ResourceRealm.ValueString() { + // 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() || config.ResourceRealm.IsUnknown() { + 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 @@ -176,8 +190,13 @@ func (r *K8sZitiControllerResource) ModifyPlan(ctx context.Context, req resource 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.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 88, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -192,7 +211,12 @@ func (r *K8sZitiControllerResource) Create(ctx context.Context, req resource.Cre } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 88, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ 294: resources_core.FormatInt64(data.ResourceCPU), @@ -207,6 +231,9 @@ func (r *K8sZitiControllerResource) Create(ctx context.Context, req resource.Cre 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()) @@ -325,6 +352,9 @@ func (r *K8sZitiControllerResource) Update(ctx context.Context, req resource.Upd if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -365,16 +395,15 @@ func (r *K8sZitiControllerResource) Delete(ctx context.Context, req resource.Del if state.ID.IsNull() || state.ID.IsUnknown() { return } - - deleteMode := "state_only" - if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { - deleteMode = "suspend" - } + 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 diff --git a/universal_rebuild/internal/resources_gen/89_flask_reconcile_action.go b/universal_rebuild/internal/resources_gen/89_flask_reconcile_action.go new file mode 100644 index 0000000..11d6549 --- /dev/null +++ b/universal_rebuild/internal/resources_gen/89_flask_reconcile_action.go @@ -0,0 +1,136 @@ +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/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: flask +// Action: reconcile + +var _ resource.Resource = &FlaskReconcileResource{} + +// FlaskReconcileResource triggers a one-time action. +type FlaskReconcileResource struct { + client *core.UniversalClient +} + +type FlaskReconcileModel struct { + ID types.String `tfsdk:"id"` + FlaskID types.String `tfsdk:"flask_id"` + RunID types.String `tfsdk:"run_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` +} + +func NewFlaskReconcileResource() resource.Resource { + return &FlaskReconcileResource{} +} + +func (r *FlaskReconcileResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_flask_reconcile" +} + +func (r *FlaskReconcileResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true}, + "flask_id": schema.StringAttribute{Required: true}, + "run_id": schema.StringAttribute{Required: true}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "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 *FlaskReconcileResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan FlaskReconcileModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.FlaskID.ValueString()) + runID := strings.TrimSpace(plan.RunID.ValueString()) + if instanceUID == "" || runID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра или run_id") + return + } + + params := 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, "reconcile", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "reconcile", runID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *FlaskReconcileResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state FlaskReconcileModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *FlaskReconcileResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan FlaskReconcileModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.FlaskID.ValueString()) + runID := strings.TrimSpace(plan.RunID.ValueString()) + if instanceUID == "" || runID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра или run_id") + return + } + + params := map[string]string{} + + if err := resources_core.RunOperationByCode(ctx, r.client, instanceUID, "reconcile", params); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "reconcile", runID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *FlaskReconcileResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // No-op: removing the resource does not trigger a remote action. +} + +func (r *FlaskReconcileResource) 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/universal_rebuild/internal/resources_gen/89_flask_redeploy_action.go b/universal_rebuild/internal/resources_gen/89_flask_redeploy_action.go new file mode 100644 index 0000000..2d63c9f --- /dev/null +++ b/universal_rebuild/internal/resources_gen/89_flask_redeploy_action.go @@ -0,0 +1,146 @@ +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/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: flask +// Action: redeploy + +var _ resource.Resource = &FlaskRedeployResource{} + +// FlaskRedeployResource triggers a one-time action. +type FlaskRedeployResource struct { + client *core.UniversalClient +} + +type FlaskRedeployModel struct { + ID types.String `tfsdk:"id"` + FlaskID types.String `tfsdk:"flask_id"` + RunID types.String `tfsdk:"run_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + StartupConfiguration types.String `tfsdk:"startup_configuration"` +} + +func NewFlaskRedeployResource() resource.Resource { + return &FlaskRedeployResource{} +} + +func (r *FlaskRedeployResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_flask_redeploy" +} + +func (r *FlaskRedeployResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true}, + "flask_id": schema.StringAttribute{Required: true}, + "run_id": schema.StringAttribute{Required: true}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "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}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *FlaskRedeployResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan FlaskRedeployModel + 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.FlaskID.ValueString()) + runID := strings.TrimSpace(plan.RunID.ValueString()) + if instanceUID == "" || runID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра или run_id") + return + } + + params := 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, "redeploy", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "redeploy", runID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *FlaskRedeployResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state FlaskRedeployModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *FlaskRedeployResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan FlaskRedeployModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.FlaskID.ValueString()) + runID := strings.TrimSpace(plan.RunID.ValueString()) + if instanceUID == "" || runID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра или run_id") + return + } + + params := map[string]string{ + "startupConfiguration": resources_core.FormatString(plan.StartupConfiguration), + } + + if err := resources_core.RunOperationByCode(ctx, r.client, instanceUID, "redeploy", params); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "redeploy", runID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *FlaskRedeployResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // No-op: removing the resource does not trigger a remote action. +} + +func (r *FlaskRedeployResource) 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/universal_rebuild/internal/resources_gen/89_flask_resource.go b/universal_rebuild/internal/resources_gen/89_flask_resource.go index 10cf82a..d0ee99f 100644 --- a/universal_rebuild/internal/resources_gen/89_flask_resource.go +++ b/universal_rebuild/internal/resources_gen/89_flask_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -10,8 +11,9 @@ import ( "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" ) @@ -31,14 +33,12 @@ type FlaskModel struct { ID types.String `tfsdk:"id"` ResourceName types.String `tfsdk:"resource_name"` OperationTimeout types.String `tfsdk:"operation_timeout"` - Domain types.String `tfsdk:"domain"` - GitPath types.String `tfsdk:"git_path"` - HealthPath types.String `tfsdk:"health_path"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration types.String `tfsdk:"access_configuration"` + AppConfiguration types.String `tfsdk:"app_configuration"` + ClusterConfiguration types.String `tfsdk:"cluster_configuration"` JsonEnv types.String `tfsdk:"json_env"` - 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"` + StartupConfiguration types.String `tfsdk:"startup_configuration"` SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` StateParams types.Map `tfsdk:"state_params"` @@ -61,17 +61,15 @@ func (r *FlaskResource) Metadata(ctx context.Context, req resource.MetadataReque func (r *FlaskResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, "resource_name": schema.StringAttribute{Required: true}, "operation_timeout": schema.StringAttribute{Optional: true}, - "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("web01"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."}, - "git_path": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("https://github.com/Foxyhhd/Baldurs-Gate-test.git"), MarkdownDescription: "Путь до Git, который будет webroot для сайта"}, - "health_path": schema.StringAttribute{Optional: true, MarkdownDescription: "Путь до healtcheck для запуска пода в Kubernetes, который отдаст 200 код Не изменяется после создания."}, - "json_env": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("{ "param": "value" }"), MarkdownDescription: "Массив переменных, будут переданы в env приложения"}, - "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(300), MarkdownDescription: "Квота ядра пода (milicores)"}, - "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов"}, - "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(256), MarkdownDescription: "Квота памяти пода (MBytes)"}, - "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания."}, + "app_configuration": schema.StringAttribute{Required: true}, + "cluster_configuration": schema.StringAttribute{Required: true}, + "json_env": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("{}")}, + "startup_configuration": 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}, @@ -122,28 +120,52 @@ func (r *FlaskResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanR return } } - if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { - if plan.Domain.ValueString() != state.Domain.ValueString() { - resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") + if !plan.AccessConfiguration.IsNull() && !plan.AccessConfiguration.IsUnknown() && !state.AccessConfiguration.IsNull() && !state.AccessConfiguration.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.AccessConfiguration.ValueString(), state.AccessConfiguration.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить access_configuration", "Параметр задается при создании и не может быть изменен.") return } } - if !plan.HealthPath.IsNull() && !plan.HealthPath.IsUnknown() && !state.HealthPath.IsNull() && !state.HealthPath.IsUnknown() { - if plan.HealthPath.ValueString() != state.HealthPath.ValueString() { - resp.Diagnostics.AddError("Нельзя изменить health_path", "Параметр задается при создании и не может быть изменен.") - return - } - } - if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { - if plan.ResourceRealm.ValueString() != state.ResourceRealm.ValueString() { - resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + if !plan.StartupConfiguration.IsNull() && !plan.StartupConfiguration.IsUnknown() && !state.StartupConfiguration.IsNull() && !state.StartupConfiguration.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.StartupConfiguration.ValueString(), state.StartupConfiguration.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить startup_configuration", "Параметр задается при создании и не может быть изменен.") return } } return } - if config.ResourceRealm.IsNull() || config.ResourceRealm.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + if config.AccessConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "access_configuration is required.") + return + } + if config.AccessConfiguration.IsUnknown() { + return + } + if config.AppConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "app_configuration is required.") + return + } + if config.AppConfiguration.IsUnknown() { + return + } + if config.ClusterConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "cluster_configuration is required.") + return + } + if config.ClusterConfiguration.IsUnknown() { + return + } + if config.StartupConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "startup_configuration is required.") + return + } + if config.StartupConfiguration.IsUnknown() { return } @@ -155,17 +177,16 @@ func (r *FlaskResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanR adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() } params := map[int]string{ - 248: resources_core.FormatString(config.Domain), - 249: resources_core.FormatString(config.ResourceRealm), - 250: resources_core.FormatInt64(config.ResourceCPU), - 251: resources_core.FormatInt64(config.ResourceMemory), - 252: resources_core.FormatInt64(config.ResourceInstances), - 253: resources_core.FormatString(config.GitPath), - 254: resources_core.FormatString(config.JsonEnv), - 255: resources_core.FormatString(config.HealthPath), + 1037: resources_core.FormatString(config.StartupConfiguration), + 1038: resources_core.FormatString(config.ClusterConfiguration), + 1039: resources_core.FormatString(config.AccessConfiguration), + 1040: resources_core.FormatString(config.AppConfiguration), + 1041: resources_core.FormatString(config.JsonEnv), } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} - resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 89, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -174,29 +195,43 @@ func (r *FlaskResource) Create(ctx context.Context, req resource.CreateRequest, if resp.Diagnostics.HasError() { return } - if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + if data.AccessConfiguration.IsNull() || data.AccessConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "access_configuration is required.") + return + } + if data.AppConfiguration.IsNull() || data.AppConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "app_configuration is required.") + return + } + if data.ClusterConfiguration.IsNull() || data.ClusterConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "cluster_configuration is required.") + return + } + if data.StartupConfiguration.IsNull() || data.StartupConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "startup_configuration is required.") return } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 89, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ - 248: resources_core.FormatString(data.Domain), - 249: resources_core.FormatString(data.ResourceRealm), - 250: resources_core.FormatInt64(data.ResourceCPU), - 251: resources_core.FormatInt64(data.ResourceMemory), - 252: resources_core.FormatInt64(data.ResourceInstances), - 253: resources_core.FormatString(data.GitPath), - 254: resources_core.FormatString(data.JsonEnv), - 255: resources_core.FormatString(data.HealthPath), + 1037: resources_core.FormatString(data.StartupConfiguration), + 1038: resources_core.FormatString(data.ClusterConfiguration), + 1039: resources_core.FormatString(data.AccessConfiguration), + 1040: resources_core.FormatString(data.AppConfiguration), + 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()) @@ -214,14 +249,11 @@ func (r *FlaskResource) Create(ctx context.Context, req resource.CreateRequest, {Code: "vault_user_path"}, {Code: "vault_fields"}, }, []resources_core.InputField{ - {Code: "domain", Field: "Domain", Type: "string"}, - {Code: "gitPath", Field: "GitPath", Type: "string"}, - {Code: "healthPath", Field: "HealthPath", Type: "string"}, + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "string"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "string"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "string"}, {Code: "jsonEnv", Field: "JsonEnv", 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"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "string"}, }) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -262,14 +294,11 @@ func (r *FlaskResource) Read(ctx context.Context, req resource.ReadRequest, resp {Code: "vault_user_path"}, {Code: "vault_fields"}, }, []resources_core.InputField{ - {Code: "domain", Field: "Domain", Type: "string"}, - {Code: "gitPath", Field: "GitPath", Type: "string"}, - {Code: "healthPath", Field: "HealthPath", Type: "string"}, + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "string"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "string"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "string"}, {Code: "jsonEnv", Field: "JsonEnv", 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"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "string"}, }) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -299,37 +328,19 @@ func (r *FlaskResource) Update(ctx context.Context, req resource.UpdateRequest, hasServiceParamChanges := false if !hasServiceParamChanges { - if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() { + if plan.ClusterConfiguration.IsNull() != state.ClusterConfiguration.IsNull() || plan.ClusterConfiguration.IsUnknown() != state.ClusterConfiguration.IsUnknown() { hasServiceParamChanges = true - } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { - if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + } else if !plan.ClusterConfiguration.IsNull() && !plan.ClusterConfiguration.IsUnknown() && !state.ClusterConfiguration.IsNull() && !state.ClusterConfiguration.IsUnknown() { + if !strings.EqualFold(plan.ClusterConfiguration.ValueString(), state.ClusterConfiguration.ValueString()) { hasServiceParamChanges = true } } } if !hasServiceParamChanges { - if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() { + if plan.AppConfiguration.IsNull() != state.AppConfiguration.IsNull() || plan.AppConfiguration.IsUnknown() != state.AppConfiguration.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 - } - } - } - if !hasServiceParamChanges { - if plan.GitPath.IsNull() != state.GitPath.IsNull() || plan.GitPath.IsUnknown() != state.GitPath.IsUnknown() { - hasServiceParamChanges = true - } else if !plan.GitPath.IsNull() && !plan.GitPath.IsUnknown() && !state.GitPath.IsNull() && !state.GitPath.IsUnknown() { - if plan.GitPath.ValueString() != state.GitPath.ValueString() { + } else if !plan.AppConfiguration.IsNull() && !plan.AppConfiguration.IsUnknown() && !state.AppConfiguration.IsNull() && !state.AppConfiguration.IsUnknown() { + if !strings.EqualFold(plan.AppConfiguration.ValueString(), state.AppConfiguration.ValueString()) { hasServiceParamChanges = true } } @@ -338,7 +349,7 @@ func (r *FlaskResource) Update(ctx context.Context, req resource.UpdateRequest, 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 plan.JsonEnv.ValueString() != state.JsonEnv.ValueString() { + if !strings.EqualFold(plan.JsonEnv.ValueString(), state.JsonEnv.ValueString()) { hasServiceParamChanges = true } } @@ -359,17 +370,18 @@ func (r *FlaskResource) Update(ctx context.Context, req resource.UpdateRequest, } params := map[int]string{ - 451: resources_core.FormatInt64(plan.ResourceCPU), - 452: resources_core.FormatInt64(plan.ResourceMemory), - 453: resources_core.FormatInt64(plan.ResourceInstances), - 454: resources_core.FormatString(plan.GitPath), - 455: resources_core.FormatString(plan.JsonEnv), + 1042: resources_core.FormatString(plan.ClusterConfiguration), + 1043: resources_core.FormatString(plan.AppConfiguration), + 1044: resources_core.FormatString(plan.JsonEnv), } operationTimeout := "" if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -386,14 +398,11 @@ func (r *FlaskResource) Update(ctx context.Context, req resource.UpdateRequest, {Code: "vault_user_path"}, {Code: "vault_fields"}, }, []resources_core.InputField{ - {Code: "domain", Field: "Domain", Type: "string"}, - {Code: "gitPath", Field: "GitPath", Type: "string"}, - {Code: "healthPath", Field: "HealthPath", Type: "string"}, + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "string"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "string"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "string"}, {Code: "jsonEnv", Field: "JsonEnv", 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"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "string"}, }) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -412,7 +421,6 @@ func (r *FlaskResource) Delete(ctx context.Context, req resource.DeleteRequest, if state.ID.IsNull() || state.ID.IsUnknown() { return } - deleteMode := "state_only" if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { deleteMode = "suspend" @@ -422,6 +430,9 @@ func (r *FlaskResource) Delete(ctx context.Context, req resource.DeleteRequest, 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 diff --git a/universal_rebuild/internal/resources_gen/90_postgres_backup_resource.go b/universal_rebuild/internal/resources_gen/90_postgres_backup_resource.go new file mode 100644 index 0000000..83c0223 --- /dev/null +++ b/universal_rebuild/internal/resources_gen/90_postgres_backup_resource.go @@ -0,0 +1,330 @@ +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(false)}, + "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 known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект подтверждён в state_out, выполняется усыновление") + 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 { + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект отсутствует и будет пропущен: "+targetValue) + return + } + resp.Diagnostics.AddError("Подресурс не найден", "Объект отсутствует: "+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/universal_rebuild/internal/resources_gen/90_postgres_database_resource.go b/universal_rebuild/internal/resources_gen/90_postgres_database_resource.go index 79ea316..fe86da9 100644 --- a/universal_rebuild/internal/resources_gen/90_postgres_database_resource.go +++ b/universal_rebuild/internal/resources_gen/90_postgres_database_resource.go @@ -32,6 +32,7 @@ type PostgresDatabaseModel struct { 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"` } @@ -46,7 +47,7 @@ func (r *PostgresDatabaseResource) Metadata(ctx context.Context, req resource.Me func (r *PostgresDatabaseResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, "postgres_id": schema.StringAttribute{ Required: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, @@ -54,6 +55,7 @@ func (r *PostgresDatabaseResource) Schema(ctx context.Context, req resource.Sche "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(false)}, "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()}}, } @@ -131,8 +133,28 @@ func (r *PostgresDatabaseResource) Create(ctx context.Context, req resource.Crea if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { operationTimeout = plan.OperationTimeout.ValueString() } - if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_database", params, operationTimeout); err != nil { - if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(err) { + 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 { @@ -149,7 +171,7 @@ func (r *PostgresDatabaseResource) Create(ctx context.Context, req resource.Crea resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") return } - resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) return } if targetValue != "" { @@ -214,6 +236,9 @@ func (r *PostgresDatabaseResource) Update(ctx context.Context, req resource.Upda if !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 @@ -286,6 +311,9 @@ func (r *PostgresDatabaseResource) Delete(ctx context.Context, req resource.Dele 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 != "" { diff --git a/universal_rebuild/internal/resources_gen/90_postgres_reconcile_action.go b/universal_rebuild/internal/resources_gen/90_postgres_reconcile_action.go new file mode 100644 index 0000000..5a9e4f6 --- /dev/null +++ b/universal_rebuild/internal/resources_gen/90_postgres_reconcile_action.go @@ -0,0 +1,136 @@ +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/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: postgres +// Action: reconcile + +var _ resource.Resource = &PostgresReconcileResource{} + +// PostgresReconcileResource triggers a one-time action. +type PostgresReconcileResource struct { + client *core.UniversalClient +} + +type PostgresReconcileModel struct { + ID types.String `tfsdk:"id"` + PostgresID types.String `tfsdk:"postgres_id"` + RunID types.String `tfsdk:"run_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` +} + +func NewPostgresReconcileResource() resource.Resource { + return &PostgresReconcileResource{} +} + +func (r *PostgresReconcileResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_postgres_reconcile" +} + +func (r *PostgresReconcileResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true}, + "postgres_id": schema.StringAttribute{Required: true}, + "run_id": schema.StringAttribute{Required: true}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "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 *PostgresReconcileResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan PostgresReconcileModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.PostgresID.ValueString()) + runID := strings.TrimSpace(plan.RunID.ValueString()) + if instanceUID == "" || runID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра или run_id") + return + } + + params := 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, "reconcile", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "reconcile", runID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *PostgresReconcileResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state PostgresReconcileModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *PostgresReconcileResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan PostgresReconcileModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.PostgresID.ValueString()) + runID := strings.TrimSpace(plan.RunID.ValueString()) + if instanceUID == "" || runID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра или run_id") + return + } + + params := map[string]string{} + + if err := resources_core.RunOperationByCode(ctx, r.client, instanceUID, "reconcile", params); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "reconcile", runID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *PostgresReconcileResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // No-op: removing the resource does not trigger a remote action. +} + +func (r *PostgresReconcileResource) 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/universal_rebuild/internal/resources_gen/90_postgres_recovery_action.go b/universal_rebuild/internal/resources_gen/90_postgres_recovery_action.go index 51004dc..87c21a8 100644 --- a/universal_rebuild/internal/resources_gen/90_postgres_recovery_action.go +++ b/universal_rebuild/internal/resources_gen/90_postgres_recovery_action.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -25,15 +25,19 @@ type PostgresRecoveryResource struct { } type PostgresRecoveryModel struct { - ID types.String `tfsdk:"id"` - PostgresID types.String `tfsdk:"postgres_id"` - RunID types.String `tfsdk:"run_id"` - OperationTimeout types.String `tfsdk:"operation_timeout"` - ResourceInstances types.Int64 `tfsdk:"resource_instances"` - ResourceMemory types.Int64 `tfsdk:"resource_memory"` - ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` - ResourceDisk types.Int64 `tfsdk:"resource_disk"` - ResourceRealm types.String `tfsdk:"resource_realm"` + ID types.String `tfsdk:"id"` + PostgresID types.String `tfsdk:"postgres_id"` + RunID types.String `tfsdk:"run_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + StartupConfiguration types.String `tfsdk:"startup_configuration"` + ClusterConfiguration types.String `tfsdk:"cluster_configuration"` + AccessConfiguration types.String `tfsdk:"access_configuration"` + PostgresConfiguration types.String `tfsdk:"postgres_configuration"` + PostgresConf types.String `tfsdk:"postgres_conf"` + BackupConfiguration types.String `tfsdk:"backup_configuration"` + AutoscaleConfiguration types.String `tfsdk:"autoscale_configuration"` } func NewPostgresRecoveryResource() resource.Resource { @@ -46,15 +50,19 @@ func (r *PostgresRecoveryResource) Metadata(ctx context.Context, req resource.Me func (r *PostgresRecoveryResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, - "postgres_id": schema.StringAttribute{Required: true}, - "run_id": schema.StringAttribute{Required: true}, - "operation_timeout": schema.StringAttribute{Optional: true}, - "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_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Квота ядра пода (Указывается в mili)
"}, - "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Квота диска (Указывается в GIGAbytes)"}, - "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания"}, + "id": schema.StringAttribute{Computed: true}, + "postgres_id": schema.StringAttribute{Required: true}, + "run_id": schema.StringAttribute{Required: true}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "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}, + "cluster_configuration": schema.StringAttribute{Required: true}, + "access_configuration": schema.StringAttribute{Required: true}, + "postgres_configuration": schema.StringAttribute{Required: true}, + "postgres_conf": schema.StringAttribute{Optional: true}, + "backup_configuration": schema.StringAttribute{Required: true}, + "autoscale_configuration": schema.StringAttribute{Required: true}, } resp.Schema = schema.Schema{Attributes: attrs} } @@ -65,8 +73,28 @@ func (r *PostgresRecoveryResource) Create(ctx context.Context, req resource.Crea if resp.Diagnostics.HasError() { return } - if plan.ResourceRealm.IsNull() || plan.ResourceRealm.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + if plan.StartupConfiguration.IsNull() || plan.StartupConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "startup_configuration is required.") + return + } + if plan.ClusterConfiguration.IsNull() || plan.ClusterConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "cluster_configuration is required.") + return + } + if plan.AccessConfiguration.IsNull() || plan.AccessConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "access_configuration is required.") + return + } + if plan.PostgresConfiguration.IsNull() || plan.PostgresConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "postgres_configuration is required.") + return + } + if plan.BackupConfiguration.IsNull() || plan.BackupConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "backup_configuration is required.") + return + } + if plan.AutoscaleConfiguration.IsNull() || plan.AutoscaleConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "autoscale_configuration is required.") return } @@ -78,17 +106,22 @@ func (r *PostgresRecoveryResource) Create(ctx context.Context, req resource.Crea } params := map[string]string{ - "resourceInstances": resources_core.FormatInt64(plan.ResourceInstances), - "resourceMemory": resources_core.FormatInt64(plan.ResourceMemory), - "resourceCPU": resources_core.FormatInt64(plan.ResourceCPU), - "resourceDisk": resources_core.FormatInt64(plan.ResourceDisk), - "resourceRealm": resources_core.FormatString(plan.ResourceRealm), + "startupConfiguration": resources_core.FormatString(plan.StartupConfiguration), + "clusterConfiguration": resources_core.FormatString(plan.ClusterConfiguration), + "accessConfiguration": resources_core.FormatString(plan.AccessConfiguration), + "postgresConfiguration": resources_core.FormatString(plan.PostgresConfiguration), + "postgresConf": resources_core.FormatString(plan.PostgresConf), + "backupConfiguration": resources_core.FormatString(plan.BackupConfiguration), + "autoscaleConfiguration": resources_core.FormatString(plan.AutoscaleConfiguration), } operationTimeout := "" if !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, "recovery", params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -122,11 +155,13 @@ func (r *PostgresRecoveryResource) Update(ctx context.Context, req resource.Upda } params := map[string]string{ - "resourceInstances": resources_core.FormatInt64(plan.ResourceInstances), - "resourceMemory": resources_core.FormatInt64(plan.ResourceMemory), - "resourceCPU": resources_core.FormatInt64(plan.ResourceCPU), - "resourceDisk": resources_core.FormatInt64(plan.ResourceDisk), - "resourceRealm": resources_core.FormatString(plan.ResourceRealm), + "startupConfiguration": resources_core.FormatString(plan.StartupConfiguration), + "clusterConfiguration": resources_core.FormatString(plan.ClusterConfiguration), + "accessConfiguration": resources_core.FormatString(plan.AccessConfiguration), + "postgresConfiguration": resources_core.FormatString(plan.PostgresConfiguration), + "postgresConf": resources_core.FormatString(plan.PostgresConf), + "backupConfiguration": resources_core.FormatString(plan.BackupConfiguration), + "autoscaleConfiguration": resources_core.FormatString(plan.AutoscaleConfiguration), } if err := resources_core.RunOperationByCode(ctx, r.client, instanceUID, "recovery", params); err != nil { diff --git a/universal_rebuild/internal/resources_gen/90_postgres_resource.go b/universal_rebuild/internal/resources_gen/90_postgres_resource.go index bf9c85b..94da64a 100644 --- a/universal_rebuild/internal/resources_gen/90_postgres_resource.go +++ b/universal_rebuild/internal/resources_gen/90_postgres_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -10,8 +11,8 @@ import ( "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/stringdefault" + "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" ) @@ -28,40 +29,27 @@ type PostgresResource struct { } type PostgresModel struct { - ID types.String `tfsdk:"id"` - ResourceName types.String `tfsdk:"resource_name"` - OperationTimeout types.String `tfsdk:"operation_timeout"` - AllowNoSSL types.Bool `tfsdk:"allow_no_s_s_l"` - AppVersion types.String `tfsdk:"app_version"` - AutoScale types.Bool `tfsdk:"auto_scale"` - AutoScalePercentage types.Int64 `tfsdk:"auto_scale_percentage"` - AutoScaleQuotaGb types.String `tfsdk:"auto_scale_quota_gb"` - AutoScaleTechWindow types.Int64 `tfsdk:"auto_scale_tech_window"` - EnablePgPoolerMaster types.Bool `tfsdk:"enable_pg_pooler_master"` - EnablePgPoolerSlave types.Bool `tfsdk:"enable_pg_pooler_slave"` - ExtBACKUPNUMTORETAIN types.Int64 `tfsdk:"ext__b_a_c_k_u_p__n_u_m__t_o__r_e_t_a_i_n"` - ExtBACKUPSCHEDULE types.String `tfsdk:"ext__b_a_c_k_u_p__s_c_h_e_d_u_l_e"` - IpSpaceNameMaster types.String `tfsdk:"ip_space_name_master"` - IpSpaceNameSlave types.String `tfsdk:"ip_space_name_slave"` - JsonParameters types.String `tfsdk:"json_parameters"` - NeedExternalAddressMaster types.Bool `tfsdk:"need_external_address_master"` - NeedExternalAddressSlave types.Bool `tfsdk:"need_external_address_slave"` - ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` - ResourceDisk types.String `tfsdk:"resource_disk"` - 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"` + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration types.String `tfsdk:"access_configuration"` + AutoscaleConfiguration types.String `tfsdk:"autoscale_configuration"` + BackupConfiguration types.String `tfsdk:"backup_configuration"` + ClusterConfiguration types.String `tfsdk:"cluster_configuration"` + PostgresConf types.String `tfsdk:"postgres_conf"` + PostgresConfiguration types.String `tfsdk:"postgres_configuration"` + StartupConfiguration types.String `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 { @@ -74,40 +62,27 @@ func (r *PostgresResource) Metadata(ctx context.Context, req resource.MetadataRe func (r *PostgresResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, - "resource_name": schema.StringAttribute{Required: true}, - "operation_timeout": schema.StringAttribute{Optional: true}, - "allow_no_s_s_l": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "По умолчанию подключение обязательное (sslmode=require). Выставление в true отключит этот параметр"}, - "app_version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("17"), MarkdownDescription: "Версия PostgreSQL"}, - "auto_scale": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает автоскейлинг PV. При включении параметра необходимо настроить autoScalePercentage и autoScaleTechWindow. Алерт при срабатывании вызывает modify у текущей услуги Не изменяется после создания."}, - "auto_scale_percentage": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Требует включения autoScale. Позволяет указать в процентах расширение от текущего макс объема Не изменяется после создания."}, - "auto_scale_quota_gb": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("1"), MarkdownDescription: "Квота для autoScale Не изменяется после создания."}, - "auto_scale_tech_window": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "Требует включения autoScale. Окно обновления (пока не реализовано). Указывается час в виде Integer (0 / 5 / 12 / 23) Не изменяется после создания."}, - "enable_pg_pooler_master": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает pgPooler (на запись). Будет создан новый под"}, - "enable_pg_pooler_slave": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает pgPooler (на чтение). Будет создан новый под"}, - "ext__b_a_c_k_u_p__n_u_m__t_o__r_e_t_a_i_n": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(7), MarkdownDescription: "Не обязательный. Количество сохраняемых бекапов"}, - "ext__b_a_c_k_u_p__s_c_h_e_d_u_l_e": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("0 0 * * *"), MarkdownDescription: "Не обязательный. Время запуска бекапа"}, - "ip_space_name_master": schema.StringAttribute{Optional: true, MarkdownDescription: "Имя ipSpace для публикации VIP master. Не может быть пустым, если включен needExternalAddressMaster. Пример: `internet-no-antiddos-v1`"}, - "ip_space_name_slave": schema.StringAttribute{Optional: true, MarkdownDescription: "Имя ipSpace для публикации VIP slave. Не может быть пустым, если включен needExternalAddressSlave"}, - "json_parameters": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("{ \"log_connections\": \"off\", \"log_disconnections\": \"off\" }"), MarkdownDescription: "Массив переменных, более подробнее про настройку можно посмотреть https://www.postgresql.org/docs/current/runtime-config.html"}, - "need_external_address_master": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Выделяет внешний (белый) IP для доступа к master"}, - "need_external_address_slave": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Выделяет внешний (белый) IP для доступа к slave. Будет работать, если slave в принципе есть"}, - "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Квота ядра пода (milicores)"}, - "resource_disk": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("10"), 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: "Платформа для развертывания Не изменяется после создания."}, - "s3_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Экземпляр (учетная запись) S3 для резервного копирования. Резервное копирование обязательно Не изменяется после создания."}, - "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}, + "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.StringAttribute{Required: true}, + "autoscale_configuration": schema.StringAttribute{Required: true}, + "backup_configuration": schema.StringAttribute{Required: true}, + "cluster_configuration": schema.StringAttribute{Required: true}, + "postgres_conf": schema.StringAttribute{Optional: true}, + "postgres_configuration": schema.StringAttribute{Required: true}, + "startup_configuration": 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} @@ -142,72 +117,63 @@ func (r *PostgresResource) ModifyPlan(ctx context.Context, req resource.ModifyPl if resp.Diagnostics.HasError() { return } - planChanged := false - if !plan.S3Uid.IsNull() && !plan.S3Uid.IsUnknown() { - resolvedS3Uid, err := r.client.ResolveRefSvcParamValue(ctx, 12, plan.S3Uid.ValueString()) - if err != nil { - resp.Diagnostics.AddWarning("Failed to resolve s3_uid", err.Error()) - } else if resolvedS3Uid != "" && resolvedS3Uid != plan.S3Uid.ValueString() { - plan.S3Uid = types.StringValue(resolvedS3Uid) - planChanged = true - } - } - if planChanged { - resp.Diagnostics.Append(resp.Plan.Set(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.AutoScale.IsNull() && !plan.AutoScale.IsUnknown() && !state.AutoScale.IsNull() && !state.AutoScale.IsUnknown() { - if plan.AutoScale.ValueBool() != state.AutoScale.ValueBool() { - resp.Diagnostics.AddError("Нельзя изменить auto_scale", "Параметр задается при создании и не может быть изменен.") - return - } - } - if !plan.AutoScalePercentage.IsNull() && !plan.AutoScalePercentage.IsUnknown() && !state.AutoScalePercentage.IsNull() && !state.AutoScalePercentage.IsUnknown() { - if plan.AutoScalePercentage.ValueInt64() != state.AutoScalePercentage.ValueInt64() { - resp.Diagnostics.AddError("Нельзя изменить auto_scale_percentage", "Параметр задается при создании и не может быть изменен.") - return - } - } - if !plan.AutoScaleQuotaGb.IsNull() && !plan.AutoScaleQuotaGb.IsUnknown() && !state.AutoScaleQuotaGb.IsNull() && !state.AutoScaleQuotaGb.IsUnknown() { - if plan.AutoScaleQuotaGb.ValueString() != state.AutoScaleQuotaGb.ValueString() { - resp.Diagnostics.AddError("Нельзя изменить auto_scale_quota_gb", "Параметр задается при создании и не может быть изменен.") - return - } - } - if !plan.AutoScaleTechWindow.IsNull() && !plan.AutoScaleTechWindow.IsUnknown() && !state.AutoScaleTechWindow.IsNull() && !state.AutoScaleTechWindow.IsUnknown() { - if plan.AutoScaleTechWindow.ValueInt64() != state.AutoScaleTechWindow.ValueInt64() { - resp.Diagnostics.AddError("Нельзя изменить auto_scale_tech_window", "Параметр задается при создании и не может быть изменен.") - return - } - } - if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { - if 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() { - if plan.S3Uid.ValueString() != state.S3Uid.ValueString() { - resp.Diagnostics.AddError("Нельзя изменить s3_uid", "Параметр задается при создании и не может быть изменен.") + if !plan.StartupConfiguration.IsNull() && !plan.StartupConfiguration.IsUnknown() && !state.StartupConfiguration.IsNull() && !state.StartupConfiguration.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.StartupConfiguration.ValueString(), state.StartupConfiguration.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить startup_configuration", "Параметр задается при создании и не может быть изменен.") return } } return } - if config.ResourceRealm.IsNull() || config.ResourceRealm.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + if config.AccessConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "access_configuration is required.") return } - if config.S3Uid.IsNull() || config.S3Uid.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "s3_uid is required.") + if config.AccessConfiguration.IsUnknown() { + return + } + if config.AutoscaleConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "autoscale_configuration is required.") + return + } + if config.AutoscaleConfiguration.IsUnknown() { + return + } + if config.BackupConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "backup_configuration is required.") + return + } + if config.BackupConfiguration.IsUnknown() { + return + } + if config.ClusterConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "cluster_configuration is required.") + return + } + if config.ClusterConfiguration.IsUnknown() { + return + } + if config.PostgresConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "postgres_configuration is required.") + return + } + if config.PostgresConfiguration.IsUnknown() { + return + } + if config.StartupConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "startup_configuration is required.") + return + } + if config.StartupConfiguration.IsUnknown() { return } @@ -218,39 +184,19 @@ func (r *PostgresResource) ModifyPlan(ctx context.Context, req resource.ModifyPl 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{ - 23: resources_core.FormatString(config.S3Uid), - 80: resources_core.FormatInt64(config.ResourceInstances), - 81: resources_core.FormatInt64(config.ResourceMemory), - 82: resources_core.FormatInt64(config.ResourceCPU), - 83: resources_core.FormatString(config.ResourceDisk), - 102: resources_core.FormatString(config.ResourceRealm), - 145: resources_core.FormatBool(config.NeedExternalAddressMaster), - 146: resources_core.FormatBool(config.NeedExternalAddressSlave), - 265: resources_core.FormatString(config.ExtBACKUPSCHEDULE), - 266: resources_core.FormatInt64(config.ExtBACKUPNUMTORETAIN), - 310: resources_core.FormatString(config.AppVersion), - 311: resources_core.FormatString(config.JsonParameters), - 312: resources_core.FormatBool(config.EnablePgPoolerMaster), - 313: resources_core.FormatBool(config.EnablePgPoolerSlave), - 314: resources_core.FormatBool(config.AllowNoSSL), - 329: resources_core.FormatBool(config.AutoScale), - 330: resources_core.FormatInt64(config.AutoScalePercentage), - 331: resources_core.FormatInt64(config.AutoScaleTechWindow), - 337: resources_core.FormatString(config.IpSpaceNameMaster), - 338: resources_core.FormatString(config.IpSpaceNameSlave), - 339: resources_core.FormatString(config.AutoScaleQuotaGb), + 788: resources_core.FormatString(config.ClusterConfiguration), + 789: resources_core.FormatString(config.StartupConfiguration), + 790: resources_core.FormatString(config.AccessConfiguration), + 791: resources_core.FormatString(config.PostgresConfiguration), + 792: resources_core.FormatString(config.PostgresConf), + 793: resources_core.FormatString(config.BackupConfiguration), + 794: resources_core.FormatString(config.AutoscaleConfiguration), } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} - resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 90, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -259,46 +205,53 @@ func (r *PostgresResource) Create(ctx context.Context, req resource.CreateReques if resp.Diagnostics.HasError() { return } - if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + if data.AccessConfiguration.IsNull() || data.AccessConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "access_configuration is required.") return } - if data.S3Uid.IsNull() || data.S3Uid.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "s3_uid is required.") + if data.AutoscaleConfiguration.IsNull() || data.AutoscaleConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "autoscale_configuration is required.") + return + } + if data.BackupConfiguration.IsNull() || data.BackupConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "backup_configuration is required.") + return + } + if data.ClusterConfiguration.IsNull() || data.ClusterConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "cluster_configuration is required.") + return + } + if data.PostgresConfiguration.IsNull() || data.PostgresConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "postgres_configuration is required.") + return + } + if data.StartupConfiguration.IsNull() || data.StartupConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "startup_configuration is required.") return } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 90, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ - 23: resources_core.FormatString(data.S3Uid), - 80: resources_core.FormatInt64(data.ResourceInstances), - 81: resources_core.FormatInt64(data.ResourceMemory), - 82: resources_core.FormatInt64(data.ResourceCPU), - 83: resources_core.FormatString(data.ResourceDisk), - 102: resources_core.FormatString(data.ResourceRealm), - 145: resources_core.FormatBool(data.NeedExternalAddressMaster), - 146: resources_core.FormatBool(data.NeedExternalAddressSlave), - 265: resources_core.FormatString(data.ExtBACKUPSCHEDULE), - 266: resources_core.FormatInt64(data.ExtBACKUPNUMTORETAIN), - 310: resources_core.FormatString(data.AppVersion), - 311: resources_core.FormatString(data.JsonParameters), - 312: resources_core.FormatBool(data.EnablePgPoolerMaster), - 313: resources_core.FormatBool(data.EnablePgPoolerSlave), - 314: resources_core.FormatBool(data.AllowNoSSL), - 329: resources_core.FormatBool(data.AutoScale), - 330: resources_core.FormatInt64(data.AutoScalePercentage), - 331: resources_core.FormatInt64(data.AutoScaleTechWindow), - 337: resources_core.FormatString(data.IpSpaceNameMaster), - 338: resources_core.FormatString(data.IpSpaceNameSlave), - 339: resources_core.FormatString(data.AutoScaleQuotaGb), + 788: resources_core.FormatString(data.ClusterConfiguration), + 789: resources_core.FormatString(data.StartupConfiguration), + 790: resources_core.FormatString(data.AccessConfiguration), + 791: resources_core.FormatString(data.PostgresConfiguration), + 792: resources_core.FormatString(data.PostgresConf), + 793: resources_core.FormatString(data.BackupConfiguration), + 794: resources_core.FormatString(data.AutoscaleConfiguration), } 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()) @@ -316,26 +269,13 @@ func (r *PostgresResource) Create(ctx context.Context, req resource.CreateReques {Code: "vault_user_path"}, {Code: "vault_fields"}, }, []resources_core.InputField{ - {Code: "allowNoSSL", Field: "AllowNoSSL", Type: "bool"}, - {Code: "appVersion", Field: "AppVersion", Type: "string"}, - {Code: "autoScale", Field: "AutoScale", Type: "bool"}, - {Code: "autoScalePercentage", Field: "AutoScalePercentage", Type: "int64"}, - {Code: "autoScaleQuotaGb", Field: "AutoScaleQuotaGb", Type: "string"}, - {Code: "autoScaleTechWindow", Field: "AutoScaleTechWindow", Type: "int64"}, - {Code: "enablePgPoolerMaster", Field: "EnablePgPoolerMaster", Type: "bool"}, - {Code: "enablePgPoolerSlave", Field: "EnablePgPoolerSlave", Type: "bool"}, - {Code: "ext_BACKUP_NUM_TO_RETAIN", Field: "ExtBACKUPNUMTORETAIN", Type: "int64"}, - {Code: "ext_BACKUP_SCHEDULE", Field: "ExtBACKUPSCHEDULE", Type: "string"}, - {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"}, - {Code: "ipSpaceNameSlave", Field: "IpSpaceNameSlave", Type: "string"}, - {Code: "jsonParameters", Field: "JsonParameters", Type: "string"}, - {Code: "needExternalAddressMaster", Field: "NeedExternalAddressMaster", Type: "bool"}, - {Code: "needExternalAddressSlave", Field: "NeedExternalAddressSlave", Type: "bool"}, - {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, - {Code: "resourceDisk", Field: "ResourceDisk", Type: "string"}, - {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, - {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, - {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "string"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "string"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "string"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "string"}, + {Code: "postgresConf", Field: "PostgresConf", Type: "string"}, + {Code: "postgresConfiguration", Field: "PostgresConfiguration", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "string"}, }) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -376,26 +316,13 @@ func (r *PostgresResource) Read(ctx context.Context, req resource.ReadRequest, r {Code: "vault_user_path"}, {Code: "vault_fields"}, }, []resources_core.InputField{ - {Code: "allowNoSSL", Field: "AllowNoSSL", Type: "bool"}, - {Code: "appVersion", Field: "AppVersion", Type: "string"}, - {Code: "autoScale", Field: "AutoScale", Type: "bool"}, - {Code: "autoScalePercentage", Field: "AutoScalePercentage", Type: "int64"}, - {Code: "autoScaleQuotaGb", Field: "AutoScaleQuotaGb", Type: "string"}, - {Code: "autoScaleTechWindow", Field: "AutoScaleTechWindow", Type: "int64"}, - {Code: "enablePgPoolerMaster", Field: "EnablePgPoolerMaster", Type: "bool"}, - {Code: "enablePgPoolerSlave", Field: "EnablePgPoolerSlave", Type: "bool"}, - {Code: "ext_BACKUP_NUM_TO_RETAIN", Field: "ExtBACKUPNUMTORETAIN", Type: "int64"}, - {Code: "ext_BACKUP_SCHEDULE", Field: "ExtBACKUPSCHEDULE", Type: "string"}, - {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"}, - {Code: "ipSpaceNameSlave", Field: "IpSpaceNameSlave", Type: "string"}, - {Code: "jsonParameters", Field: "JsonParameters", Type: "string"}, - {Code: "needExternalAddressMaster", Field: "NeedExternalAddressMaster", Type: "bool"}, - {Code: "needExternalAddressSlave", Field: "NeedExternalAddressSlave", Type: "bool"}, - {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, - {Code: "resourceDisk", Field: "ResourceDisk", Type: "string"}, - {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, - {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, - {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "string"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "string"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "string"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "string"}, + {Code: "postgresConf", Field: "PostgresConf", Type: "string"}, + {Code: "postgresConfiguration", Field: "PostgresConfiguration", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "string"}, }) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -425,136 +352,55 @@ func (r *PostgresResource) Update(ctx context.Context, req resource.UpdateReques hasServiceParamChanges := false if !hasServiceParamChanges { - if plan.ResourceInstances.IsNull() != state.ResourceInstances.IsNull() || plan.ResourceInstances.IsUnknown() != state.ResourceInstances.IsUnknown() { + if plan.ClusterConfiguration.IsNull() != state.ClusterConfiguration.IsNull() || plan.ClusterConfiguration.IsUnknown() != state.ClusterConfiguration.IsUnknown() { hasServiceParamChanges = true - } else if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() { - if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() { + } else if !plan.ClusterConfiguration.IsNull() && !plan.ClusterConfiguration.IsUnknown() && !state.ClusterConfiguration.IsNull() && !state.ClusterConfiguration.IsUnknown() { + if !strings.EqualFold(plan.ClusterConfiguration.ValueString(), state.ClusterConfiguration.ValueString()) { hasServiceParamChanges = true } } } if !hasServiceParamChanges { - if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() { + if plan.AccessConfiguration.IsNull() != state.AccessConfiguration.IsNull() || plan.AccessConfiguration.IsUnknown() != state.AccessConfiguration.IsUnknown() { hasServiceParamChanges = true - } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() { - if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() { + } else if !plan.AccessConfiguration.IsNull() && !plan.AccessConfiguration.IsUnknown() && !state.AccessConfiguration.IsNull() && !state.AccessConfiguration.IsUnknown() { + if !strings.EqualFold(plan.AccessConfiguration.ValueString(), state.AccessConfiguration.ValueString()) { hasServiceParamChanges = true } } } if !hasServiceParamChanges { - if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() { + if plan.PostgresConfiguration.IsNull() != state.PostgresConfiguration.IsNull() || plan.PostgresConfiguration.IsUnknown() != state.PostgresConfiguration.IsUnknown() { hasServiceParamChanges = true - } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { - if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + } else if !plan.PostgresConfiguration.IsNull() && !plan.PostgresConfiguration.IsUnknown() && !state.PostgresConfiguration.IsNull() && !state.PostgresConfiguration.IsUnknown() { + if !strings.EqualFold(plan.PostgresConfiguration.ValueString(), state.PostgresConfiguration.ValueString()) { hasServiceParamChanges = true } } } if !hasServiceParamChanges { - if plan.ResourceDisk.IsNull() != state.ResourceDisk.IsNull() || plan.ResourceDisk.IsUnknown() != state.ResourceDisk.IsUnknown() { + if plan.PostgresConf.IsNull() != state.PostgresConf.IsNull() || plan.PostgresConf.IsUnknown() != state.PostgresConf.IsUnknown() { hasServiceParamChanges = true - } else if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() { - if plan.ResourceDisk.ValueString() != state.ResourceDisk.ValueString() { + } 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 { - if plan.NeedExternalAddressMaster.IsNull() != state.NeedExternalAddressMaster.IsNull() || plan.NeedExternalAddressMaster.IsUnknown() != state.NeedExternalAddressMaster.IsUnknown() { + if plan.BackupConfiguration.IsNull() != state.BackupConfiguration.IsNull() || plan.BackupConfiguration.IsUnknown() != state.BackupConfiguration.IsUnknown() { hasServiceParamChanges = true - } else if !plan.NeedExternalAddressMaster.IsNull() && !plan.NeedExternalAddressMaster.IsUnknown() && !state.NeedExternalAddressMaster.IsNull() && !state.NeedExternalAddressMaster.IsUnknown() { - if plan.NeedExternalAddressMaster.ValueBool() != state.NeedExternalAddressMaster.ValueBool() { + } else if !plan.BackupConfiguration.IsNull() && !plan.BackupConfiguration.IsUnknown() && !state.BackupConfiguration.IsNull() && !state.BackupConfiguration.IsUnknown() { + if !strings.EqualFold(plan.BackupConfiguration.ValueString(), state.BackupConfiguration.ValueString()) { hasServiceParamChanges = true } } } if !hasServiceParamChanges { - if plan.NeedExternalAddressSlave.IsNull() != state.NeedExternalAddressSlave.IsNull() || plan.NeedExternalAddressSlave.IsUnknown() != state.NeedExternalAddressSlave.IsUnknown() { + if plan.AutoscaleConfiguration.IsNull() != state.AutoscaleConfiguration.IsNull() || plan.AutoscaleConfiguration.IsUnknown() != state.AutoscaleConfiguration.IsUnknown() { hasServiceParamChanges = true - } else if !plan.NeedExternalAddressSlave.IsNull() && !plan.NeedExternalAddressSlave.IsUnknown() && !state.NeedExternalAddressSlave.IsNull() && !state.NeedExternalAddressSlave.IsUnknown() { - if plan.NeedExternalAddressSlave.ValueBool() != state.NeedExternalAddressSlave.ValueBool() { - hasServiceParamChanges = true - } - } - } - if !hasServiceParamChanges { - if plan.ExtBACKUPSCHEDULE.IsNull() != state.ExtBACKUPSCHEDULE.IsNull() || plan.ExtBACKUPSCHEDULE.IsUnknown() != state.ExtBACKUPSCHEDULE.IsUnknown() { - hasServiceParamChanges = true - } else if !plan.ExtBACKUPSCHEDULE.IsNull() && !plan.ExtBACKUPSCHEDULE.IsUnknown() && !state.ExtBACKUPSCHEDULE.IsNull() && !state.ExtBACKUPSCHEDULE.IsUnknown() { - if plan.ExtBACKUPSCHEDULE.ValueString() != state.ExtBACKUPSCHEDULE.ValueString() { - hasServiceParamChanges = true - } - } - } - if !hasServiceParamChanges { - if plan.ExtBACKUPNUMTORETAIN.IsNull() != state.ExtBACKUPNUMTORETAIN.IsNull() || plan.ExtBACKUPNUMTORETAIN.IsUnknown() != state.ExtBACKUPNUMTORETAIN.IsUnknown() { - hasServiceParamChanges = true - } else if !plan.ExtBACKUPNUMTORETAIN.IsNull() && !plan.ExtBACKUPNUMTORETAIN.IsUnknown() && !state.ExtBACKUPNUMTORETAIN.IsNull() && !state.ExtBACKUPNUMTORETAIN.IsUnknown() { - if plan.ExtBACKUPNUMTORETAIN.ValueInt64() != state.ExtBACKUPNUMTORETAIN.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 plan.AppVersion.ValueString() != state.AppVersion.ValueString() { - hasServiceParamChanges = true - } - } - } - if !hasServiceParamChanges { - if plan.JsonParameters.IsNull() != state.JsonParameters.IsNull() || plan.JsonParameters.IsUnknown() != state.JsonParameters.IsUnknown() { - hasServiceParamChanges = true - } else if !plan.JsonParameters.IsNull() && !plan.JsonParameters.IsUnknown() && !state.JsonParameters.IsNull() && !state.JsonParameters.IsUnknown() { - if plan.JsonParameters.ValueString() != state.JsonParameters.ValueString() { - hasServiceParamChanges = true - } - } - } - if !hasServiceParamChanges { - if plan.EnablePgPoolerMaster.IsNull() != state.EnablePgPoolerMaster.IsNull() || plan.EnablePgPoolerMaster.IsUnknown() != state.EnablePgPoolerMaster.IsUnknown() { - hasServiceParamChanges = true - } else if !plan.EnablePgPoolerMaster.IsNull() && !plan.EnablePgPoolerMaster.IsUnknown() && !state.EnablePgPoolerMaster.IsNull() && !state.EnablePgPoolerMaster.IsUnknown() { - if plan.EnablePgPoolerMaster.ValueBool() != state.EnablePgPoolerMaster.ValueBool() { - hasServiceParamChanges = true - } - } - } - if !hasServiceParamChanges { - if plan.EnablePgPoolerSlave.IsNull() != state.EnablePgPoolerSlave.IsNull() || plan.EnablePgPoolerSlave.IsUnknown() != state.EnablePgPoolerSlave.IsUnknown() { - hasServiceParamChanges = true - } else if !plan.EnablePgPoolerSlave.IsNull() && !plan.EnablePgPoolerSlave.IsUnknown() && !state.EnablePgPoolerSlave.IsNull() && !state.EnablePgPoolerSlave.IsUnknown() { - if plan.EnablePgPoolerSlave.ValueBool() != state.EnablePgPoolerSlave.ValueBool() { - hasServiceParamChanges = true - } - } - } - if !hasServiceParamChanges { - if plan.AllowNoSSL.IsNull() != state.AllowNoSSL.IsNull() || plan.AllowNoSSL.IsUnknown() != state.AllowNoSSL.IsUnknown() { - hasServiceParamChanges = true - } else if !plan.AllowNoSSL.IsNull() && !plan.AllowNoSSL.IsUnknown() && !state.AllowNoSSL.IsNull() && !state.AllowNoSSL.IsUnknown() { - if plan.AllowNoSSL.ValueBool() != state.AllowNoSSL.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 plan.IpSpaceNameMaster.ValueString() != state.IpSpaceNameMaster.ValueString() { - hasServiceParamChanges = true - } - } - } - if !hasServiceParamChanges { - if plan.IpSpaceNameSlave.IsNull() != state.IpSpaceNameSlave.IsNull() || plan.IpSpaceNameSlave.IsUnknown() != state.IpSpaceNameSlave.IsUnknown() { - hasServiceParamChanges = true - } else if !plan.IpSpaceNameSlave.IsNull() && !plan.IpSpaceNameSlave.IsUnknown() && !state.IpSpaceNameSlave.IsNull() && !state.IpSpaceNameSlave.IsUnknown() { - if plan.IpSpaceNameSlave.ValueString() != state.IpSpaceNameSlave.ValueString() { + } else if !plan.AutoscaleConfiguration.IsNull() && !plan.AutoscaleConfiguration.IsUnknown() && !state.AutoscaleConfiguration.IsNull() && !state.AutoscaleConfiguration.IsUnknown() { + if !strings.EqualFold(plan.AutoscaleConfiguration.ValueString(), state.AutoscaleConfiguration.ValueString()) { hasServiceParamChanges = true } } @@ -575,27 +421,21 @@ func (r *PostgresResource) Update(ctx context.Context, req resource.UpdateReques } params := map[int]string{ - 91: resources_core.FormatInt64(plan.ResourceInstances), - 92: resources_core.FormatInt64(plan.ResourceMemory), - 93: resources_core.FormatInt64(plan.ResourceCPU), - 94: resources_core.FormatString(plan.ResourceDisk), - 147: resources_core.FormatBool(plan.NeedExternalAddressMaster), - 148: resources_core.FormatBool(plan.NeedExternalAddressSlave), - 267: resources_core.FormatString(plan.ExtBACKUPSCHEDULE), - 268: resources_core.FormatInt64(plan.ExtBACKUPNUMTORETAIN), - 315: resources_core.FormatString(plan.AppVersion), - 316: resources_core.FormatString(plan.JsonParameters), - 317: resources_core.FormatBool(plan.EnablePgPoolerMaster), - 318: resources_core.FormatBool(plan.EnablePgPoolerSlave), - 319: resources_core.FormatBool(plan.AllowNoSSL), - 541: resources_core.FormatString(plan.IpSpaceNameMaster), - 542: resources_core.FormatString(plan.IpSpaceNameSlave), + 795: resources_core.FormatString(plan.ClusterConfiguration), + 796: resources_core.FormatString(plan.AccessConfiguration), + 797: resources_core.FormatString(plan.PostgresConfiguration), + 798: resources_core.FormatString(plan.PostgresConf), + 799: resources_core.FormatString(plan.BackupConfiguration), + 800: resources_core.FormatString(plan.AutoscaleConfiguration), } operationTimeout := "" if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -612,26 +452,13 @@ func (r *PostgresResource) Update(ctx context.Context, req resource.UpdateReques {Code: "vault_user_path"}, {Code: "vault_fields"}, }, []resources_core.InputField{ - {Code: "allowNoSSL", Field: "AllowNoSSL", Type: "bool"}, - {Code: "appVersion", Field: "AppVersion", Type: "string"}, - {Code: "autoScale", Field: "AutoScale", Type: "bool"}, - {Code: "autoScalePercentage", Field: "AutoScalePercentage", Type: "int64"}, - {Code: "autoScaleQuotaGb", Field: "AutoScaleQuotaGb", Type: "string"}, - {Code: "autoScaleTechWindow", Field: "AutoScaleTechWindow", Type: "int64"}, - {Code: "enablePgPoolerMaster", Field: "EnablePgPoolerMaster", Type: "bool"}, - {Code: "enablePgPoolerSlave", Field: "EnablePgPoolerSlave", Type: "bool"}, - {Code: "ext_BACKUP_NUM_TO_RETAIN", Field: "ExtBACKUPNUMTORETAIN", Type: "int64"}, - {Code: "ext_BACKUP_SCHEDULE", Field: "ExtBACKUPSCHEDULE", Type: "string"}, - {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"}, - {Code: "ipSpaceNameSlave", Field: "IpSpaceNameSlave", Type: "string"}, - {Code: "jsonParameters", Field: "JsonParameters", Type: "string"}, - {Code: "needExternalAddressMaster", Field: "NeedExternalAddressMaster", Type: "bool"}, - {Code: "needExternalAddressSlave", Field: "NeedExternalAddressSlave", Type: "bool"}, - {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, - {Code: "resourceDisk", Field: "ResourceDisk", Type: "string"}, - {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, - {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, - {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "string"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "string"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "string"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "string"}, + {Code: "postgresConf", Field: "PostgresConf", Type: "string"}, + {Code: "postgresConfiguration", Field: "PostgresConfiguration", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "string"}, }) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -650,7 +477,6 @@ func (r *PostgresResource) Delete(ctx context.Context, req resource.DeleteReques if state.ID.IsNull() || state.ID.IsUnknown() { return } - deleteMode := "state_only" if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { deleteMode = "suspend" @@ -660,6 +486,9 @@ func (r *PostgresResource) Delete(ctx context.Context, req resource.DeleteReques 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 diff --git a/universal_rebuild/internal/resources_gen/90_postgres_restart_action.go b/universal_rebuild/internal/resources_gen/90_postgres_restart_action.go index 1f6824c..38ce584 100644 --- a/universal_rebuild/internal/resources_gen/90_postgres_restart_action.go +++ b/universal_rebuild/internal/resources_gen/90_postgres_restart_action.go @@ -9,6 +9,7 @@ import ( "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/types" ) @@ -24,10 +25,12 @@ type PostgresRestartResource struct { } type PostgresRestartModel struct { - ID types.String `tfsdk:"id"` - PostgresID types.String `tfsdk:"postgres_id"` - RunID types.String `tfsdk:"run_id"` - OperationTimeout types.String `tfsdk:"operation_timeout"` + ID types.String `tfsdk:"id"` + PostgresID types.String `tfsdk:"postgres_id"` + RunID types.String `tfsdk:"run_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` } func NewPostgresRestartResource() resource.Resource { @@ -40,10 +43,12 @@ func (r *PostgresRestartResource) Metadata(ctx context.Context, req resource.Met func (r *PostgresRestartResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, - "postgres_id": schema.StringAttribute{Required: true}, - "run_id": schema.StringAttribute{Required: true}, - "operation_timeout": schema.StringAttribute{Optional: true}, + "id": schema.StringAttribute{Computed: true}, + "postgres_id": schema.StringAttribute{Required: true}, + "run_id": schema.StringAttribute{Required: true}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "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} } @@ -68,6 +73,9 @@ func (r *PostgresRestartResource) Create(ctx context.Context, req resource.Creat if !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, "restart", params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return diff --git a/universal_rebuild/internal/resources_gen/90_postgres_user_resource.go b/universal_rebuild/internal/resources_gen/90_postgres_user_resource.go index e307576..2122df2 100644 --- a/universal_rebuild/internal/resources_gen/90_postgres_user_resource.go +++ b/universal_rebuild/internal/resources_gen/90_postgres_user_resource.go @@ -32,6 +32,7 @@ type PostgresUserModel struct { 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"` } @@ -46,7 +47,7 @@ func (r *PostgresUserResource) Metadata(ctx context.Context, req resource.Metada func (r *PostgresUserResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, "postgres_id": schema.StringAttribute{ Required: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, @@ -54,6 +55,7 @@ func (r *PostgresUserResource) Schema(ctx context.Context, req resource.SchemaRe "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(false)}, "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()}}, } @@ -131,8 +133,28 @@ func (r *PostgresUserResource) Create(ctx context.Context, req resource.CreateRe if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { operationTimeout = plan.OperationTimeout.ValueString() } - if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout); err != nil { - if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(err) { + 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 { @@ -149,7 +171,7 @@ func (r *PostgresUserResource) Create(ctx context.Context, req resource.CreateRe resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") return } - resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) return } if targetValue != "" { @@ -214,6 +236,9 @@ func (r *PostgresUserResource) Update(ctx context.Context, req resource.UpdateRe if !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 @@ -286,6 +311,9 @@ func (r *PostgresUserResource) Delete(ctx context.Context, req resource.DeleteRe 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 != "" { diff --git a/universal_rebuild/internal/resources_gen/91_redis_reconcile_action.go b/universal_rebuild/internal/resources_gen/91_redis_reconcile_action.go new file mode 100644 index 0000000..1a113ef --- /dev/null +++ b/universal_rebuild/internal/resources_gen/91_redis_reconcile_action.go @@ -0,0 +1,136 @@ +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/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: redis +// Action: reconcile + +var _ resource.Resource = &RedisReconcileResource{} + +// RedisReconcileResource triggers a one-time action. +type RedisReconcileResource struct { + client *core.UniversalClient +} + +type RedisReconcileModel struct { + ID types.String `tfsdk:"id"` + RedisID types.String `tfsdk:"redis_id"` + RunID types.String `tfsdk:"run_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` +} + +func NewRedisReconcileResource() resource.Resource { + return &RedisReconcileResource{} +} + +func (r *RedisReconcileResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_redis_reconcile" +} + +func (r *RedisReconcileResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true}, + "redis_id": schema.StringAttribute{Required: true}, + "run_id": schema.StringAttribute{Required: true}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "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 *RedisReconcileResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan RedisReconcileModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.RedisID.ValueString()) + runID := strings.TrimSpace(plan.RunID.ValueString()) + if instanceUID == "" || runID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра или run_id") + return + } + + params := 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, "reconcile", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "reconcile", runID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *RedisReconcileResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state RedisReconcileModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *RedisReconcileResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan RedisReconcileModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.RedisID.ValueString()) + runID := strings.TrimSpace(plan.RunID.ValueString()) + if instanceUID == "" || runID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра или run_id") + return + } + + params := map[string]string{} + + if err := resources_core.RunOperationByCode(ctx, r.client, instanceUID, "reconcile", params); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "reconcile", runID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *RedisReconcileResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // No-op: removing the resource does not trigger a remote action. +} + +func (r *RedisReconcileResource) 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/universal_rebuild/internal/resources_gen/91_redis_resource.go b/universal_rebuild/internal/resources_gen/91_redis_resource.go index b52f62e..5998295 100644 --- a/universal_rebuild/internal/resources_gen/91_redis_resource.go +++ b/universal_rebuild/internal/resources_gen/91_redis_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -10,7 +11,8 @@ import ( "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" ) @@ -27,28 +29,23 @@ type RedisResource struct { } type RedisModel struct { - ID types.String `tfsdk:"id"` - ResourceName types.String `tfsdk:"resource_name"` - OperationTimeout types.String `tfsdk:"operation_timeout"` - IpSpaceNameMaster types.String `tfsdk:"ip_space_name_master"` - IpSpaceNameSlave types.String `tfsdk:"ip_space_name_slave"` - NeedExternalAddressMaster types.Bool `tfsdk:"need_external_address_master"` - NeedExternalAddressSlave types.Bool `tfsdk:"need_external_address_slave"` - 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"` + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration types.String `tfsdk:"access_configuration"` + ClusterConfiguration types.String `tfsdk:"cluster_configuration"` + StartupConfiguration types.String `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 { @@ -61,28 +58,23 @@ func (r *RedisResource) Metadata(ctx context.Context, req resource.MetadataReque func (r *RedisResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, - "resource_name": schema.StringAttribute{Required: true}, - "operation_timeout": schema.StringAttribute{Optional: true}, - "ip_space_name_master": schema.StringAttribute{Optional: true, MarkdownDescription: "Имя ipSpace для публикации VIP master. Не может быть пустым, если включен needExternalAddressMaster Не изменяется после создания."}, - "ip_space_name_slave": schema.StringAttribute{Optional: true, MarkdownDescription: "Имя ipSpace для публикации VIP slave. Не может быть пустым, если включен needExternalAddressSlave Не изменяется после создания."}, - "need_external_address_master": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Выделяет внешний IP для доступа к master Не изменяется после создания."}, - "need_external_address_slave": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Выделяет внешний IP для доступа к slave. Будет работать если slave в целом есть Не изменяется после создания."}, - "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1000), MarkdownDescription: "Квота ядра пода (Указывается в mili) Не изменяется после создания."}, - "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(30), 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(1024), 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}, + "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.StringAttribute{Required: true}, + "cluster_configuration": schema.StringAttribute{Required: true}, + "startup_configuration": 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} @@ -123,64 +115,36 @@ func (r *RedisResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanR return } } - if !plan.IpSpaceNameMaster.IsNull() && !plan.IpSpaceNameMaster.IsUnknown() && !state.IpSpaceNameMaster.IsNull() && !state.IpSpaceNameMaster.IsUnknown() { - if plan.IpSpaceNameMaster.ValueString() != state.IpSpaceNameMaster.ValueString() { - resp.Diagnostics.AddError("Нельзя изменить ip_space_name_master", "Параметр задается при создании и не может быть изменен.") - return - } - } - if !plan.IpSpaceNameSlave.IsNull() && !plan.IpSpaceNameSlave.IsUnknown() && !state.IpSpaceNameSlave.IsNull() && !state.IpSpaceNameSlave.IsUnknown() { - if plan.IpSpaceNameSlave.ValueString() != state.IpSpaceNameSlave.ValueString() { - resp.Diagnostics.AddError("Нельзя изменить ip_space_name_slave", "Параметр задается при создании и не может быть изменен.") - return - } - } - if !plan.NeedExternalAddressMaster.IsNull() && !plan.NeedExternalAddressMaster.IsUnknown() && !state.NeedExternalAddressMaster.IsNull() && !state.NeedExternalAddressMaster.IsUnknown() { - if plan.NeedExternalAddressMaster.ValueBool() != state.NeedExternalAddressMaster.ValueBool() { - resp.Diagnostics.AddError("Нельзя изменить need_external_address_master", "Параметр задается при создании и не может быть изменен.") - return - } - } - if !plan.NeedExternalAddressSlave.IsNull() && !plan.NeedExternalAddressSlave.IsUnknown() && !state.NeedExternalAddressSlave.IsNull() && !state.NeedExternalAddressSlave.IsUnknown() { - if plan.NeedExternalAddressSlave.ValueBool() != state.NeedExternalAddressSlave.ValueBool() { - resp.Diagnostics.AddError("Нельзя изменить need_external_address_slave", "Параметр задается при создании и не может быть изменен.") - 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() { - if plan.ResourceRealm.ValueString() != state.ResourceRealm.ValueString() { - resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + if !plan.StartupConfiguration.IsNull() && !plan.StartupConfiguration.IsUnknown() && !state.StartupConfiguration.IsNull() && !state.StartupConfiguration.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.StartupConfiguration.ValueString(), state.StartupConfiguration.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить startup_configuration", "Параметр задается при создании и не может быть изменен.") return } } return } - if config.ResourceRealm.IsNull() || config.ResourceRealm.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + if config.AccessConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "access_configuration is required.") + return + } + if config.AccessConfiguration.IsUnknown() { + return + } + if config.ClusterConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "cluster_configuration is required.") + return + } + if config.ClusterConfiguration.IsUnknown() { + return + } + if config.StartupConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "startup_configuration is required.") + return + } + if config.StartupConfiguration.IsUnknown() { return } @@ -192,18 +156,14 @@ func (r *RedisResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanR adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() } params := map[int]string{ - 112: resources_core.FormatInt64(config.ResourceCPU), - 113: resources_core.FormatInt64(config.ResourceMemory), - 114: resources_core.FormatInt64(config.ResourceDisk), - 181: resources_core.FormatInt64(config.ResourceInstances), - 182: resources_core.FormatString(config.ResourceRealm), - 183: resources_core.FormatBool(config.NeedExternalAddressMaster), - 184: resources_core.FormatBool(config.NeedExternalAddressSlave), - 548: resources_core.FormatString(config.IpSpaceNameMaster), - 549: resources_core.FormatString(config.IpSpaceNameSlave), + 1061: resources_core.FormatString(config.StartupConfiguration), + 1062: resources_core.FormatString(config.ClusterConfiguration), + 1063: resources_core.FormatString(config.AccessConfiguration), } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} - resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 91, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -212,30 +172,37 @@ func (r *RedisResource) Create(ctx context.Context, req resource.CreateRequest, if resp.Diagnostics.HasError() { return } - if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + if data.AccessConfiguration.IsNull() || data.AccessConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "access_configuration is required.") + return + } + if data.ClusterConfiguration.IsNull() || data.ClusterConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "cluster_configuration is required.") + return + } + if data.StartupConfiguration.IsNull() || data.StartupConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "startup_configuration is required.") return } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 91, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ - 112: resources_core.FormatInt64(data.ResourceCPU), - 113: resources_core.FormatInt64(data.ResourceMemory), - 114: resources_core.FormatInt64(data.ResourceDisk), - 181: resources_core.FormatInt64(data.ResourceInstances), - 182: resources_core.FormatString(data.ResourceRealm), - 183: resources_core.FormatBool(data.NeedExternalAddressMaster), - 184: resources_core.FormatBool(data.NeedExternalAddressSlave), - 548: resources_core.FormatString(data.IpSpaceNameMaster), - 549: resources_core.FormatString(data.IpSpaceNameSlave), + 1061: resources_core.FormatString(data.StartupConfiguration), + 1062: resources_core.FormatString(data.ClusterConfiguration), + 1063: resources_core.FormatString(data.AccessConfiguration), } 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()) @@ -253,15 +220,9 @@ func (r *RedisResource) Create(ctx context.Context, req resource.CreateRequest, {Code: "vault_user_path"}, {Code: "vault_fields"}, }, []resources_core.InputField{ - {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"}, - {Code: "ipSpaceNameSlave", Field: "IpSpaceNameSlave", Type: "string"}, - {Code: "needExternalAddressMaster", Field: "NeedExternalAddressMaster", Type: "bool"}, - {Code: "needExternalAddressSlave", Field: "NeedExternalAddressSlave", 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"}, + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "string"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "string"}, }) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -302,15 +263,9 @@ func (r *RedisResource) Read(ctx context.Context, req resource.ReadRequest, resp {Code: "vault_user_path"}, {Code: "vault_fields"}, }, []resources_core.InputField{ - {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"}, - {Code: "ipSpaceNameSlave", Field: "IpSpaceNameSlave", Type: "string"}, - {Code: "needExternalAddressMaster", Field: "NeedExternalAddressMaster", Type: "bool"}, - {Code: "needExternalAddressSlave", Field: "NeedExternalAddressSlave", 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"}, + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "string"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "string"}, }) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -339,6 +294,24 @@ func (r *RedisResource) Update(ctx context.Context, req resource.UpdateRequest, } hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.ClusterConfiguration.IsNull() != state.ClusterConfiguration.IsNull() || plan.ClusterConfiguration.IsUnknown() != state.ClusterConfiguration.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ClusterConfiguration.IsNull() && !plan.ClusterConfiguration.IsUnknown() && !state.ClusterConfiguration.IsNull() && !state.ClusterConfiguration.IsUnknown() { + if !strings.EqualFold(plan.ClusterConfiguration.ValueString(), state.ClusterConfiguration.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.AccessConfiguration.IsNull() != state.AccessConfiguration.IsNull() || plan.AccessConfiguration.IsUnknown() != state.AccessConfiguration.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.AccessConfiguration.IsNull() && !plan.AccessConfiguration.IsUnknown() && !state.AccessConfiguration.IsNull() && !state.AccessConfiguration.IsUnknown() { + if !strings.EqualFold(plan.AccessConfiguration.ValueString(), state.AccessConfiguration.ValueString()) { + hasServiceParamChanges = true + } + } + } if !hasServiceParamChanges { plan.ID = instanceID @@ -354,12 +327,18 @@ func (r *RedisResource) Update(ctx context.Context, req resource.UpdateRequest, return } - params := map[int]string{} + params := map[int]string{ + 1064: resources_core.FormatString(plan.ClusterConfiguration), + 1065: resources_core.FormatString(plan.AccessConfiguration), + } operationTimeout := "" if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -376,15 +355,9 @@ func (r *RedisResource) Update(ctx context.Context, req resource.UpdateRequest, {Code: "vault_user_path"}, {Code: "vault_fields"}, }, []resources_core.InputField{ - {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"}, - {Code: "ipSpaceNameSlave", Field: "IpSpaceNameSlave", Type: "string"}, - {Code: "needExternalAddressMaster", Field: "NeedExternalAddressMaster", Type: "bool"}, - {Code: "needExternalAddressSlave", Field: "NeedExternalAddressSlave", 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"}, + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "string"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "string"}, }) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -403,7 +376,6 @@ func (r *RedisResource) Delete(ctx context.Context, req resource.DeleteRequest, if state.ID.IsNull() || state.ID.IsUnknown() { return } - deleteMode := "state_only" if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { deleteMode = "suspend" @@ -413,6 +385,9 @@ func (r *RedisResource) Delete(ctx context.Context, req resource.DeleteRequest, 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 diff --git a/universal_rebuild/internal/resources_gen/92_mongodb_resource.go b/universal_rebuild/internal/resources_gen/92_mongodb_resource.go index 4b7b992..8c3be63 100644 --- a/universal_rebuild/internal/resources_gen/92_mongodb_resource.go +++ b/universal_rebuild/internal/resources_gen/92_mongodb_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -10,7 +11,9 @@ import ( "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" ) @@ -27,26 +30,27 @@ type MongodbResource struct { } type MongodbModel struct { - ID types.String `tfsdk:"id"` - ResourceName types.String `tfsdk:"resource_name"` - OperationTimeout types.String `tfsdk:"operation_timeout"` - IpSpaceNameMaster types.String `tfsdk:"ip_space_name_master"` - NeedExternalAddressMaster types.String `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"` + 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 { @@ -59,26 +63,27 @@ func (r *MongodbResource) Metadata(ctx context.Context, req resource.MetadataReq func (r *MongodbResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, - "resource_name": schema.StringAttribute{Required: true}, - "operation_timeout": schema.StringAttribute{Optional: true}, - "ip_space_name_master": schema.StringAttribute{Optional: true, MarkdownDescription: "Если включен предыдущий параметр, то поле необходимо заполнить. Пример: `internet-no-antiddos-v1` Не изменяется после создания."}, - "need_external_address_master": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("false"), MarkdownDescription: "Флаг, определяющий, нужно ли выделять внешний IP для доступа к master-брокеру. Пример: `true` или `false` Не изменяется после создания."}, - "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{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}, + "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{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} @@ -119,70 +124,52 @@ func (r *MongodbResource) ModifyPlan(ctx context.Context, req resource.ModifyPla return } } - if !plan.IpSpaceNameMaster.IsNull() && !plan.IpSpaceNameMaster.IsUnknown() && !state.IpSpaceNameMaster.IsNull() && !state.IpSpaceNameMaster.IsUnknown() { - if plan.IpSpaceNameMaster.ValueString() != state.IpSpaceNameMaster.ValueString() { - resp.Diagnostics.AddError("Нельзя изменить ip_space_name_master", "Параметр задается при создании и не может быть изменен.") - return - } - } - if !plan.NeedExternalAddressMaster.IsNull() && !plan.NeedExternalAddressMaster.IsUnknown() && !state.NeedExternalAddressMaster.IsNull() && !state.NeedExternalAddressMaster.IsUnknown() { - if plan.NeedExternalAddressMaster.ValueString() != state.NeedExternalAddressMaster.ValueString() { - resp.Diagnostics.AddError("Нельзя изменить need_external_address_master", "Параметр задается при создании и не может быть изменен.") - 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() { - if plan.ResourceRealm.ValueString() != state.ResourceRealm.ValueString() { + // 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() || config.ResourceCPU.IsUnknown() { + if config.ResourceCPU.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "resource_c_p_u is required.") return } - if config.ResourceDisk.IsNull() || config.ResourceDisk.IsUnknown() { + if config.ResourceCPU.IsUnknown() { + return + } + if config.ResourceDisk.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "resource_disk is required.") return } - if config.ResourceInstances.IsNull() || config.ResourceInstances.IsUnknown() { + if config.ResourceDisk.IsUnknown() { + return + } + if config.ResourceInstances.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "resource_instances is required.") return } - if config.ResourceMemory.IsNull() || config.ResourceMemory.IsUnknown() { + if config.ResourceInstances.IsUnknown() { + return + } + if config.ResourceMemory.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "resource_memory is required.") return } - if config.ResourceRealm.IsNull() || config.ResourceRealm.IsUnknown() { + if config.ResourceMemory.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 @@ -197,11 +184,13 @@ func (r *MongodbResource) ModifyPlan(ctx context.Context, req resource.ModifyPla 589: resources_core.FormatInt64(config.ResourceCPU), 590: resources_core.FormatInt64(config.ResourceDisk), 595: resources_core.FormatString(config.ResourceRealm), - 645: resources_core.FormatString(config.NeedExternalAddressMaster), 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.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 92, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -232,7 +221,9 @@ func (r *MongodbResource) Create(ctx context.Context, req resource.CreateRequest } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 92, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ 487: resources_core.FormatInt64(data.ResourceInstances), @@ -240,14 +231,17 @@ func (r *MongodbResource) Create(ctx context.Context, req resource.CreateRequest 589: resources_core.FormatInt64(data.ResourceCPU), 590: resources_core.FormatInt64(data.ResourceDisk), 595: resources_core.FormatString(data.ResourceRealm), - 645: resources_core.FormatString(data.NeedExternalAddressMaster), 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()) @@ -266,7 +260,7 @@ func (r *MongodbResource) Create(ctx context.Context, req resource.CreateRequest {Code: "vault_fields"}, }, []resources_core.InputField{ {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"}, - {Code: "needExternalAddressMaster", Field: "NeedExternalAddressMaster", 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"}, @@ -313,7 +307,7 @@ func (r *MongodbResource) Read(ctx context.Context, req resource.ReadRequest, re {Code: "vault_fields"}, }, []resources_core.InputField{ {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"}, - {Code: "needExternalAddressMaster", Field: "NeedExternalAddressMaster", 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"}, @@ -347,6 +341,60 @@ func (r *MongodbResource) Update(ctx context.Context, req resource.UpdateRequest } 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 @@ -362,12 +410,22 @@ func (r *MongodbResource) Update(ctx context.Context, req resource.UpdateRequest return } - params := map[int]string{} + 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), + } operationTimeout := "" if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -385,7 +443,7 @@ func (r *MongodbResource) Update(ctx context.Context, req resource.UpdateRequest {Code: "vault_fields"}, }, []resources_core.InputField{ {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"}, - {Code: "needExternalAddressMaster", Field: "NeedExternalAddressMaster", 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"}, @@ -409,7 +467,6 @@ func (r *MongodbResource) Delete(ctx context.Context, req resource.DeleteRequest if state.ID.IsNull() || state.ID.IsUnknown() { return } - deleteMode := "state_only" if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { deleteMode = "suspend" @@ -419,6 +476,9 @@ func (r *MongodbResource) Delete(ctx context.Context, req resource.DeleteRequest 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 diff --git a/universal_rebuild/internal/resources_gen/92_mongodb_user_resource.go b/universal_rebuild/internal/resources_gen/92_mongodb_user_resource.go index 67a7b2c..af2d6d9 100644 --- a/universal_rebuild/internal/resources_gen/92_mongodb_user_resource.go +++ b/universal_rebuild/internal/resources_gen/92_mongodb_user_resource.go @@ -32,6 +32,7 @@ type MongodbUserModel struct { 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"` @@ -47,7 +48,7 @@ func (r *MongodbUserResource) Metadata(ctx context.Context, req resource.Metadat func (r *MongodbUserResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, "mongodb_id": schema.StringAttribute{ Required: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, @@ -55,6 +56,7 @@ func (r *MongodbUserResource) Schema(ctx context.Context, req resource.SchemaReq "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(false)}, "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()}}, @@ -136,8 +138,28 @@ func (r *MongodbUserResource) Create(ctx context.Context, req resource.CreateReq if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { operationTimeout = plan.OperationTimeout.ValueString() } - if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout); err != nil { - if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(err) { + 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 { @@ -154,7 +176,7 @@ func (r *MongodbUserResource) Create(ctx context.Context, req resource.CreateReq resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") return } - resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) return } if targetValue != "" { @@ -222,6 +244,9 @@ func (r *MongodbUserResource) Update(ctx context.Context, req resource.UpdateReq if !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 @@ -292,6 +317,9 @@ func (r *MongodbUserResource) Delete(ctx context.Context, req resource.DeleteReq 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 != "" { diff --git a/universal_rebuild/internal/resources_gen/93_rabbitmq_resource.go b/universal_rebuild/internal/resources_gen/93_rabbitmq_resource.go index 0e99b59..f8286e2 100644 --- a/universal_rebuild/internal/resources_gen/93_rabbitmq_resource.go +++ b/universal_rebuild/internal/resources_gen/93_rabbitmq_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -11,6 +12,8 @@ import ( "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" ) @@ -30,10 +33,9 @@ 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"` - IpSpaceNameSlave types.String `tfsdk:"ip_space_name_slave"` NeedExternalAddressMaster types.Bool `tfsdk:"need_external_address_master"` - NeedExternalAddressSlave types.Bool `tfsdk:"need_external_address_slave"` ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` ResourceDisk types.Int64 `tfsdk:"resource_disk"` ResourceInstances types.Int64 `tfsdk:"resource_instances"` @@ -61,13 +63,12 @@ func (r *RabbitmqResource) Metadata(ctx context.Context, req resource.MetadataRe func (r *RabbitmqResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "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`"}, - "ip_space_name_slave": 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`"}, - "need_external_address_slave": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Выделяет внешний (белый) IP для доступа к slave"}, "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`"}, @@ -124,17 +125,23 @@ func (r *RabbitmqResource) ModifyPlan(ctx context.Context, req resource.ModifyPl } } if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { - if plan.ResourceRealm.ValueString() != state.ResourceRealm.ValueString() { + // 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() || config.ResourceRealm.IsUnknown() { + 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 @@ -152,8 +159,10 @@ func (r *RabbitmqResource) ModifyPlan(ctx context.Context, req resource.ModifyPl 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.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 93, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -168,7 +177,9 @@ func (r *RabbitmqResource) Create(ctx context.Context, req resource.CreateReques } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 93, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ 116: resources_core.FormatInt64(data.ResourceCPU), @@ -184,6 +195,9 @@ func (r *RabbitmqResource) Create(ctx context.Context, req resource.CreateReques 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()) @@ -202,9 +216,7 @@ func (r *RabbitmqResource) Create(ctx context.Context, req resource.CreateReques {Code: "vault_fields"}, }, []resources_core.InputField{ {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"}, - {Code: "ipSpaceNameSlave", Field: "IpSpaceNameSlave", Type: "string"}, {Code: "needExternalAddressMaster", Field: "NeedExternalAddressMaster", Type: "bool"}, - {Code: "needExternalAddressSlave", Field: "NeedExternalAddressSlave", Type: "bool"}, {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, @@ -251,9 +263,7 @@ func (r *RabbitmqResource) Read(ctx context.Context, req resource.ReadRequest, r {Code: "vault_fields"}, }, []resources_core.InputField{ {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"}, - {Code: "ipSpaceNameSlave", Field: "IpSpaceNameSlave", Type: "string"}, {Code: "needExternalAddressMaster", Field: "NeedExternalAddressMaster", Type: "bool"}, - {Code: "needExternalAddressSlave", Field: "NeedExternalAddressSlave", Type: "bool"}, {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, @@ -332,29 +342,11 @@ func (r *RabbitmqResource) Update(ctx context.Context, req resource.UpdateReques } } } - if !hasServiceParamChanges { - if plan.NeedExternalAddressSlave.IsNull() != state.NeedExternalAddressSlave.IsNull() || plan.NeedExternalAddressSlave.IsUnknown() != state.NeedExternalAddressSlave.IsUnknown() { - hasServiceParamChanges = true - } else if !plan.NeedExternalAddressSlave.IsNull() && !plan.NeedExternalAddressSlave.IsUnknown() && !state.NeedExternalAddressSlave.IsNull() && !state.NeedExternalAddressSlave.IsUnknown() { - if plan.NeedExternalAddressSlave.ValueBool() != state.NeedExternalAddressSlave.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 plan.IpSpaceNameMaster.ValueString() != state.IpSpaceNameMaster.ValueString() { - hasServiceParamChanges = true - } - } - } - if !hasServiceParamChanges { - if plan.IpSpaceNameSlave.IsNull() != state.IpSpaceNameSlave.IsNull() || plan.IpSpaceNameSlave.IsUnknown() != state.IpSpaceNameSlave.IsUnknown() { - hasServiceParamChanges = true - } else if !plan.IpSpaceNameSlave.IsNull() && !plan.IpSpaceNameSlave.IsUnknown() && !state.IpSpaceNameSlave.IsNull() && !state.IpSpaceNameSlave.IsUnknown() { - if plan.IpSpaceNameSlave.ValueString() != state.IpSpaceNameSlave.ValueString() { + if !strings.EqualFold(plan.IpSpaceNameMaster.ValueString(), state.IpSpaceNameMaster.ValueString()) { hasServiceParamChanges = true } } @@ -380,15 +372,16 @@ func (r *RabbitmqResource) Update(ctx context.Context, req resource.UpdateReques 306: resources_core.FormatInt64(plan.ResourceDisk), 307: resources_core.FormatInt64(plan.ResourceInstances), 308: resources_core.FormatBool(plan.NeedExternalAddressMaster), - 309: resources_core.FormatBool(plan.NeedExternalAddressSlave), 545: resources_core.FormatString(plan.IpSpaceNameMaster), - 546: resources_core.FormatString(plan.IpSpaceNameSlave), } operationTimeout := "" if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -406,9 +399,7 @@ func (r *RabbitmqResource) Update(ctx context.Context, req resource.UpdateReques {Code: "vault_fields"}, }, []resources_core.InputField{ {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"}, - {Code: "ipSpaceNameSlave", Field: "IpSpaceNameSlave", Type: "string"}, {Code: "needExternalAddressMaster", Field: "NeedExternalAddressMaster", Type: "bool"}, - {Code: "needExternalAddressSlave", Field: "NeedExternalAddressSlave", Type: "bool"}, {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, @@ -432,7 +423,6 @@ func (r *RabbitmqResource) Delete(ctx context.Context, req resource.DeleteReques if state.ID.IsNull() || state.ID.IsUnknown() { return } - deleteMode := "state_only" if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { deleteMode = "suspend" @@ -442,6 +432,9 @@ func (r *RabbitmqResource) Delete(ctx context.Context, req resource.DeleteReques 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 diff --git a/universal_rebuild/internal/resources_gen/94_lucee_reconcile_action.go b/universal_rebuild/internal/resources_gen/94_lucee_reconcile_action.go new file mode 100644 index 0000000..2b531f3 --- /dev/null +++ b/universal_rebuild/internal/resources_gen/94_lucee_reconcile_action.go @@ -0,0 +1,136 @@ +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/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: lucee +// Action: reconcile + +var _ resource.Resource = &LuceeReconcileResource{} + +// LuceeReconcileResource triggers a one-time action. +type LuceeReconcileResource struct { + client *core.UniversalClient +} + +type LuceeReconcileModel struct { + ID types.String `tfsdk:"id"` + LuceeID types.String `tfsdk:"lucee_id"` + RunID types.String `tfsdk:"run_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` +} + +func NewLuceeReconcileResource() resource.Resource { + return &LuceeReconcileResource{} +} + +func (r *LuceeReconcileResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_lucee_reconcile" +} + +func (r *LuceeReconcileResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true}, + "lucee_id": schema.StringAttribute{Required: true}, + "run_id": schema.StringAttribute{Required: true}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "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 *LuceeReconcileResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan LuceeReconcileModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.LuceeID.ValueString()) + runID := strings.TrimSpace(plan.RunID.ValueString()) + if instanceUID == "" || runID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра или run_id") + return + } + + params := 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, "reconcile", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "reconcile", runID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *LuceeReconcileResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state LuceeReconcileModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *LuceeReconcileResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan LuceeReconcileModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.LuceeID.ValueString()) + runID := strings.TrimSpace(plan.RunID.ValueString()) + if instanceUID == "" || runID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра или run_id") + return + } + + params := map[string]string{} + + if err := resources_core.RunOperationByCode(ctx, r.client, instanceUID, "reconcile", params); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "reconcile", runID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *LuceeReconcileResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // No-op: removing the resource does not trigger a remote action. +} + +func (r *LuceeReconcileResource) 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/universal_rebuild/internal/resources_gen/94_lucee_redeploy_action.go b/universal_rebuild/internal/resources_gen/94_lucee_redeploy_action.go index 635a4da..4cfe52f 100644 --- a/universal_rebuild/internal/resources_gen/94_lucee_redeploy_action.go +++ b/universal_rebuild/internal/resources_gen/94_lucee_redeploy_action.go @@ -9,6 +9,7 @@ import ( "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/types" ) @@ -24,11 +25,13 @@ type LuceeRedeployResource struct { } type LuceeRedeployModel struct { - ID types.String `tfsdk:"id"` - LuceeID types.String `tfsdk:"lucee_id"` - RunID types.String `tfsdk:"run_id"` - OperationTimeout types.String `tfsdk:"operation_timeout"` - ResourceRealm types.String `tfsdk:"resource_realm"` + ID types.String `tfsdk:"id"` + LuceeID types.String `tfsdk:"lucee_id"` + RunID types.String `tfsdk:"run_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + StartupConfiguration types.String `tfsdk:"startup_configuration"` } func NewLuceeRedeployResource() resource.Resource { @@ -41,11 +44,13 @@ func (r *LuceeRedeployResource) Metadata(ctx context.Context, req resource.Metad func (r *LuceeRedeployResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, - "lucee_id": schema.StringAttribute{Required: true}, - "run_id": schema.StringAttribute{Required: true}, - "operation_timeout": schema.StringAttribute{Optional: true}, - "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания"}, + "id": schema.StringAttribute{Computed: true}, + "lucee_id": schema.StringAttribute{Required: true}, + "run_id": schema.StringAttribute{Required: true}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "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}, } resp.Schema = schema.Schema{Attributes: attrs} } @@ -56,8 +61,8 @@ func (r *LuceeRedeployResource) Create(ctx context.Context, req resource.CreateR if resp.Diagnostics.HasError() { return } - if plan.ResourceRealm.IsNull() || plan.ResourceRealm.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + if plan.StartupConfiguration.IsNull() || plan.StartupConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "startup_configuration is required.") return } @@ -69,13 +74,16 @@ func (r *LuceeRedeployResource) Create(ctx context.Context, req resource.CreateR } params := map[string]string{ - "resourceRealm": resources_core.FormatString(plan.ResourceRealm), + "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, "redeploy", params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -109,7 +117,7 @@ func (r *LuceeRedeployResource) Update(ctx context.Context, req resource.UpdateR } params := map[string]string{ - "resourceRealm": resources_core.FormatString(plan.ResourceRealm), + "startupConfiguration": resources_core.FormatString(plan.StartupConfiguration), } if err := resources_core.RunOperationByCode(ctx, r.client, instanceUID, "redeploy", params); err != nil { diff --git a/universal_rebuild/internal/resources_gen/94_lucee_resource.go b/universal_rebuild/internal/resources_gen/94_lucee_resource.go index 09ccecc..bcb7439 100644 --- a/universal_rebuild/internal/resources_gen/94_lucee_resource.go +++ b/universal_rebuild/internal/resources_gen/94_lucee_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -10,8 +11,9 @@ import ( "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" ) @@ -31,15 +33,12 @@ type LuceeModel struct { ID types.String `tfsdk:"id"` ResourceName types.String `tfsdk:"resource_name"` OperationTimeout types.String `tfsdk:"operation_timeout"` - AppVersion types.String `tfsdk:"app_version"` - Domain types.String `tfsdk:"domain"` - GitPath types.String `tfsdk:"git_path"` - HealthPath types.String `tfsdk:"health_path"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration types.String `tfsdk:"access_configuration"` + AppConfiguration types.String `tfsdk:"app_configuration"` + ClusterConfiguration types.String `tfsdk:"cluster_configuration"` JsonEnv types.String `tfsdk:"json_env"` - 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"` + StartupConfiguration types.String `tfsdk:"startup_configuration"` SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` StateParams types.Map `tfsdk:"state_params"` @@ -62,18 +61,15 @@ func (r *LuceeResource) Metadata(ctx context.Context, req resource.MetadataReque func (r *LuceeResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, "resource_name": schema.StringAttribute{Required: true}, "operation_timeout": schema.StringAttribute{Optional: true}, - "app_version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("5.4"), MarkdownDescription: "Версия Lucee"}, - "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("web01"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."}, - "git_path": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("https://github.com/xahys/testlucee"), MarkdownDescription: "Путь до Git, который будет webroot для сайта"}, - "health_path": schema.StringAttribute{Optional: true, MarkdownDescription: "Путь до healtcheck для запуска пода в Kubernetes, который отдаст 200 код"}, - "json_env": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("{ \"param\": \"value\" }"), MarkdownDescription: "Здесь вы можете задать переменные среды в формате KEY=VALUE. Они будут доступны внутри вашего приложения."}, - "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(300), MarkdownDescription: "Квота ядра пода (milicores)"}, - "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: "Платформа для развертывания Не изменяется после создания."}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания."}, + "app_configuration": schema.StringAttribute{Required: true}, + "cluster_configuration": schema.StringAttribute{Required: true}, + "json_env": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("{}"), MarkdownDescription: "Передаётся в формате КЛЮЧ:ЗНАЧЕНИЕ"}, + "startup_configuration": 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}, @@ -124,22 +120,52 @@ func (r *LuceeResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanR return } } - if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { - if plan.Domain.ValueString() != state.Domain.ValueString() { - resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") + if !plan.AccessConfiguration.IsNull() && !plan.AccessConfiguration.IsUnknown() && !state.AccessConfiguration.IsNull() && !state.AccessConfiguration.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.AccessConfiguration.ValueString(), state.AccessConfiguration.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить access_configuration", "Параметр задается при создании и не может быть изменен.") return } } - if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { - if plan.ResourceRealm.ValueString() != state.ResourceRealm.ValueString() { - resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + if !plan.StartupConfiguration.IsNull() && !plan.StartupConfiguration.IsUnknown() && !state.StartupConfiguration.IsNull() && !state.StartupConfiguration.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.StartupConfiguration.ValueString(), state.StartupConfiguration.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить startup_configuration", "Параметр задается при создании и не может быть изменен.") return } } return } - if config.ResourceRealm.IsNull() || config.ResourceRealm.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + if config.AccessConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "access_configuration is required.") + return + } + if config.AccessConfiguration.IsUnknown() { + return + } + if config.AppConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "app_configuration is required.") + return + } + if config.AppConfiguration.IsUnknown() { + return + } + if config.ClusterConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "cluster_configuration is required.") + return + } + if config.ClusterConfiguration.IsUnknown() { + return + } + if config.StartupConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "startup_configuration is required.") + return + } + if config.StartupConfiguration.IsUnknown() { return } @@ -151,18 +177,16 @@ func (r *LuceeResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanR adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() } params := map[int]string{ - 103: resources_core.FormatString(config.Domain), - 104: resources_core.FormatString(config.GitPath), - 105: resources_core.FormatString(config.JsonEnv), - 106: resources_core.FormatInt64(config.ResourceCPU), - 107: resources_core.FormatInt64(config.ResourceMemory), - 108: resources_core.FormatString(config.ResourceRealm), - 132: resources_core.FormatString(config.HealthPath), - 139: resources_core.FormatInt64(config.ResourceInstances), - 263: resources_core.FormatString(config.AppVersion), + 1019: resources_core.FormatString(config.StartupConfiguration), + 1020: resources_core.FormatString(config.ClusterConfiguration), + 1021: resources_core.FormatString(config.AccessConfiguration), + 1022: resources_core.FormatString(config.AppConfiguration), + 1023: resources_core.FormatString(config.JsonEnv), } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} - resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 94, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -171,30 +195,43 @@ func (r *LuceeResource) Create(ctx context.Context, req resource.CreateRequest, if resp.Diagnostics.HasError() { return } - if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + if data.AccessConfiguration.IsNull() || data.AccessConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "access_configuration is required.") + return + } + if data.AppConfiguration.IsNull() || data.AppConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "app_configuration is required.") + return + } + if data.ClusterConfiguration.IsNull() || data.ClusterConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "cluster_configuration is required.") + return + } + if data.StartupConfiguration.IsNull() || data.StartupConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "startup_configuration is required.") return } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 94, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ - 103: resources_core.FormatString(data.Domain), - 104: resources_core.FormatString(data.GitPath), - 105: resources_core.FormatString(data.JsonEnv), - 106: resources_core.FormatInt64(data.ResourceCPU), - 107: resources_core.FormatInt64(data.ResourceMemory), - 108: resources_core.FormatString(data.ResourceRealm), - 132: resources_core.FormatString(data.HealthPath), - 139: resources_core.FormatInt64(data.ResourceInstances), - 263: resources_core.FormatString(data.AppVersion), + 1019: resources_core.FormatString(data.StartupConfiguration), + 1020: resources_core.FormatString(data.ClusterConfiguration), + 1021: resources_core.FormatString(data.AccessConfiguration), + 1022: resources_core.FormatString(data.AppConfiguration), + 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()) @@ -212,15 +249,11 @@ func (r *LuceeResource) Create(ctx context.Context, req resource.CreateRequest, {Code: "vault_user_path"}, {Code: "vault_fields"}, }, []resources_core.InputField{ - {Code: "appVersion", Field: "AppVersion", Type: "string"}, - {Code: "domain", Field: "Domain", Type: "string"}, - {Code: "gitPath", Field: "GitPath", Type: "string"}, - {Code: "healthPath", Field: "HealthPath", Type: "string"}, + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "string"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "string"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "string"}, {Code: "jsonEnv", Field: "JsonEnv", 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"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "string"}, }) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -261,15 +294,11 @@ func (r *LuceeResource) Read(ctx context.Context, req resource.ReadRequest, resp {Code: "vault_user_path"}, {Code: "vault_fields"}, }, []resources_core.InputField{ - {Code: "appVersion", Field: "AppVersion", Type: "string"}, - {Code: "domain", Field: "Domain", Type: "string"}, - {Code: "gitPath", Field: "GitPath", Type: "string"}, - {Code: "healthPath", Field: "HealthPath", Type: "string"}, + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "string"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "string"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "string"}, {Code: "jsonEnv", Field: "JsonEnv", 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"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "string"}, }) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -299,19 +328,19 @@ func (r *LuceeResource) Update(ctx context.Context, req resource.UpdateRequest, hasServiceParamChanges := false if !hasServiceParamChanges { - if plan.GitPath.IsNull() != state.GitPath.IsNull() || plan.GitPath.IsUnknown() != state.GitPath.IsUnknown() { + if plan.ClusterConfiguration.IsNull() != state.ClusterConfiguration.IsNull() || plan.ClusterConfiguration.IsUnknown() != state.ClusterConfiguration.IsUnknown() { hasServiceParamChanges = true - } else if !plan.GitPath.IsNull() && !plan.GitPath.IsUnknown() && !state.GitPath.IsNull() && !state.GitPath.IsUnknown() { - if plan.GitPath.ValueString() != state.GitPath.ValueString() { + } else if !plan.ClusterConfiguration.IsNull() && !plan.ClusterConfiguration.IsUnknown() && !state.ClusterConfiguration.IsNull() && !state.ClusterConfiguration.IsUnknown() { + if !strings.EqualFold(plan.ClusterConfiguration.ValueString(), state.ClusterConfiguration.ValueString()) { hasServiceParamChanges = true } } } if !hasServiceParamChanges { - if plan.HealthPath.IsNull() != state.HealthPath.IsNull() || plan.HealthPath.IsUnknown() != state.HealthPath.IsUnknown() { + if plan.AppConfiguration.IsNull() != state.AppConfiguration.IsNull() || plan.AppConfiguration.IsUnknown() != state.AppConfiguration.IsUnknown() { hasServiceParamChanges = true - } else if !plan.HealthPath.IsNull() && !plan.HealthPath.IsUnknown() && !state.HealthPath.IsNull() && !state.HealthPath.IsUnknown() { - if plan.HealthPath.ValueString() != state.HealthPath.ValueString() { + } else if !plan.AppConfiguration.IsNull() && !plan.AppConfiguration.IsUnknown() && !state.AppConfiguration.IsNull() && !state.AppConfiguration.IsUnknown() { + if !strings.EqualFold(plan.AppConfiguration.ValueString(), state.AppConfiguration.ValueString()) { hasServiceParamChanges = true } } @@ -320,43 +349,7 @@ func (r *LuceeResource) Update(ctx context.Context, req resource.UpdateRequest, 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 plan.JsonEnv.ValueString() != state.JsonEnv.ValueString() { - 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.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.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 plan.AppVersion.ValueString() != state.AppVersion.ValueString() { + if !strings.EqualFold(plan.JsonEnv.ValueString(), state.JsonEnv.ValueString()) { hasServiceParamChanges = true } } @@ -377,19 +370,18 @@ func (r *LuceeResource) Update(ctx context.Context, req resource.UpdateRequest, } params := map[int]string{ - 133: resources_core.FormatString(plan.GitPath), - 134: resources_core.FormatString(plan.HealthPath), - 135: resources_core.FormatString(plan.JsonEnv), - 136: resources_core.FormatInt64(plan.ResourceCPU), - 137: resources_core.FormatInt64(plan.ResourceMemory), - 138: resources_core.FormatInt64(plan.ResourceInstances), - 264: resources_core.FormatString(plan.AppVersion), + 1024: resources_core.FormatString(plan.ClusterConfiguration), + 1025: resources_core.FormatString(plan.AppConfiguration), + 1026: resources_core.FormatString(plan.JsonEnv), } operationTimeout := "" if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -406,15 +398,11 @@ func (r *LuceeResource) Update(ctx context.Context, req resource.UpdateRequest, {Code: "vault_user_path"}, {Code: "vault_fields"}, }, []resources_core.InputField{ - {Code: "appVersion", Field: "AppVersion", Type: "string"}, - {Code: "domain", Field: "Domain", Type: "string"}, - {Code: "gitPath", Field: "GitPath", Type: "string"}, - {Code: "healthPath", Field: "HealthPath", Type: "string"}, + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "string"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "string"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "string"}, {Code: "jsonEnv", Field: "JsonEnv", 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"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "string"}, }) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -433,7 +421,6 @@ func (r *LuceeResource) Delete(ctx context.Context, req resource.DeleteRequest, if state.ID.IsNull() || state.ID.IsUnknown() { return } - deleteMode := "state_only" if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { deleteMode = "suspend" @@ -443,6 +430,9 @@ func (r *LuceeResource) Delete(ctx context.Context, req resource.DeleteRequest, 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 diff --git a/universal_rebuild/internal/resources_gen/94_lucee_restart_action.go b/universal_rebuild/internal/resources_gen/94_lucee_restart_action.go index 4432ff7..f7614f0 100644 --- a/universal_rebuild/internal/resources_gen/94_lucee_restart_action.go +++ b/universal_rebuild/internal/resources_gen/94_lucee_restart_action.go @@ -9,6 +9,7 @@ import ( "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/types" ) @@ -24,10 +25,12 @@ type LuceeRestartResource struct { } type LuceeRestartModel struct { - ID types.String `tfsdk:"id"` - LuceeID types.String `tfsdk:"lucee_id"` - RunID types.String `tfsdk:"run_id"` - OperationTimeout types.String `tfsdk:"operation_timeout"` + ID types.String `tfsdk:"id"` + LuceeID types.String `tfsdk:"lucee_id"` + RunID types.String `tfsdk:"run_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` } func NewLuceeRestartResource() resource.Resource { @@ -40,10 +43,12 @@ func (r *LuceeRestartResource) Metadata(ctx context.Context, req resource.Metada func (r *LuceeRestartResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, - "lucee_id": schema.StringAttribute{Required: true}, - "run_id": schema.StringAttribute{Required: true}, - "operation_timeout": schema.StringAttribute{Optional: true}, + "id": schema.StringAttribute{Computed: true}, + "lucee_id": schema.StringAttribute{Required: true}, + "run_id": schema.StringAttribute{Required: true}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "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} } @@ -68,6 +73,9 @@ func (r *LuceeRestartResource) Create(ctx context.Context, req resource.CreateRe if !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, "restart", params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return diff --git a/universal_rebuild/internal/resources_gen/95_nodejs_reconcile_action.go b/universal_rebuild/internal/resources_gen/95_nodejs_reconcile_action.go new file mode 100644 index 0000000..90ff262 --- /dev/null +++ b/universal_rebuild/internal/resources_gen/95_nodejs_reconcile_action.go @@ -0,0 +1,136 @@ +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/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: nodejs +// Action: reconcile + +var _ resource.Resource = &NodejsReconcileResource{} + +// NodejsReconcileResource triggers a one-time action. +type NodejsReconcileResource struct { + client *core.UniversalClient +} + +type NodejsReconcileModel struct { + ID types.String `tfsdk:"id"` + NodejsID types.String `tfsdk:"nodejs_id"` + RunID types.String `tfsdk:"run_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` +} + +func NewNodejsReconcileResource() resource.Resource { + return &NodejsReconcileResource{} +} + +func (r *NodejsReconcileResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_nodejs_reconcile" +} + +func (r *NodejsReconcileResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true}, + "nodejs_id": schema.StringAttribute{Required: true}, + "run_id": schema.StringAttribute{Required: true}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "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 *NodejsReconcileResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan NodejsReconcileModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.NodejsID.ValueString()) + runID := strings.TrimSpace(plan.RunID.ValueString()) + if instanceUID == "" || runID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра или run_id") + return + } + + params := 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, "reconcile", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "reconcile", runID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *NodejsReconcileResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state NodejsReconcileModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *NodejsReconcileResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan NodejsReconcileModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.NodejsID.ValueString()) + runID := strings.TrimSpace(plan.RunID.ValueString()) + if instanceUID == "" || runID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра или run_id") + return + } + + params := map[string]string{} + + if err := resources_core.RunOperationByCode(ctx, r.client, instanceUID, "reconcile", params); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "reconcile", runID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *NodejsReconcileResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // No-op: removing the resource does not trigger a remote action. +} + +func (r *NodejsReconcileResource) 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/universal_rebuild/internal/resources_gen/95_nodejs_redeploy_action.go b/universal_rebuild/internal/resources_gen/95_nodejs_redeploy_action.go index 11804bc..d767ed5 100644 --- a/universal_rebuild/internal/resources_gen/95_nodejs_redeploy_action.go +++ b/universal_rebuild/internal/resources_gen/95_nodejs_redeploy_action.go @@ -9,6 +9,7 @@ import ( "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/types" ) @@ -24,11 +25,13 @@ type NodejsRedeployResource struct { } type NodejsRedeployModel struct { - ID types.String `tfsdk:"id"` - NodejsID types.String `tfsdk:"nodejs_id"` - RunID types.String `tfsdk:"run_id"` - OperationTimeout types.String `tfsdk:"operation_timeout"` - ResourceRealm types.String `tfsdk:"resource_realm"` + ID types.String `tfsdk:"id"` + NodejsID types.String `tfsdk:"nodejs_id"` + RunID types.String `tfsdk:"run_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + StartupConfiguration types.String `tfsdk:"startup_configuration"` } func NewNodejsRedeployResource() resource.Resource { @@ -41,11 +44,13 @@ func (r *NodejsRedeployResource) Metadata(ctx context.Context, req resource.Meta func (r *NodejsRedeployResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, - "nodejs_id": schema.StringAttribute{Required: true}, - "run_id": schema.StringAttribute{Required: true}, - "operation_timeout": schema.StringAttribute{Optional: true}, - "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания"}, + "id": schema.StringAttribute{Computed: true}, + "nodejs_id": schema.StringAttribute{Required: true}, + "run_id": schema.StringAttribute{Required: true}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "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}, } resp.Schema = schema.Schema{Attributes: attrs} } @@ -56,8 +61,8 @@ func (r *NodejsRedeployResource) Create(ctx context.Context, req resource.Create if resp.Diagnostics.HasError() { return } - if plan.ResourceRealm.IsNull() || plan.ResourceRealm.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + if plan.StartupConfiguration.IsNull() || plan.StartupConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "startup_configuration is required.") return } @@ -69,13 +74,16 @@ func (r *NodejsRedeployResource) Create(ctx context.Context, req resource.Create } params := map[string]string{ - "resourceRealm": resources_core.FormatString(plan.ResourceRealm), + "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, "redeploy", params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -109,7 +117,7 @@ func (r *NodejsRedeployResource) Update(ctx context.Context, req resource.Update } params := map[string]string{ - "resourceRealm": resources_core.FormatString(plan.ResourceRealm), + "startupConfiguration": resources_core.FormatString(plan.StartupConfiguration), } if err := resources_core.RunOperationByCode(ctx, r.client, instanceUID, "redeploy", params); err != nil { diff --git a/universal_rebuild/internal/resources_gen/95_nodejs_resource.go b/universal_rebuild/internal/resources_gen/95_nodejs_resource.go index a6be789..e638e34 100644 --- a/universal_rebuild/internal/resources_gen/95_nodejs_resource.go +++ b/universal_rebuild/internal/resources_gen/95_nodejs_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -10,8 +11,9 @@ import ( "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" ) @@ -31,15 +33,12 @@ type NodejsModel struct { ID types.String `tfsdk:"id"` ResourceName types.String `tfsdk:"resource_name"` OperationTimeout types.String `tfsdk:"operation_timeout"` - AppVersion types.String `tfsdk:"app_version"` - Domain types.String `tfsdk:"domain"` - GitPath types.String `tfsdk:"git_path"` - HealthPath types.String `tfsdk:"health_path"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration types.String `tfsdk:"access_configuration"` + AppConfiguration types.String `tfsdk:"app_configuration"` + ClusterConfiguration types.String `tfsdk:"cluster_configuration"` JsonEnv types.String `tfsdk:"json_env"` - 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"` + StartupConfiguration types.String `tfsdk:"startup_configuration"` SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` StateParams types.Map `tfsdk:"state_params"` @@ -62,18 +61,15 @@ func (r *NodejsResource) Metadata(ctx context.Context, req resource.MetadataRequ func (r *NodejsResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, "resource_name": schema.StringAttribute{Required: true}, "operation_timeout": schema.StringAttribute{Optional: true}, - "app_version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("23"), MarkdownDescription: "Версия приложения"}, - "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("web01"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."}, - "git_path": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("https://github.com/xahys/testnode.git"), MarkdownDescription: "Путь до Git, который будет webroot для сайта"}, - "health_path": schema.StringAttribute{Optional: true, MarkdownDescription: "Путь до healtcheck для запуска пода в Kubernetes, который отдаст 200 код. Если оставить пустым, healtcheck'а не будет"}, - "json_env": schema.StringAttribute{Optional: true, MarkdownDescription: "Массив переменных, будут переданы в env приложения"}, - "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Квота ядра пода (milicores). 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: "Квота памяти пода (MBytes)"}, - "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания."}, + "app_configuration": schema.StringAttribute{Required: true}, + "cluster_configuration": schema.StringAttribute{Required: true}, + "json_env": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("{}"), MarkdownDescription: "Передаётся в формате КЛЮЧ:ЗНАЧЕНИЕ"}, + "startup_configuration": 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}, @@ -124,22 +120,52 @@ func (r *NodejsResource) ModifyPlan(ctx context.Context, req resource.ModifyPlan return } } - if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { - if plan.Domain.ValueString() != state.Domain.ValueString() { - resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") + if !plan.AccessConfiguration.IsNull() && !plan.AccessConfiguration.IsUnknown() && !state.AccessConfiguration.IsNull() && !state.AccessConfiguration.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.AccessConfiguration.ValueString(), state.AccessConfiguration.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить access_configuration", "Параметр задается при создании и не может быть изменен.") return } } - if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { - if plan.ResourceRealm.ValueString() != state.ResourceRealm.ValueString() { - resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + if !plan.StartupConfiguration.IsNull() && !plan.StartupConfiguration.IsUnknown() && !state.StartupConfiguration.IsNull() && !state.StartupConfiguration.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.StartupConfiguration.ValueString(), state.StartupConfiguration.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить startup_configuration", "Параметр задается при создании и не может быть изменен.") return } } return } - if config.ResourceRealm.IsNull() || config.ResourceRealm.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + if config.AccessConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "access_configuration is required.") + return + } + if config.AccessConfiguration.IsUnknown() { + return + } + if config.AppConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "app_configuration is required.") + return + } + if config.AppConfiguration.IsUnknown() { + return + } + if config.ClusterConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "cluster_configuration is required.") + return + } + if config.ClusterConfiguration.IsUnknown() { + return + } + if config.StartupConfiguration.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "startup_configuration is required.") + return + } + if config.StartupConfiguration.IsUnknown() { return } @@ -151,18 +177,16 @@ func (r *NodejsResource) ModifyPlan(ctx context.Context, req resource.ModifyPlan adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() } params := map[int]string{ - 149: resources_core.FormatString(config.Domain), - 150: resources_core.FormatString(config.GitPath), - 151: resources_core.FormatString(config.HealthPath), - 152: resources_core.FormatString(config.JsonEnv), - 153: resources_core.FormatInt64(config.ResourceCPU), - 154: resources_core.FormatInt64(config.ResourceMemory), - 155: resources_core.FormatInt64(config.ResourceInstances), - 156: resources_core.FormatString(config.ResourceRealm), - 270: resources_core.FormatString(config.AppVersion), + 1028: resources_core.FormatString(config.StartupConfiguration), + 1029: resources_core.FormatString(config.ClusterConfiguration), + 1030: resources_core.FormatString(config.AccessConfiguration), + 1031: resources_core.FormatString(config.AppConfiguration), + 1032: resources_core.FormatString(config.JsonEnv), } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} - resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 95, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -171,30 +195,43 @@ func (r *NodejsResource) Create(ctx context.Context, req resource.CreateRequest, if resp.Diagnostics.HasError() { return } - if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() { - resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + if data.AccessConfiguration.IsNull() || data.AccessConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "access_configuration is required.") + return + } + if data.AppConfiguration.IsNull() || data.AppConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "app_configuration is required.") + return + } + if data.ClusterConfiguration.IsNull() || data.ClusterConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "cluster_configuration is required.") + return + } + if data.StartupConfiguration.IsNull() || data.StartupConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "startup_configuration is required.") return } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 95, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ - 149: resources_core.FormatString(data.Domain), - 150: resources_core.FormatString(data.GitPath), - 151: resources_core.FormatString(data.HealthPath), - 152: resources_core.FormatString(data.JsonEnv), - 153: resources_core.FormatInt64(data.ResourceCPU), - 154: resources_core.FormatInt64(data.ResourceMemory), - 155: resources_core.FormatInt64(data.ResourceInstances), - 156: resources_core.FormatString(data.ResourceRealm), - 270: resources_core.FormatString(data.AppVersion), + 1028: resources_core.FormatString(data.StartupConfiguration), + 1029: resources_core.FormatString(data.ClusterConfiguration), + 1030: resources_core.FormatString(data.AccessConfiguration), + 1031: resources_core.FormatString(data.AppConfiguration), + 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()) @@ -212,15 +249,11 @@ func (r *NodejsResource) Create(ctx context.Context, req resource.CreateRequest, {Code: "vault_user_path"}, {Code: "vault_fields"}, }, []resources_core.InputField{ - {Code: "appVersion", Field: "AppVersion", Type: "string"}, - {Code: "domain", Field: "Domain", Type: "string"}, - {Code: "gitPath", Field: "GitPath", Type: "string"}, - {Code: "healthPath", Field: "HealthPath", Type: "string"}, + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "string"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "string"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "string"}, {Code: "jsonEnv", Field: "JsonEnv", 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"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "string"}, }) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -261,15 +294,11 @@ func (r *NodejsResource) Read(ctx context.Context, req resource.ReadRequest, res {Code: "vault_user_path"}, {Code: "vault_fields"}, }, []resources_core.InputField{ - {Code: "appVersion", Field: "AppVersion", Type: "string"}, - {Code: "domain", Field: "Domain", Type: "string"}, - {Code: "gitPath", Field: "GitPath", Type: "string"}, - {Code: "healthPath", Field: "HealthPath", Type: "string"}, + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "string"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "string"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "string"}, {Code: "jsonEnv", Field: "JsonEnv", 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"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "string"}, }) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -299,19 +328,19 @@ func (r *NodejsResource) Update(ctx context.Context, req resource.UpdateRequest, hasServiceParamChanges := false if !hasServiceParamChanges { - if plan.GitPath.IsNull() != state.GitPath.IsNull() || plan.GitPath.IsUnknown() != state.GitPath.IsUnknown() { + if plan.ClusterConfiguration.IsNull() != state.ClusterConfiguration.IsNull() || plan.ClusterConfiguration.IsUnknown() != state.ClusterConfiguration.IsUnknown() { hasServiceParamChanges = true - } else if !plan.GitPath.IsNull() && !plan.GitPath.IsUnknown() && !state.GitPath.IsNull() && !state.GitPath.IsUnknown() { - if plan.GitPath.ValueString() != state.GitPath.ValueString() { + } else if !plan.ClusterConfiguration.IsNull() && !plan.ClusterConfiguration.IsUnknown() && !state.ClusterConfiguration.IsNull() && !state.ClusterConfiguration.IsUnknown() { + if !strings.EqualFold(plan.ClusterConfiguration.ValueString(), state.ClusterConfiguration.ValueString()) { hasServiceParamChanges = true } } } if !hasServiceParamChanges { - if plan.HealthPath.IsNull() != state.HealthPath.IsNull() || plan.HealthPath.IsUnknown() != state.HealthPath.IsUnknown() { + if plan.AppConfiguration.IsNull() != state.AppConfiguration.IsNull() || plan.AppConfiguration.IsUnknown() != state.AppConfiguration.IsUnknown() { hasServiceParamChanges = true - } else if !plan.HealthPath.IsNull() && !plan.HealthPath.IsUnknown() && !state.HealthPath.IsNull() && !state.HealthPath.IsUnknown() { - if plan.HealthPath.ValueString() != state.HealthPath.ValueString() { + } else if !plan.AppConfiguration.IsNull() && !plan.AppConfiguration.IsUnknown() && !state.AppConfiguration.IsNull() && !state.AppConfiguration.IsUnknown() { + if !strings.EqualFold(plan.AppConfiguration.ValueString(), state.AppConfiguration.ValueString()) { hasServiceParamChanges = true } } @@ -320,43 +349,7 @@ func (r *NodejsResource) Update(ctx context.Context, req resource.UpdateRequest, 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 plan.JsonEnv.ValueString() != state.JsonEnv.ValueString() { - 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.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.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 plan.AppVersion.ValueString() != state.AppVersion.ValueString() { + if !strings.EqualFold(plan.JsonEnv.ValueString(), state.JsonEnv.ValueString()) { hasServiceParamChanges = true } } @@ -377,19 +370,18 @@ func (r *NodejsResource) Update(ctx context.Context, req resource.UpdateRequest, } params := map[int]string{ - 157: resources_core.FormatString(plan.GitPath), - 158: resources_core.FormatString(plan.HealthPath), - 159: resources_core.FormatString(plan.JsonEnv), - 160: resources_core.FormatInt64(plan.ResourceCPU), - 161: resources_core.FormatInt64(plan.ResourceMemory), - 162: resources_core.FormatInt64(plan.ResourceInstances), - 271: resources_core.FormatString(plan.AppVersion), + 1033: resources_core.FormatString(plan.ClusterConfiguration), + 1034: resources_core.FormatString(plan.AppConfiguration), + 1035: resources_core.FormatString(plan.JsonEnv), } operationTimeout := "" if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -406,15 +398,11 @@ func (r *NodejsResource) Update(ctx context.Context, req resource.UpdateRequest, {Code: "vault_user_path"}, {Code: "vault_fields"}, }, []resources_core.InputField{ - {Code: "appVersion", Field: "AppVersion", Type: "string"}, - {Code: "domain", Field: "Domain", Type: "string"}, - {Code: "gitPath", Field: "GitPath", Type: "string"}, - {Code: "healthPath", Field: "HealthPath", Type: "string"}, + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "string"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "string"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "string"}, {Code: "jsonEnv", Field: "JsonEnv", 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"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "string"}, }) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -433,7 +421,6 @@ func (r *NodejsResource) Delete(ctx context.Context, req resource.DeleteRequest, if state.ID.IsNull() || state.ID.IsUnknown() { return } - deleteMode := "state_only" if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { deleteMode = "suspend" @@ -443,6 +430,9 @@ func (r *NodejsResource) Delete(ctx context.Context, req resource.DeleteRequest, 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 diff --git a/universal_rebuild/internal/resources_gen/96_pgadmin_redeploy_action.go b/universal_rebuild/internal/resources_gen/96_pgadmin_redeploy_action.go index 2ad2820..b2bda6e 100644 --- a/universal_rebuild/internal/resources_gen/96_pgadmin_redeploy_action.go +++ b/universal_rebuild/internal/resources_gen/96_pgadmin_redeploy_action.go @@ -9,6 +9,7 @@ import ( "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/types" ) @@ -24,11 +25,13 @@ type PgadminRedeployResource struct { } type PgadminRedeployModel struct { - ID types.String `tfsdk:"id"` - PgadminID types.String `tfsdk:"pgadmin_id"` - RunID types.String `tfsdk:"run_id"` - OperationTimeout types.String `tfsdk:"operation_timeout"` - ResourceRealm types.String `tfsdk:"resource_realm"` + ID types.String `tfsdk:"id"` + PgadminID types.String `tfsdk:"pgadmin_id"` + RunID types.String `tfsdk:"run_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + ResourceRealm types.String `tfsdk:"resource_realm"` } func NewPgadminRedeployResource() resource.Resource { @@ -41,11 +44,13 @@ func (r *PgadminRedeployResource) Metadata(ctx context.Context, req resource.Met func (r *PgadminRedeployResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, - "pgadmin_id": schema.StringAttribute{Required: true}, - "run_id": schema.StringAttribute{Required: true}, - "operation_timeout": schema.StringAttribute{Optional: true}, - "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания"}, + "id": schema.StringAttribute{Computed: true}, + "pgadmin_id": schema.StringAttribute{Required: true}, + "run_id": schema.StringAttribute{Required: true}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "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: "Платформа для развертывания"}, } resp.Schema = schema.Schema{Attributes: attrs} } @@ -76,6 +81,9 @@ func (r *PgadminRedeployResource) Create(ctx context.Context, req resource.Creat if !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, "redeploy", params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return diff --git a/universal_rebuild/internal/resources_gen/96_pgadmin_resource.go b/universal_rebuild/internal/resources_gen/96_pgadmin_resource.go index f94e314..dd1136e 100644 --- a/universal_rebuild/internal/resources_gen/96_pgadmin_resource.go +++ b/universal_rebuild/internal/resources_gen/96_pgadmin_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -11,7 +12,9 @@ import ( "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" ) @@ -31,6 +34,7 @@ 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"` Domain types.String `tfsdk:"domain"` Login types.String `tfsdk:"login"` Password types.String `tfsdk:"password"` @@ -60,15 +64,16 @@ func (r *PgadminResource) Metadata(ctx context.Context, req resource.MetadataReq func (r *PgadminResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "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("web01"), 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(200), MarkdownDescription: "Квота ядра пода (milicores). 1000Mili = 1 Ядро"}, + "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(256), MarkdownDescription: "Квота памяти пода (MBytes)"}, + "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(true)}, "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, @@ -121,43 +126,64 @@ func (r *PgadminResource) ModifyPlan(ctx context.Context, req resource.ModifyPla } } if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { - if plan.Domain.ValueString() != state.Domain.ValueString() { + // 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() { - if plan.Login.ValueString() != state.Login.ValueString() { + // 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() { - if plan.Password.ValueString() != state.Password.ValueString() { + // 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() { - if plan.ResourceRealm.ValueString() != state.ResourceRealm.ValueString() { + // 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() || config.Login.IsUnknown() { + if config.Login.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "login is required.") return } - if config.Password.IsNull() || config.Password.IsUnknown() { + if config.Login.IsUnknown() { + return + } + if config.Password.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "password is required.") return } - if config.ResourceRealm.IsNull() || config.ResourceRealm.IsUnknown() { + 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 @@ -175,8 +201,13 @@ func (r *PgadminResource) ModifyPlan(ctx context.Context, req resource.ModifyPla 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.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 96, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -199,7 +230,12 @@ func (r *PgadminResource) Create(ctx context.Context, req resource.CreateRequest } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 96, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ 164: resources_core.FormatString(data.Domain), @@ -215,6 +251,9 @@ func (r *PgadminResource) Create(ctx context.Context, req resource.CreateRequest 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()) @@ -366,6 +405,9 @@ func (r *PgadminResource) Update(ctx context.Context, req resource.UpdateRequest if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -407,7 +449,6 @@ func (r *PgadminResource) Delete(ctx context.Context, req resource.DeleteRequest if state.ID.IsNull() || state.ID.IsUnknown() { return } - deleteMode := "state_only" if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { deleteMode = "suspend" @@ -417,6 +458,9 @@ func (r *PgadminResource) Delete(ctx context.Context, req resource.DeleteRequest 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 diff --git a/universal_rebuild/internal/resources_gen/97_nodered_resource.go b/universal_rebuild/internal/resources_gen/97_nodered_resource.go index 6ce2414..af05cdd 100644 --- a/universal_rebuild/internal/resources_gen/97_nodered_resource.go +++ b/universal_rebuild/internal/resources_gen/97_nodered_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -11,6 +12,8 @@ import ( "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" ) @@ -30,6 +33,7 @@ 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"` @@ -58,9 +62,10 @@ func (r *NoderedResource) Metadata(ctx context.Context, req resource.MetadataReq func (r *NoderedResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "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: "Наименование поддомена, будет добавлено в 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) Не изменяется после создания."}, @@ -118,7 +123,10 @@ func (r *NoderedResource) ModifyPlan(ctx context.Context, req resource.ModifyPla } } if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { - if plan.Domain.ValueString() != state.Domain.ValueString() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) { resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") return } @@ -148,21 +156,30 @@ func (r *NoderedResource) ModifyPlan(ctx context.Context, req resource.ModifyPla } } if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { - if plan.ResourceRealm.ValueString() != state.ResourceRealm.ValueString() { + // 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() || config.Domain.IsUnknown() { + if config.Domain.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "domain is required.") return } - if config.ResourceRealm.IsNull() || config.ResourceRealm.IsUnknown() { + if config.Domain.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 @@ -179,8 +196,13 @@ func (r *NoderedResource) ModifyPlan(ctx context.Context, req resource.ModifyPla 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.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 97, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -199,7 +221,12 @@ func (r *NoderedResource) Create(ctx context.Context, req resource.CreateRequest } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 97, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ 193: resources_core.FormatInt64(data.ResourceCPU), @@ -214,6 +241,9 @@ func (r *NoderedResource) Create(ctx context.Context, req resource.CreateRequest 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()) @@ -332,6 +362,9 @@ func (r *NoderedResource) Update(ctx context.Context, req resource.UpdateRequest if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -372,16 +405,15 @@ func (r *NoderedResource) Delete(ctx context.Context, req resource.DeleteRequest if state.ID.IsNull() || state.ID.IsUnknown() { return } - - deleteMode := "state_only" - if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { - deleteMode = "suspend" - } + 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 diff --git a/universal_rebuild/internal/resources_gen/98_http_redeploy_action.go b/universal_rebuild/internal/resources_gen/98_http_redeploy_action.go index a3f4ea0..ef28e4b 100644 --- a/universal_rebuild/internal/resources_gen/98_http_redeploy_action.go +++ b/universal_rebuild/internal/resources_gen/98_http_redeploy_action.go @@ -9,6 +9,7 @@ import ( "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/types" ) @@ -24,11 +25,13 @@ type HttpRedeployResource struct { } type HttpRedeployModel struct { - ID types.String `tfsdk:"id"` - HttpID types.String `tfsdk:"http_id"` - RunID types.String `tfsdk:"run_id"` - OperationTimeout types.String `tfsdk:"operation_timeout"` - ResourceRealm types.String `tfsdk:"resource_realm"` + ID types.String `tfsdk:"id"` + HttpID types.String `tfsdk:"http_id"` + RunID types.String `tfsdk:"run_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + ResourceRealm types.String `tfsdk:"resource_realm"` } func NewHttpRedeployResource() resource.Resource { @@ -41,11 +44,13 @@ func (r *HttpRedeployResource) Metadata(ctx context.Context, req resource.Metada func (r *HttpRedeployResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, - "http_id": schema.StringAttribute{Required: true}, - "run_id": schema.StringAttribute{Required: true}, - "operation_timeout": schema.StringAttribute{Optional: true}, - "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания"}, + "id": schema.StringAttribute{Computed: true}, + "http_id": schema.StringAttribute{Required: true}, + "run_id": schema.StringAttribute{Required: true}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "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: "Платформа для развертывания"}, } resp.Schema = schema.Schema{Attributes: attrs} } @@ -76,6 +81,9 @@ func (r *HttpRedeployResource) Create(ctx context.Context, req resource.CreateRe if !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, "redeploy", params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return diff --git a/universal_rebuild/internal/resources_gen/98_http_resource.go b/universal_rebuild/internal/resources_gen/98_http_resource.go index 63dbf17..1c7a5fb 100644 --- a/universal_rebuild/internal/resources_gen/98_http_resource.go +++ b/universal_rebuild/internal/resources_gen/98_http_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -11,7 +12,9 @@ import ( "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" ) @@ -31,7 +34,9 @@ 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"` 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"` @@ -60,11 +65,13 @@ func (r *HttpResource) Metadata(ctx context.Context, req resource.MetadataReques func (r *HttpResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, + "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("web01"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."}, - "json_env": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("{"param": "value"}"), MarkdownDescription: "Массив переменных, будут переданы в env приложения Не изменяется после создания."}, + "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: "Количество узлов"}, @@ -121,35 +128,59 @@ func (r *HttpResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRe } } if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { - if plan.Domain.ValueString() != state.Domain.ValueString() { + // 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() { - if plan.JsonEnv.ValueString() != state.JsonEnv.ValueString() { + // 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() { - if plan.RegistryPath.ValueString() != state.RegistryPath.ValueString() { + // 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() { - if plan.ResourceRealm.ValueString() != state.ResourceRealm.ValueString() { + // 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() || config.ResourceRealm.IsUnknown() { + 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 @@ -166,9 +197,15 @@ func (r *HttpResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRe 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.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 98, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -183,7 +220,12 @@ func (r *HttpResource) Create(ctx context.Context, req resource.CreateRequest, r } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 98, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ 212: resources_core.FormatString(data.Domain), @@ -193,12 +235,16 @@ func (r *HttpResource) Create(ctx context.Context, req resource.CreateRequest, r 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()) @@ -217,6 +263,7 @@ func (r *HttpResource) Create(ctx context.Context, req resource.CreateRequest, r {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"}, @@ -264,6 +311,7 @@ func (r *HttpResource) Read(ctx context.Context, req resource.ReadRequest, resp {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"}, @@ -350,6 +398,9 @@ func (r *HttpResource) Update(ctx context.Context, req resource.UpdateRequest, r if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -367,6 +418,7 @@ func (r *HttpResource) Update(ctx context.Context, req resource.UpdateRequest, r {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"}, @@ -391,7 +443,6 @@ func (r *HttpResource) Delete(ctx context.Context, req resource.DeleteRequest, r if state.ID.IsNull() || state.ID.IsUnknown() { return } - deleteMode := "state_only" if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { deleteMode = "suspend" @@ -401,6 +452,9 @@ func (r *HttpResource) Delete(ctx context.Context, req resource.DeleteRequest, r 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 diff --git a/universal_rebuild/internal/resources_gen/99_gitea_resource.go b/universal_rebuild/internal/resources_gen/99_gitea_resource.go index b2a90ac..8848384 100644 --- a/universal_rebuild/internal/resources_gen/99_gitea_resource.go +++ b/universal_rebuild/internal/resources_gen/99_gitea_resource.go @@ -2,6 +2,7 @@ package resources_gen import ( "context" + "strings" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -11,7 +12,9 @@ import ( "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" ) @@ -28,25 +31,28 @@ type GiteaResource struct { } type GiteaModel struct { - ID types.String `tfsdk:"id"` - ResourceName types.String `tfsdk:"resource_name"` - OperationTimeout types.String `tfsdk:"operation_timeout"` - Domain types.String `tfsdk:"domain"` - 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"` + 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 { @@ -59,25 +65,28 @@ func (r *GiteaResource) Metadata(ctx context.Context, req resource.MetadataReque func (r *GiteaResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ - "id": schema.StringAttribute{Computed: true}, - "resource_name": schema.StringAttribute{Required: true}, - "operation_timeout": schema.StringAttribute{Optional: true}, - "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("web01"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."}, - "psql_uid": schema.StringAttribute{Required: 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}, + "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("web01"), 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} @@ -112,22 +121,11 @@ func (r *GiteaResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanR if resp.Diagnostics.HasError() { return } - planChanged := false - if !plan.PsqlUid.IsNull() && !plan.PsqlUid.IsUnknown() { - resolvedPsqlUid, err := r.client.ResolveRefSvcParamValue(ctx, 90, plan.PsqlUid.ValueString()) - if err != nil { - resp.Diagnostics.AddWarning("Failed to resolve psql_uid", err.Error()) - } else if resolvedPsqlUid != "" && resolvedPsqlUid != plan.PsqlUid.ValueString() { - plan.PsqlUid = types.StringValue(resolvedPsqlUid) - planChanged = true - } - } - if planChanged { - resp.Diagnostics.Append(resp.Plan.Set(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 задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") @@ -135,23 +133,32 @@ func (r *GiteaResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanR } } if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { - if plan.Domain.ValueString() != state.Domain.ValueString() { + // 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() { - if plan.PsqlUid.ValueString() != state.PsqlUid.ValueString() { + // 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() || config.PsqlUid.IsUnknown() { + 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 @@ -175,9 +182,16 @@ func (r *GiteaResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanR 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.PlanExistingResourceDiagnosticsWithParams(ctx, r.client, 99, config.ResourceName.ValueString(), adoptExistingOnCreate, params)...) + 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) { @@ -190,6 +204,24 @@ func (r *GiteaResource) Create(ctx context.Context, req resource.CreateRequest, 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 { @@ -202,7 +234,12 @@ func (r *GiteaResource) Create(ctx context.Context, req resource.CreateRequest, } resourceName := data.ResourceName.ValueString() - resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, 99, resourceName, data.AdoptExistingOnCreate.ValueBool())...) + 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)...) params := map[int]string{ 232: resources_core.FormatInt64(data.ResourceCPU), @@ -211,12 +248,17 @@ func (r *GiteaResource) Create(ctx context.Context, req resource.CreateRequest, 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()) @@ -235,6 +277,8 @@ func (r *GiteaResource) Create(ctx context.Context, req resource.CreateRequest, {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"}, @@ -253,6 +297,16 @@ func (r *GiteaResource) Create(ctx context.Context, req resource.CreateRequest, 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)...) } @@ -289,6 +343,8 @@ func (r *GiteaResource) Read(ctx context.Context, req resource.ReadRequest, resp {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"}, @@ -307,6 +363,16 @@ func (r *GiteaResource) Read(ctx context.Context, req resource.ReadRequest, resp 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)...) } @@ -366,6 +432,24 @@ func (r *GiteaResource) Update(ctx context.Context, req resource.UpdateRequest, } } } + 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 @@ -386,12 +470,17 @@ func (r *GiteaResource) Update(ctx context.Context, req resource.UpdateRequest, 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), } operationTimeout := "" if !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.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return @@ -409,6 +498,8 @@ func (r *GiteaResource) Update(ctx context.Context, req resource.UpdateRequest, {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"}, @@ -427,6 +518,15 @@ func (r *GiteaResource) Update(ctx context.Context, req resource.UpdateRequest, 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)...) } @@ -440,7 +540,6 @@ func (r *GiteaResource) Delete(ctx context.Context, req resource.DeleteRequest, if state.ID.IsNull() || state.ID.IsUnknown() { return } - deleteMode := "state_only" if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { deleteMode = "suspend" @@ -450,6 +549,9 @@ func (r *GiteaResource) Delete(ctx context.Context, req resource.DeleteRequest, 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 diff --git a/universal_rebuild/internal/resources_gen/registry.go b/universal_rebuild/internal/resources_gen/registry.go index 183b8c8..bb08f2e 100644 --- a/universal_rebuild/internal/resources_gen/registry.go +++ b/universal_rebuild/internal/resources_gen/registry.go @@ -47,30 +47,50 @@ func AllResources() []func() resource.Resource { NewVcVmV2Resource, NewVcVmV3Resource, NewVcexternalipResource, - NewVmpostgreResource, NewClickhouseDatabaseResource, NewClickhouseUserResource, + NewK8sSthutrvalClusterUserResource, NewKafkaTopicResource, NewKafkaUserResource, + NewMariadbDatabaseResource, + NewMariadbUserResource, NewMongodbUserResource, + NewNextcloudBackupResource, + NewPostgresBackupResource, + NewPostgresDatabaseResource, + NewPostgresUserResource, NewS3SubUserResource, + NewVcOrgUserResource, + NewVcVdcGroupVdcResource, + NewClickhouseReconcileResource, + NewDnsrecordReconcileResource, + NewDummyReconcileResource, NewDummyRedeployResource, + NewFlaskReconcileResource, + NewFlaskRedeployResource, NewHttpRedeployResource, NewK8sSthutrvalClusterReconcileResource, - NewK8sSthutrvalClusterUpdateSecretsResource, + NewLuceeReconcileResource, NewLuceeRedeployResource, NewLuceeRestartResource, + NewMariadbReconcileResource, + NewNextcloudReconcileResource, + NewNextcloudRecoveryResource, NewNextcloudRestartResource, + NewNodejsReconcileResource, NewNodejsRedeployResource, NewPgadminRedeployResource, + NewPostgresReconcileResource, NewPostgresRecoveryResource, NewPostgresRestartResource, + NewRedisReconcileResource, + NewS3ReconcileResource, + NewTemplateReconcileResource, NewVcNsxtReconcileResource, NewVcOrgReconcileResource, NewVcVdcReconcileResource, - NewVcVdcGroupAddVdcResource, NewVcVdcGroupReconcileResource, - NewVcVdcGroupRemoveVdcResource, + NewVcVmV3ReconcileResource, NewVcVmV3RedeployResource, } } diff --git a/universal_rebuild/resources_yaml/100_openwhisk.yaml b/universal_rebuild/resources_yaml/100_openwhisk.yaml new file mode 100644 index 0000000..ec17796 --- /dev/null +++ b/universal_rebuild/resources_yaml/100_openwhisk.yaml @@ -0,0 +1,48 @@ +name: openwhisk +service_id: 100 +service_display_name: Serverless Openwhisk (openwhisk_tenant) +service_short_name: openwhisk +service_man: Добавление и удаления пользователя для работы с Openwhisk +lifecycle: + suspend_on_destroy_default: false + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 41 + kind: instance + action: create + params: + - id: 64 + code: username + data_type: string + required: true + descr: Имя пользователя Openwhisk + - name: delete + id: 42 + kind: instance + action: delete + params: + - id: 65 + code: username + data_type: string + required: true + descr: Имя пользователя Openwhisk diff --git a/universal_rebuild/resources_yaml/110_dnszone.yaml b/universal_rebuild/resources_yaml/110_dnszone.yaml new file mode 100644 index 0000000..03d2540 --- /dev/null +++ b/universal_rebuild/resources_yaml/110_dnszone.yaml @@ -0,0 +1,94 @@ +name: dnszone +service_id: 110 +service_display_name: DNS зона +service_short_name: dnszone +service_man: '## ![image](https://img.icons8.com/?size=50&id=41400&format=png) Необходимо делегировать зону у регистратора на Авторитетные DNS (Сервера зон)

ns3.ngcloud.ru
``` bash
185.247.187.83
2a10:aa80:0:5::3
```

ns4.ngcloud.ru
``` bash
81.22.46.43
2a10:aa80:0:6::3
```' +lifecycle: + suspend_on_destroy_default: false + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 43 + kind: instance + action: create + params: + - id: 66 + code: zoneEmail + data_type: string + required: true + descr: Контактный e-mail + - id: 67 + code: zoneName + data_type: string + required: true + regex: ^([a-z0-9-]+\.)+[a-z0-9-]+$ + descr: Имя зоны + man: Имя зоны + - id: 68 + code: zoneKind + data_type: string + required: true + default: NATIVE + value_list: + - NATIVE + - SLAVE + descr: Тип зоны + - id: 69 + code: zoneMastersForSlave + data_type: string + required: false + descr: Использовать если выбран тип SLAVE. IP адреса в формате через пробел для настройки Master-серверов + - id: 70 + code: zoneTimeToRefresh + data_type: string + required: true + default: "86400" + descr: Время ожидания ответа вторичного DNS перед запросом SOA-записи с первичных серверов + - id: 71 + code: zoneTimeToRetry + data_type: string + required: true + default: "7200" + descr: Время в течение которого сервер повторно попробует синхронизировать информацию с первичных DNS-серверов + - id: 72 + code: zoneTimeToExpire + data_type: string + required: true + default: "3600000" + descr: Время в течение которого вторичный DNS будет пытаться завершить синхронизацию зоны с первичным + - id: 73 + code: zoneTimeToLive + data_type: string + required: true + default: "86400" + descr: Время, в течение которого информация будет кешироваться другими DNS-серверами + - id: 284 + code: resourceRealm + data_type: string + required: true + func: getAvailableResourceRealms + descr: Платформа для развертывания + - name: delete + id: 44 + kind: instance + action: delete + params: [] diff --git a/universal_rebuild/resources_yaml/111_dnsrecord.yaml b/universal_rebuild/resources_yaml/111_dnsrecord.yaml new file mode 100644 index 0000000..d87a3c1 --- /dev/null +++ b/universal_rebuild/resources_yaml/111_dnsrecord.yaml @@ -0,0 +1,105 @@ +name: dnsrecord +service_id: 111 +service_display_name: DNS запись +service_short_name: dnsrecord +service_man: Необходима DNS зона, в которой будет создана DNS запись.
Выберите
* Зону в которой будет создана запись
* Тип записи
* Имя записи
* Значение записи +lifecycle: + suspend_on_destroy_default: false + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 45 + kind: instance + action: create + params: + - id: 75 + code: zoneUid + data_type: uuid + required: true + ref_svc_id: 109 + descr: UUID экземпляра услуги DNS-зоны + man: UUID экземпляра услуги DNS-зоны + sort: 10 + - id: 76 + code: recordType + data_type: string + required: true + value_list: + - A + - AAAA + - CNAME + - MX + - PTR + - SOA + - SRV + - TXT + - LUA + - NS + - SSHFP + descr: Тип DNS-записи + man: Тип DNS-записи + sort: 20 + - id: 77 + code: recordName + data_type: string + required: true + unique_scope: parent + descr: Имя записи. Имя зоны в конце проставляется автоматически + man: Имя записи. Имя зоны в конце проставляется автоматически + sort: 30 + - id: 78 + code: recordInput + data_type: string + required: true + descr: Значение. Необходима валидация, в зависимости от типа записи. Должно быть заменено на UUID-внешнийIP + man: Значение. Необходима валидация, в зависимости от типа записи. Должно быть заменено на UUID-внешнийIP + sort: 40 + is_modifiable: true + - id: 450 + code: recordTTL + data_type: integer > 0 + required: true + default: "3600" + maxvalue: 604800 + minvalue: 30 + descr: Time-to-Leave записи. Раз в какое время запись будет обновлена + sort: 50 + - name: delete + id: 46 + kind: instance + action: delete + params: [] + - name: modify + id: 90 + kind: instance + action: modify + params: + - id: 256 + code: recordInput + data_type: string + required: true + descr: Запись. Необходима валидация, в зависимости от типа записи. Должно быть заменено на UUID-внешнийIP + - name: reconcile + id: 408 + kind: action + action: reconcile + params: [] diff --git a/universal_rebuild/resources_yaml/112_tenant.yaml b/universal_rebuild/resources_yaml/112_tenant.yaml new file mode 100644 index 0000000..a7ae715 --- /dev/null +++ b/universal_rebuild/resources_yaml/112_tenant.yaml @@ -0,0 +1,57 @@ +name: tenant +service_id: 112 +service_display_name: Тенант в Grafana +service_short_name: tenant +lifecycle: + suspend_on_destroy_default: false + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 84 + kind: instance + action: create + params: + - id: 239 + code: grafanaOrgName + data_type: string + required: true + regex: ^WZ[\d]+$ + descr: WZ-Код клиента + sort: 10 + - id: 240 + code: resourceRealm + data_type: string + required: true + func: getAvailableResourceRealms + descr: Платформа для развертывания + sort: 20 + - name: delete + id: 86 + kind: instance + action: delete + params: [] + - name: modify + id: 87 + kind: instance + action: modify + man: Просто удаляет все текущие датасоурсы и ставит их заново
Полезно, когда нужно "ресетнуть" / "добавить" + params: [] diff --git a/universal_rebuild/resources_yaml/113_vc_complex.yaml b/universal_rebuild/resources_yaml/113_vc_complex.yaml new file mode 100644 index 0000000..038f922 --- /dev/null +++ b/universal_rebuild/resources_yaml/113_vc_complex.yaml @@ -0,0 +1,155 @@ +name: vc_complex +service_id: 113 +service_display_name: Быстрый старт +service_short_name: vcComplex +service_man: '# Создает окружение для ВМ.

## Создает сервисы:
**Организация**
**Виртуальный датацентр**
**Каталог**
**Сетевой шлюз периметра**

' +lifecycle: + suspend_on_destroy_default: false + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 107 + kind: instance + action: create + params: + - id: 332 + code: resourceRealm + data_type: string + required: true + func: getAvailableResourceRealms + descr: Платформа для развертывания + man: Наименование ресурсной платформы + sort: 1 + - id: 334 + code: vappName + data_type: string + required: true + regex: ^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$ + descr: Имя vApp + sort: 10 + - id: 358 + code: vdcProviderGateway + data_type: string + required: true + default: sandbox-v1cl1-pvdc + descr: Взять из свойств ресурсной платформы.
Путь .vcd.providerVdc - взять KEY

providerGateway Cloud Director. Влияет ресурсы будут предоставлены. Является полем (List<String>) в свойствах рес. платформы ЛК, но сам параметр передаётся только как один String + sort: 200 + - id: 359 + code: virtualServicesCount + required: true + default: "1" + maxvalue: 4 + minvalue: 1 + descr: Кол-во виртуальных сервисов, которые выделяются на AVI. Во избежании коллапса пока выделяется до 4 + sort: 110 + - id: 360 + code: ipSpaceName + data_type: string + required: false + default: internet-ipv4-v1 + descr: Имя ip Space для внешнего IP. Целесообразнее брать самый дешёвый, так как это vip для исходящего трафика. Необходимо задавать если включен needExternalAddressSNAT + sort: 130 + - id: 399 + code: storageConfig + data_type: json + required: true + default: '[{"name": "", "size": 1024}]' + descr: Описывается как массив объектов, где описывается имя политики и её размер + sort: 210 + - id: 400 + code: vdcNetworkPool + data_type: string + required: true + default: nsxt-sandbox-geneve-np + sort: 120 + - id: 401 + code: cpuGuaranteed + data_type: integer > 0 + required: true + default: "20" + value_list: + - "20" + - "50" + - "80" + sort: 20 + - id: 402 + code: memGuaranteed + data_type: integer > 0 + required: true + default: "20" + value_list: + - "20" + - "50" + - "80" + sort: 30 + - id: 403 + code: segroupName + data_type: string + required: false + default: SEGROUP-SANDBOX-CL1-SHARED-01 + descr: Наименование segroup, в котором будут выделяться пулы на AVI + sort: 100 + - id: 404 + code: needExternalAddressSNAT + data_type: boolean + required: true + default: "true" + value_list: + - "false" + - "true" + descr: Выделяет внешний IP на Edge. Используется для SNAT всего трафика + sort: 40 + - id: 405 + code: needEnableAVI + data_type: boolean + required: true + default: "false" + value_list: + - "false" + - "true" + descr: Включает AVI в Cloud Director + sort: 50 + - id: 566 + code: cpuAllocated + data_type: string + required: true + default: "1" + minvalue: 1 + descr: шт + sort: 21 + - id: 567 + code: memAllocated + data_type: integer > 0 + required: true + default: "10" + minvalue: 1 + descr: gb + sort: 31 + - id: 568 + code: vIPCount + data_type: integer > 0 + required: true + default: "0" + maxvalue: 20 + minvalue: 0 + descr: ш + sort: 131 diff --git a/universal_rebuild/resources_yaml/114_gitea_complex.yaml b/universal_rebuild/resources_yaml/114_gitea_complex.yaml new file mode 100644 index 0000000..b18075e --- /dev/null +++ b/universal_rebuild/resources_yaml/114_gitea_complex.yaml @@ -0,0 +1,28 @@ +name: gitea_complex +service_id: 114 +service_display_name: Комплексная услуга по созданию gitea +service_short_name: GiteaComplex +service_man: Комплексная услуга из ["gittea", "postgres"] +lifecycle: + suspend_on_destroy_default: false + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: [] diff --git a/universal_rebuild/resources_yaml/115_mariadb.yaml b/universal_rebuild/resources_yaml/115_mariadb.yaml new file mode 100644 index 0000000..60d7492 --- /dev/null +++ b/universal_rebuild/resources_yaml/115_mariadb.yaml @@ -0,0 +1,203 @@ +name: mariadb +service_id: 115 +service_display_name: Mariadb +service_short_name: mariadb +service_man: 'Mariadb кластер. Ресурсная платформа: k8s. Автоматизация: k8s operator.
Автоматическое резервное копирование, ручное восстановление.' +lifecycle: + suspend_on_destroy_default: true + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 138 + kind: instance + action: create + params: + - id: 877 + code: startupConfiguration + data_type: map-fixed + required: true + sort: 10 + - id: 878 + code: clusterConfiguration + data_type: map-fixed + required: true + sort: 20 + is_modifiable: true + - id: 879 + code: accessConfiguration + data_type: map-fixed + required: true + sort: 30 + is_modifiable: true + - id: 880 + code: mariadbConfiguration + data_type: map-fixed + required: true + sort: 40 + is_modifiable: true + - id: 881 + code: backupConfiguration + data_type: map-fixed + required: true + sort: 60 + is_modifiable: true + - id: 882 + code: autoscaleConfiguration + data_type: map-fixed + required: true + sort: 70 + is_modifiable: true + - name: create_database + id: 242 + kind: subresource + action: create + subresource: database + params: + - id: 734 + code: dbName + data_type: string + required: true + default: db- + regex: ^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$ + descr: 'Пример: `db01`' + - name: create_user + id: 243 + kind: subresource + action: create + subresource: user + params: + - id: 735 + code: username + data_type: string + required: true + default: user + regex: ^(?![0-9_])[a-zA-Z_][a-zA-Z0-9_]{0,62}$ + maxlength: 62 + minlength: 2 + descr: 'Пример: `myusername`' + sort: 10 + - id: 736 + code: password + data_type: string + required: true + maxlength: 62 + minlength: 8 + descr: Секрет будет записан в хранилище секретов и не будет отображаться публично + sort: 20 + is_sensitive: true + - id: 737 + code: dbName + data_type: array-map-fixed + required: true + default: '[{}]' + descr: Необходимо указать массив существующих баз + man: Если базы не существует - при выполнении операции вернётся ошибка + sort: 30 + - id: 738 + code: accessHosts + data_type: string + required: true + default: '%' + descr: 'Пример: `%`' + sort: 40 + - name: delete + id: 139 + kind: instance + action: delete + params: + - id: 456 + code: deleteS3Bucket + data_type: boolean + required: true + default: "true" + value_list: + - "false" + - "true" + descr: Также удалить технический S3 Bucket с бэкапами? + sort: 10 + - name: delete_database + id: 248 + kind: subresource + action: delete + subresource: database + params: + - id: 745 + code: dbName + data_type: string + required: true + descr: ВСЕ ДАННЫЕ ВНУТРИ БД БУДУТ УДАЛЕНЫ + - name: delete_user + id: 247 + kind: subresource + action: delete + subresource: user + params: + - id: 744 + code: username + data_type: string + required: true + descr: Имя пользователя, созданное через операцию `create_user` + - name: modify + id: 142 + kind: instance + action: modify + params: + - id: 883 + code: clusterConfiguration + data_type: map-fixed + required: true + sort: 10 + - id: 884 + code: accessConfiguration + data_type: map-fixed + required: true + sort: 20 + - id: 885 + code: mariadbConfiguration + data_type: map-fixed + required: true + sort: 30 + - id: 886 + code: backupConfiguration + data_type: map-fixed + required: true + sort: 40 + - id: 887 + code: autoscaleConfiguration + data_type: map-fixed + required: true + sort: 50 + - name: reconcile + id: 287 + kind: action + action: reconcile + params: [] + - name: resume + id: 141 + kind: instance + action: resume + params: [] + - name: suspend + id: 140 + kind: instance + action: suspend + params: [] diff --git a/universal_rebuild/resources_yaml/116_kafka.yaml b/universal_rebuild/resources_yaml/116_kafka.yaml new file mode 100644 index 0000000..d2f3408 --- /dev/null +++ b/universal_rebuild/resources_yaml/116_kafka.yaml @@ -0,0 +1,291 @@ +name: kafka +service_id: 116 +service_display_name: ApacheKafka +service_short_name: kafka +service_man: "# Инструкция по развертыванию Apache Kafka через платформу

---
## 1. Общая информация
Брокер сообщений Apache Kafka — это отказоустойчивая распределённая платформа для обмена сообщениями и потоковой обработки данных в реальном времени.
Позволяет надёжно передавать события между сервисами и системами, интегрироваться с внешними решениями и использовать как внутренние, так и защищённые TLS-подключения. Развёртывание полностью автоматизировано платформой.

## Доступные операции
- **create** — Создание кластера
- **delete** — Удаление кластера
- **suspend** — Остановка кластера
- **resume** — Запуск кластера

- **create_topic** — Создание топика
- **modify_topic** — Модификация топика
- **delete_topic** — Удаление топика

- **create_user** — Создание пользователя
- **modify_user** — Модификация пользователя
- **delete_user** — Удаление пользователя


---
## 2. Параметры развертывания кластера
Ниже перечислены параметры, которые необходимо указать при создании ресурса Kafka:

* **Платформа для развертывания:**
Платформа для развертывания (среда, в которой будет развернут кластер). Пример: `\tk8s-3.ext.nubes.ru`.

* **Количество узлов в сети:**
Количество узлов (брокеров) в кластере Kafka

* **Квота (megabyte) памяти пода:**
Квота памяти пода в мегабайтах. Пример: `2048`

* **Квота (millicore ) ядра пода:**
Квота CPU пода в миликорах (1000 mCPU = 1 vCPU). Пример: `1000`

* **Размер (gigabyte) диска для базы данных:**
Размер диска для хранения данных в гигабайтах. Пример: `20`

* **Выделяет внешний IP для доступа к master:**
Флаг, определяющий, нужно ли выделять внешний IP для доступа к master-брокеру. Пример: `true` или `false`

* **Имя ipSpace для публикации внешнего IP**
Если включен предыдущий параметр, то поле необходимо заполнить. Пример: `internet-no-antiddos-v1`

---
## 4. Выходные параметры
После успешного развертывания ресурса Kafka платформа возвращает ряд параметров.
Ключевым из них является:

**internalConnect**
Это внутренний эндпоинт, который используется для подключения к кластеру Kafka из других сервисов, развернутых внутри той же сети.
Эндпоинт предоставляет доступ к брокерам Kafka по внутренним портам, указанным в конфигурации слушателей.

**externalConnect**
Это внешний эндпоинт (fqdn), который используется для подключения к кластеру Kafka. Для подключения используеться 9093 порт.

Для подключения используются следующие порты:

* 9092 — внутренний нешифрованный доступ (plain).
* 9093 — внутренний и внешний доступ по TLS, с обязательной TLS-аутентификацией.

---
## 5. Дополнительная информация
Для создания топиков, пользователей и управления ими, а также для интеграции с кластерами Kafka используйте соответствующие сервисы и раздел "Облачные сервисы".
---" +lifecycle: + suspend_on_destroy_default: true + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 148 + kind: instance + action: create + params: + - id: 461 + code: resourceInstances + data_type: integer > 0 + required: true + value_list: + - "1" + - "2" + - "3" + - "4" + - "5" + minvalue: 1 + descr: 'Пример: `2`' + sort: 20 + is_modifiable: true + - id: 462 + code: resourceMemory + data_type: integer > 0 + required: true + minvalue: 1000 + descr: 'Пример: `2048`' + sort: 30 + is_modifiable: true + - id: 463 + code: resourceCPU + data_type: integer > 0 + required: true + minvalue: 1000 + descr: 'Пример: `2000`' + sort: 40 + is_modifiable: true + - id: 464 + code: resourceDisk + data_type: integer > 0 + required: true + descr: 'Пример: `20`' + sort: 50 + is_modifiable: true + - id: 465 + code: needExternalAddressMaster + data_type: boolean + required: true + default: "false" + value_list: + - "false" + - "true" + descr: 'Флаг, определяющий, нужно ли выделять внешний IP для доступа к master-брокеру. Пример: `true` или `false`' + sort: 60 + is_modifiable: true + - id: 466 + code: ipSpaceNameMaster + data_type: string + required: false + descr: 'Если включен предыдущий параметр, то поле необходимо заполнить. Пример: `internet-no-antiddos-v1`' + sort: 70 + is_modifiable: true + - id: 469 + code: resourceRealm + data_type: string + required: true + func: getAvailableResourceRealms + descr: Выбрать платформу из списка + sort: 5 + - name: create_topic + id: 165 + kind: subresource + action: create + subresource: topic + man: '# Инструкция по управлению топиками Apache Kafka через платформу

## 1. Общая информация

---

Топик — это основная сущность в Apache Kafka, предназначенная для приёма сообщений от продюсеров и передачи их консюмерам.
Платформа позволяет создавать, удалять и изменять параметры топиков без прямого доступа к брокерам, обеспечивая удобное управление через интерфейс или API.

---

## Доступные операции

- **create_topic** — Создание топика
- **modify_topic** — Модификация топика
- **delete_topic** — Удаление топика

---

## 2. Параметры создания топика

Ниже перечислены параметры, которые необходимо указать при создании нового топика:

* **Имя топика**
Название топика.
Должно быть уникальным в пределах кластера.
Пример: `orders-events`.

* **Партиции**
Количество партиций топика.
Рекомендуется выбирать с учётом уровня параллелизма потребителей и предполагаемой нагрузки.
Пример: `6`. Здесь важно рассказать о том как происходит расчет оптимального количества
Обратите внимание: уменьшить количество партиций после создания невозможно, поэтому лучше закладывать небольшой запас на будущий рост нагрузки.

* **Реплики**
Количество реплик для каждой партиции.
Обычно равно количеству доступных брокеров или меньше.
Пример: `3`.


---

## 4. Параметры модификации топика

Для уже существующих топиков можно изменять параметры. Все параметры являются опциональными:

* **Имя топика**
Название топика.
Пример: `orders-events`.

* **Партиции**
Новое количество партиций.
Пример: `12`.

* **Реплики**
Новое количество реплик.
Пример: `2`.

---

## 5. Рекомендации

* Планируйте количество партиций с запасом — уменьшение их числа невозможно.
* Количество реплик должно соответствовать требуемому уровню отказоустойчивости, но не превышать количество брокеров.
* Изменение параметров топика в рабочем окружении может временно влиять на производительность. Рекомендуется проводить крупные изменения в периоды минимальной нагрузки.

---' + params: + - id: 523 + code: nameTopic + data_type: string + required: true + descr: 'Пример: `orders-events`' + sort: 10 + - id: 524 + code: partitions + data_type: integer > 0 + required: true + descr: Число партиций можно увеличить, но уменьшить их нельзя. + - id: 525 + code: replicas + data_type: integer > 0 + required: true + descr: Количество реплик не может привышать количество реплик брокера + sort: 20 + - name: create_user + id: 168 + kind: subresource + action: create + subresource: user + man: '# Инструкция по управлению пользователями Apache Kafka.

---

## 1. Общая информация

Пользователи в Apache Kafka используются для аутентификации и авторизации при подключении к кластеру.
Платформа позволяет создавать, удалять и изменять пользователей без прямого доступа к брокерам, а также настраивать права доступа к топикам и группам.
Это обеспечивает централизованное и безопасное управление доступом к ресурсам Kafka.

---

## Доступные операции

- **create_user** — Создание пользователя
- **modify_user** — Модификация пользователя
- **delete_user** — Удаление пользователя

---

## 2. Параметры создания пользователя

Ниже перечислены параметры, которые необходимо указать при создании нового пользователя:

* **Имя пользователя**
Имя создаваемого пользователя. Используется для подключения к Kafka и настройки прав.
Пример: `username-admin`

* **Топик**
Название топика, к которому будут назначены права для данного пользователя.
Пример: `orders-events`.
Использовать маску нельзя (`orders*`), только полное название или `*`.

* **Операции**
Список разрешённых операций на указанном топике.
Пример: `Describe, Read, Write`

* **Группа**
Имя группы, которой разрешён доступ. Используется при настройке консюмер-групп.
Пример: `order-consumers`.

* **Разрешённые хосты**
Список хостов или подсетей, с которых разрешено подключение пользователя.
Пример: `192.168.1.0/24` или `*`.

---

## 3. Параметры модификации пользователя

Ниже перечислены параметры, которые необходимо указать при модификации пользователя:

* **Имя пользователя**
Имя создаваемого пользователя. Используется для подключения к Kafka и настройки прав.
Пример: `username-admin`

* **Топик**
Название топика, к которому будут назначены права для данного пользователя.
Пример: `orders-events`.
Использовать маску нельзя (`orders*`), только полное название или `*`.

* **Операции**
Список разрешённых операций на указанном топике.
Пример: `Describe, Read, Write`

* **Группа**
Имя группы, которой разрешён доступ. Используется при настройке консюмер-групп.
Пример: `order-consumers`.

* **Разрешённые хосты**
Список хостов или подсетей, с которых разрешено подключение пользователя.
Пример: `192.168.1.0/24` или `*`.

---

## 4. Параметры удалении пользователя

Ниже перечислены параметры, которые необходимо указать при удалении пользователя:

* **Имя пользователя**
Имя удаляемого пользователя.
Пример: `username-admin`

## 5. Выходные параметры

После успешного создания пользователя платформа возвращает параметры, необходимые для подключения:

* **ca.crt**
Корневой сертификат (Certificate Authority), используемый для проверки подлинности сервера при TLS-подключении.
Файл содержит публичный ключ удостоверяющего центра, которому доверяют брокеры и клиенты. Выдаеться в формате base64 для получения файла сертификата использовать команду
```bash
echo ''<содержание поля>'' | base64 -d > <название>.crt
```

* **ca.p12**
PKCS#12-хранилище с корневым сертификатом (Certificate Authority), используемое для проверки подлинности сервера при TLS-подключении.
Выдаеться в формате base64 для получения файла сертификата использовать команду
```bash
echo ''<содержание поля>'' | base64 -d > <название>.p12
```

* **ca.password**
Пароль для доступа к хранилищу `ca.p12`. Выдаеться в чистом виде. Раскодировать не нужно.

* **user.crt**
Клиентский сертификат, подтверждающий личность пользователя при подключении к Kafka.
Используется совместно с приватным ключом `user.key` для аутентификации.
Выдаеться в формате base64 для получения файла сертификата использовать команду
```bash
echo ''<содержание поля>'' | base64 -d > <название>.crt
```

* **user.key**
Приватный ключ пользователя, применяемый для установления защищённого TLS-соединения и подтверждения подлинности.
Выдаеться в формате base64 для получения файла сертификата использовать команду
```bash
echo ''<содержание поля>'' | base64 -d > <название>.key
```

* **user.p12**
PKCS#12-хранилище с клиентским сертификатом и приватным ключом, предназначенное для аутентификации пользователя в Kafka.
Выдаеться в формате base64 для получения файла сертификата использовать команду
```bash
echo "<содержание поля>" | base64 -d > <название>.p12
```

* **user.password**
Пароль для доступа к хранилищу `user.p12`. Выдаеться в чистом виде. Раскодировать не нужно.

Эти параметры используются для настройки TLS-подключения.

---

## 6. Пример конфигурации клиента внутри кластера

Пример пода:

```yaml
apiVersion: v1
kind: Pod
metadata:
name: kafka-4
spec:
volumes:
- name: work
emptyDir: {}
containers:
- name: kcat
image: quay.io/strimzi/kafka:latest-kafka-4.0.0
command: ["sleep", "5600"]
volumeMounts:
- name: work
mountPath: /work
restartPolicy: Never
```

Создаём под и сохраняем сертификаты:

```bash
kubectl cp user.p12 kafka-4:/work/
kubectl cp ca.p12 kafka-4:/work/
```

Создаём файлы конфигурации:

```bash
cat > /work/user-read.properties <<''EOF''
security.protocol=SSL
ssl.keystore.type=PKCS12
ssl.keystore.location=/work/user.p12
ssl.keystore.password=<user.password>
ssl.truststore.type=PKCS12
ssl.truststore.location=/work/ca.p12
ssl.truststore.password=<ca.password>
EOF
```

```bash
cat > /work/user-write.properties <<''EOF''
security.protocol=SSL
ssl.keystore.type=PKCS12
ssl.keystore.location=/work/user-write.p12
ssl.keystore.password=<user.password>
ssl.truststore.type=PKCS12
ssl.truststore.location=/work/ca.p12
ssl.truststore.password=<ca.password>
EOF
```

Листинг доступных топиков:

```bash
/opt/kafka/bin/kafka-topics.sh \
--bootstrap-server kafkak8s-bootstrap-external:9093 \
--command-config /work/user-read.properties \
--list
```

Запись сообщений:

```bash
/opt/kafka/bin/kafka-console-producer.sh \
--bootstrap-server <адрес сервис брокера>:9093 \
--topic <topic> \
--producer.config /work/user-write.properties
```

Чтение сообщений:

```bash
/opt/kafka/bin/kafka-console-consumer.sh \
--bootstrap-server <адрес сервис брокера>:9093 \
--topic <topic> \
--from-beginning \
--group <group> \
--consumer.config /work/user-read.properties \
--max-messages 50
```

---

## 7. Пример внешнего подключения с использованием **kcat**

**Листинг топиков**

```bash
kcat -b <externalConnect.bootstrap.fqdn>:9093 \
-L \
-X security.protocol=SSL \
-X ssl.ca.location=./ca.crt \
-X ssl.certificate.location=./user-read.crt \
-X ssl.key.location=./user-read.key \
-X ssl.endpoint.identification.algorithm=none
```

**Запись сообщения**

```bash
echo "hello from kcat" | kcat -P \
-b <externalConnect.bootstrap.fqdn>:9093 \
-t write-test \
-X security.protocol=SSL \
-X ssl.ca.location=./ca.crt \
-X ssl.certificate.location=./user-write.crt \
-X ssl.key.location=./user-write.key \
-X ssl.endpoint.identification.algorithm=none
```

**Чтение сообщений**

```bash
kcat -C \
-b <externalConnect.bootstrap.fqdn>:9093 \
-t write-test \
-o beginning \
-q \
-X security.protocol=SSL \
-X ssl.ca.location=./ca.crt \
-X ssl.certificate.location=./user-read.crt \
-X ssl.key.location=./user-read.key \
-X ssl.endpoint.identification.algorithm=none
```

---


## 9. Рекомендации

* Минимизируйте набор разрешённых операций для повышения безопасности.
* Используйте отдельные учётные записи для продюсеров, консюмеров и системных сервисов.
* Ограничивайте доступ по IP или подсетям с помощью параметра **Разрешённые хосты**.
* После изменения прав или групп пользователя рекомендуется проверить подключение и доступ к топикам.

---' + params: + - id: 530 + code: username + data_type: string + required: true + descr: 'Пример: `username-admin`' + - id: 531 + code: nameTopic + data_type: string + required: true + descr: 'Пример: `orders-events`. Использовать маску нельзя(`orders*`), только полное название или `*`' + - id: 532 + code: operations + data_type: string + required: true + descr: 'Пример: `Create, Describe, Read, Write`' + - id: 533 + code: group + data_type: string + required: true + descr: 'Пример: `order-consumers` или `*`' + - id: 534 + code: accessHosts + data_type: string + required: true + descr: 'Пример: `192.168.1.0/24` или `*`' + - name: delete + id: 149 + kind: instance + action: delete + params: [] + - name: delete_topic + id: 167 + kind: subresource + action: delete + subresource: topic + params: + - id: 529 + code: nameTopic + data_type: string + required: true + descr: Необходимо выбрать топик, который необходимо удалить + - name: delete_user + id: 170 + kind: subresource + action: delete + subresource: user + man: '# Инструкция по управлению пользователями Apache Kafka.

---

## 1. Общая информация

Пользователи в Apache Kafka используются для аутентификации и авторизации при подключении к кластеру.
Платформа позволяет создавать, удалять и изменять пользователей без прямого доступа к брокерам, а также настраивать права доступа к топикам и группам.
Это обеспечивает централизованное и безопасное управление доступом к ресурсам Kafka.

---

## Доступные операции

- **create_user** — Создание пользователя
- **modify_user** — Модификация пользователя
- **delete_user** — Удаление пользователя

---

## 2. Параметры создания пользователя

Ниже перечислены параметры, которые необходимо указать при создании нового пользователя:

* **Имя пользователя**
Имя создаваемого пользователя. Используется для подключения к Kafka и настройки прав.
Пример: `username-admin`

* **Топик**
Название топика, к которому будут назначены права для данного пользователя.
Пример: `orders-events`.
Использовать маску нельзя (`orders*`), только полное название или `*`.

* **Операции**
Список разрешённых операций на указанном топике.
Пример: `Describe, Read, Write`

* **Группа**
Имя группы, которой разрешён доступ. Используется при настройке консюмер-групп.
Пример: `order-consumers`.

* **Разрешённые хосты**
Список хостов или подсетей, с которых разрешено подключение пользователя.
Пример: `192.168.1.0/24` или `*`.

---

## 3. Параметры модификации пользователя

Ниже перечислены параметры, которые необходимо указать при модификации пользователя:

* **Имя пользователя**
Имя создаваемого пользователя. Используется для подключения к Kafka и настройки прав.
Пример: `username-admin`

* **Топик**
Название топика, к которому будут назначены права для данного пользователя.
Пример: `orders-events`.
Использовать маску нельзя (`orders*`), только полное название или `*`.

* **Операции**
Список разрешённых операций на указанном топике.
Пример: `Describe, Read, Write`

* **Группа**
Имя группы, которой разрешён доступ. Используется при настройке консюмер-групп.
Пример: `order-consumers`.

* **Разрешённые хосты**
Список хостов или подсетей, с которых разрешено подключение пользователя.
Пример: `192.168.1.0/24` или `*`.

---

## 4. Параметры удалении пользователя

Ниже перечислены параметры, которые необходимо указать при удалении пользователя:

* **Имя пользователя**
Имя удаляемого пользователя.
Пример: `username-admin`

## 5. Выходные параметры

После успешного создания пользователя платформа возвращает параметры, необходимые для подключения:

* **ca.crt**
Корневой сертификат (Certificate Authority), используемый для проверки подлинности сервера при TLS-подключении.
Файл содержит публичный ключ удостоверяющего центра, которому доверяют брокеры и клиенты. Выдаеться в формате base64 для получения файла сертификата использовать команду
```bash
echo ''<содержание поля>'' | base64 -d > <название>.crt
```

* **ca.p12**
PKCS#12-хранилище с корневым сертификатом (Certificate Authority), используемое для проверки подлинности сервера при TLS-подключении.
Выдаеться в формате base64 для получения файла сертификата использовать команду
```bash
echo ''<содержание поля>'' | base64 -d > <название>.p12
```

* **ca.password**
Пароль для доступа к хранилищу `ca.p12`. Выдаеться в чистом виде. Раскодировать не нужно.

* **user.crt**
Клиентский сертификат, подтверждающий личность пользователя при подключении к Kafka.
Используется совместно с приватным ключом `user.key` для аутентификации.
Выдаеться в формате base64 для получения файла сертификата использовать команду
```bash
echo ''<содержание поля>'' | base64 -d > <название>.crt
```

* **user.key**
Приватный ключ пользователя, применяемый для установления защищённого TLS-соединения и подтверждения подлинности.
Выдаеться в формате base64 для получения файла сертификата использовать команду
```bash
echo ''<содержание поля>'' | base64 -d > <название>.key
```

* **user.p12**
PKCS#12-хранилище с клиентским сертификатом и приватным ключом, предназначенное для аутентификации пользователя в Kafka.
Выдаеться в формате base64 для получения файла сертификата использовать команду
```bash
echo "<содержание поля>" | base64 -d > <название>.p12
```

* **user.password**
Пароль для доступа к хранилищу `user.p12`. Выдаеться в чистом виде. Раскодировать не нужно.

Эти параметры используются для настройки TLS-подключения.

---

## 6. Пример конфигурации клиента внутри кластера

Пример пода:

```yaml
apiVersion: v1
kind: Pod
metadata:
name: kafka-4
spec:
volumes:
- name: work
emptyDir: {}
containers:
- name: kcat
image: quay.io/strimzi/kafka:latest-kafka-4.0.0
command: ["sleep", "5600"]
volumeMounts:
- name: work
mountPath: /work
restartPolicy: Never
```

Создаём под и сохраняем сертификаты:

```bash
kubectl cp user.p12 kafka-4:/work/
kubectl cp ca.p12 kafka-4:/work/
```

Создаём файлы конфигурации:

```bash
cat > /work/user-read.properties <<''EOF''
security.protocol=SSL
ssl.keystore.type=PKCS12
ssl.keystore.location=/work/user.p12
ssl.keystore.password=<user.password>
ssl.truststore.type=PKCS12
ssl.truststore.location=/work/ca.p12
ssl.truststore.password=<ca.password>
EOF
```

```bash
cat > /work/user-write.properties <<''EOF''
security.protocol=SSL
ssl.keystore.type=PKCS12
ssl.keystore.location=/work/user-write.p12
ssl.keystore.password=<user.password>
ssl.truststore.type=PKCS12
ssl.truststore.location=/work/ca.p12
ssl.truststore.password=<ca.password>
EOF
```

Листинг доступных топиков:

```bash
/opt/kafka/bin/kafka-topics.sh \
--bootstrap-server kafkak8s-bootstrap-external:9093 \
--command-config /work/user-read.properties \
--list
```

Запись сообщений:

```bash
/opt/kafka/bin/kafka-console-producer.sh \
--bootstrap-server <адрес сервис брокера>:9093 \
--topic <topic> \
--producer.config /work/user-write.properties
```

Чтение сообщений:

```bash
/opt/kafka/bin/kafka-console-consumer.sh \
--bootstrap-server <адрес сервис брокера>:9093 \
--topic <topic> \
--from-beginning \
--group <group> \
--consumer.config /work/user-read.properties \
--max-messages 50
```

---

## 7. Пример внешнего подключения с использованием **kcat**

**Листинг топиков**

```bash
kcat -b <externalConnect.bootstrap.fqdn>:9093 \
-L \
-X security.protocol=SSL \
-X ssl.ca.location=./ca.crt \
-X ssl.certificate.location=./user-read.crt \
-X ssl.key.location=./user-read.key \
-X ssl.endpoint.identification.algorithm=none
```

**Запись сообщения**

```bash
echo "hello from kcat" | kcat -P \
-b <externalConnect.bootstrap.fqdn>:9093 \
-t write-test \
-X security.protocol=SSL \
-X ssl.ca.location=./ca.crt \
-X ssl.certificate.location=./user-write.crt \
-X ssl.key.location=./user-write.key \
-X ssl.endpoint.identification.algorithm=none
```

**Чтение сообщений**

```bash
kcat -C \
-b <externalConnect.bootstrap.fqdn>:9093 \
-t write-test \
-o beginning \
-q \
-X security.protocol=SSL \
-X ssl.ca.location=./ca.crt \
-X ssl.certificate.location=./user-read.crt \
-X ssl.key.location=./user-read.key \
-X ssl.endpoint.identification.algorithm=none
```

---


## 9. Рекомендации

* Минимизируйте набор разрешённых операций для повышения безопасности.
* Используйте отдельные учётные записи для продюсеров, консюмеров и системных сервисов.
* Ограничивайте доступ по IP или подсетям с помощью параметра **Разрешённые хосты**.
* После изменения прав или групп пользователя рекомендуется проверить подключение и доступ к топикам.

---' + params: + - id: 540 + code: username + data_type: string + required: true + descr: 'Пример: `username-admin`' + - name: modify + id: 162 + kind: instance + action: modify + params: + - id: 501 + code: resourceInstances + data_type: integer > 0 + required: false + value_list: + - "1" + - "2" + - "3" + - "4" + - "5" + descr: 'Пример: `2`' + - id: 502 + code: resourceMemory + data_type: integer > 0 + required: false + minvalue: 1000 + descr: 'Пример: `2048`' + - id: 503 + code: resourceCPU + required: false + minvalue: 1000 + descr: 'Пример: `2000`' + - id: 504 + code: resourceDisk + data_type: integer > 0 + required: false + descr: 'Пример: `20`' + - id: 505 + code: needExternalAddressMaster + data_type: boolean + required: false + default: "false" + value_list: + - "false" + - "true" + descr: 'Флаг, определяющий, нужно ли выделять внешний IP для доступа к master-брокеру. Пример: `true` или `false`' + - id: 506 + code: ipSpaceNameMaster + data_type: string + required: false + descr: 'Если включен предыдущий параметр, то поле необходимо заполнить. Пример: `internet-no-antiddos-v1`' + - name: modify_topic + id: 166 + kind: subresource + action: modify + subresource: topic + man: '# Инструкция по управлению топиками Apache Kafka через платформу

## 1. Общая информация

---

Топик — это основная сущность в Apache Kafka, предназначенная для приёма сообщений от продюсеров и передачи их консюмерам.
Платформа позволяет создавать, удалять и изменять параметры топиков без прямого доступа к брокерам, обеспечивая удобное управление через интерфейс или API.

---

## Доступные операции

- **create_topic** — Создание топика
- **modify_topic** — Модификация топика
- **delete_topic** — Удаление топика

---

## 2. Параметры создания топика

Ниже перечислены параметры, которые необходимо указать при создании нового топика:

* **Имя топика**
Название топика.
Должно быть уникальным в пределах кластера.
Пример: `orders-events`.

* **Партиции**
Количество партиций топика.
Рекомендуется выбирать с учётом уровня параллелизма потребителей и предполагаемой нагрузки.
Пример: `6`. Здесь важно рассказать о том как происходит расчет оптимального количества
Обратите внимание: уменьшить количество партиций после создания невозможно, поэтому лучше закладывать небольшой запас на будущий рост нагрузки.

* **Реплики**
Количество реплик для каждой партиции.
Обычно равно количеству доступных брокеров или меньше.
Пример: `3`.


---

## 4. Параметры модификации топика

Для уже существующих топиков можно изменять параметры. Все параметры являются опциональными:

* **Имя топика**
Название топика.
Пример: `orders-events`.

* **Партиции**
Новое количество партиций.
Пример: `12`.

* **Реплики**
Новое количество реплик.
Пример: `2`.

---

## 5. Рекомендации

* Планируйте количество партиций с запасом — уменьшение их числа невозможно.
* Количество реплик должно соответствовать требуемому уровню отказоустойчивости, но не превышать количество брокеров.
* Изменение параметров топика в рабочем окружении может временно влиять на производительность. Рекомендуется проводить крупные изменения в периоды минимальной нагрузки.

---' + params: + - id: 526 + code: nameTopic + data_type: string + required: true + descr: 'Пример: `orders-events`' + sort: 10 + - id: 527 + code: partitions + required: false + descr: Число партиций на кластер ≤ 100 × (число брокеров) × (фактор репликации). Пример `50` + sort: 30 + - id: 528 + code: replicas + required: false + descr: Количество партиций ≤ (число брокеров). Пример `2` + sort: 20 + - name: modify_user + id: 169 + kind: subresource + action: modify + subresource: user + man: '# Инструкция по управлению пользователями Apache Kafka.

---

## 1. Общая информация

Пользователи в Apache Kafka используются для аутентификации и авторизации при подключении к кластеру.
Платформа позволяет создавать, удалять и изменять пользователей без прямого доступа к брокерам, а также настраивать права доступа к топикам и группам.
Это обеспечивает централизованное и безопасное управление доступом к ресурсам Kafka.

---

## Доступные операции

- **create_user** — Создание пользователя
- **modify_user** — Модификация пользователя
- **delete_user** — Удаление пользователя

---

## 2. Параметры создания пользователя

Ниже перечислены параметры, которые необходимо указать при создании нового пользователя:

* **Имя пользователя**
Имя создаваемого пользователя. Используется для подключения к Kafka и настройки прав.
Пример: `username-admin`

* **Топик**
Название топика, к которому будут назначены права для данного пользователя.
Пример: `orders-events`.
Использовать маску нельзя (`orders*`), только полное название или `*`.

* **Операции**
Список разрешённых операций на указанном топике.
Пример: `Describe, Read, Write`

* **Группа**
Имя группы, которой разрешён доступ. Используется при настройке консюмер-групп.
Пример: `order-consumers`.

* **Разрешённые хосты**
Список хостов или подсетей, с которых разрешено подключение пользователя.
Пример: `192.168.1.0/24` или `*`.

---

## 3. Параметры модификации пользователя

Ниже перечислены параметры, которые необходимо указать при модификации пользователя:

* **Имя пользователя**
Имя создаваемого пользователя. Используется для подключения к Kafka и настройки прав.
Пример: `username-admin`

* **Топик**
Название топика, к которому будут назначены права для данного пользователя.
Пример: `orders-events`.
Использовать маску нельзя (`orders*`), только полное название или `*`.

* **Операции**
Список разрешённых операций на указанном топике.
Пример: `Describe, Read, Write`

* **Группа**
Имя группы, которой разрешён доступ. Используется при настройке консюмер-групп.
Пример: `order-consumers`.

* **Разрешённые хосты**
Список хостов или подсетей, с которых разрешено подключение пользователя.
Пример: `192.168.1.0/24` или `*`.

---

## 4. Параметры удалении пользователя

Ниже перечислены параметры, которые необходимо указать при удалении пользователя:

* **Имя пользователя**
Имя удаляемого пользователя.
Пример: `username-admin`

## 5. Выходные параметры

После успешного создания пользователя платформа возвращает параметры, необходимые для подключения:

* **ca.crt**
Корневой сертификат (Certificate Authority), используемый для проверки подлинности сервера при TLS-подключении.
Файл содержит публичный ключ удостоверяющего центра, которому доверяют брокеры и клиенты. Выдаеться в формате base64 для получения файла сертификата использовать команду
```bash
echo ''<содержание поля>'' | base64 -d > <название>.crt
```

* **ca.p12**
PKCS#12-хранилище с корневым сертификатом (Certificate Authority), используемое для проверки подлинности сервера при TLS-подключении.
Выдаеться в формате base64 для получения файла сертификата использовать команду
```bash
echo ''<содержание поля>'' | base64 -d > <название>.p12
```

* **ca.password**
Пароль для доступа к хранилищу `ca.p12`. Выдаеться в чистом виде. Раскодировать не нужно.

* **user.crt**
Клиентский сертификат, подтверждающий личность пользователя при подключении к Kafka.
Используется совместно с приватным ключом `user.key` для аутентификации.
Выдаеться в формате base64 для получения файла сертификата использовать команду
```bash
echo ''<содержание поля>'' | base64 -d > <название>.crt
```

* **user.key**
Приватный ключ пользователя, применяемый для установления защищённого TLS-соединения и подтверждения подлинности.
Выдаеться в формате base64 для получения файла сертификата использовать команду
```bash
echo ''<содержание поля>'' | base64 -d > <название>.key
```

* **user.p12**
PKCS#12-хранилище с клиентским сертификатом и приватным ключом, предназначенное для аутентификации пользователя в Kafka.
Выдаеться в формате base64 для получения файла сертификата использовать команду
```bash
echo "<содержание поля>" | base64 -d > <название>.p12
```

* **user.password**
Пароль для доступа к хранилищу `user.p12`. Выдаеться в чистом виде. Раскодировать не нужно.

Эти параметры используются для настройки TLS-подключения.

---

## 6. Пример конфигурации клиента внутри кластера

Пример пода:

```yaml
apiVersion: v1
kind: Pod
metadata:
name: kafka-4
spec:
volumes:
- name: work
emptyDir: {}
containers:
- name: kcat
image: quay.io/strimzi/kafka:latest-kafka-4.0.0
command: ["sleep", "5600"]
volumeMounts:
- name: work
mountPath: /work
restartPolicy: Never
```

Создаём под и сохраняем сертификаты:

```bash
kubectl cp user.p12 kafka-4:/work/
kubectl cp ca.p12 kafka-4:/work/
```

Создаём файлы конфигурации:

```bash
cat > /work/user-read.properties <<''EOF''
security.protocol=SSL
ssl.keystore.type=PKCS12
ssl.keystore.location=/work/user.p12
ssl.keystore.password=<user.password>
ssl.truststore.type=PKCS12
ssl.truststore.location=/work/ca.p12
ssl.truststore.password=<ca.password>
EOF
```

```bash
cat > /work/user-write.properties <<''EOF''
security.protocol=SSL
ssl.keystore.type=PKCS12
ssl.keystore.location=/work/user-write.p12
ssl.keystore.password=<user.password>
ssl.truststore.type=PKCS12
ssl.truststore.location=/work/ca.p12
ssl.truststore.password=<ca.password>
EOF
```

Листинг доступных топиков:

```bash
/opt/kafka/bin/kafka-topics.sh \
--bootstrap-server kafkak8s-bootstrap-external:9093 \
--command-config /work/user-read.properties \
--list
```

Запись сообщений:

```bash
/opt/kafka/bin/kafka-console-producer.sh \
--bootstrap-server <адрес сервис брокера>:9093 \
--topic <topic> \
--producer.config /work/user-write.properties
```

Чтение сообщений:

```bash
/opt/kafka/bin/kafka-console-consumer.sh \
--bootstrap-server <адрес сервис брокера>:9093 \
--topic <topic> \
--from-beginning \
--group <group> \
--consumer.config /work/user-read.properties \
--max-messages 50
```

---

## 7. Пример внешнего подключения с использованием **kcat**

**Листинг топиков**

```bash
kcat -b <externalConnect.bootstrap.fqdn>:9093 \
-L \
-X security.protocol=SSL \
-X ssl.ca.location=./ca.crt \
-X ssl.certificate.location=./user-read.crt \
-X ssl.key.location=./user-read.key \
-X ssl.endpoint.identification.algorithm=none
```

**Запись сообщения**

```bash
echo "hello from kcat" | kcat -P \
-b <externalConnect.bootstrap.fqdn>:9093 \
-t write-test \
-X security.protocol=SSL \
-X ssl.ca.location=./ca.crt \
-X ssl.certificate.location=./user-write.crt \
-X ssl.key.location=./user-write.key \
-X ssl.endpoint.identification.algorithm=none
```

**Чтение сообщений**

```bash
kcat -C \
-b <externalConnect.bootstrap.fqdn>:9093 \
-t write-test \
-o beginning \
-q \
-X security.protocol=SSL \
-X ssl.ca.location=./ca.crt \
-X ssl.certificate.location=./user-read.crt \
-X ssl.key.location=./user-read.key \
-X ssl.endpoint.identification.algorithm=none
```

---


## 9. Рекомендации

* Минимизируйте набор разрешённых операций для повышения безопасности.
* Используйте отдельные учётные записи для продюсеров, консюмеров и системных сервисов.
* Ограничивайте доступ по IP или подсетям с помощью параметра **Разрешённые хосты**.
* После изменения прав или групп пользователя рекомендуется проверить подключение и доступ к топикам.

---' + params: + - id: 535 + code: nameTopic + data_type: string + required: false + descr: 'Пример: `orders-events`. Использовать маску нельзя(`orders*`), только полное название или `*`' + - id: 536 + code: operations + data_type: string + required: false + descr: 'Пример: `Create, Describe, Read, Write`' + - id: 537 + code: group + data_type: string + required: false + descr: 'Пример: `order-consumers` или `*`' + - id: 538 + code: accessHosts + data_type: string + required: false + descr: 'Пример: `192.168.1.0/24` или `*`' + - id: 539 + code: username + data_type: string + required: true + descr: 'Пример: `username-admin`' + sort: 10 + - name: resume + id: 161 + kind: instance + action: resume + params: [] + - name: suspend + id: 160 + kind: instance + action: suspend + params: [] diff --git a/universal_rebuild/resources_yaml/117_nifi.yaml b/universal_rebuild/resources_yaml/117_nifi.yaml new file mode 100644 index 0000000..39c49bd --- /dev/null +++ b/universal_rebuild/resources_yaml/117_nifi.yaml @@ -0,0 +1,81 @@ +name: nifi +service_id: 117 +service_display_name: Nifi +service_short_name: nifi +lifecycle: + suspend_on_destroy_default: false + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 150 + kind: instance + action: create + params: + - id: 471 + code: kafkaUid + data_type: uuid + required: true + ref_svc_id: 116 + descr: Выбрать кластер из списка + sort: 10 + - id: 472 + code: partitions + data_type: integer > 0 + required: true + descr: Число партиций на кластер ≤ 100 × (число брокеров) × (фактор репликации). Пример `50` + man: 'Обратите внимание: уменьшить количество партиций после создания невозможно, поэтому лучше закладывать небольшой запас на будущий рост нагрузки.' + sort: 30 + is_modifiable: true + - id: 473 + code: replicas + data_type: integer > 0 + required: true + descr: Количество партиций ≤ (число брокеров). Пример `2` + sort: 40 + is_modifiable: true + - id: 474 + code: nameTopic + data_type: string + required: true + descr: 'Пример: `orders-events`' + sort: 20 + - name: delete + id: 151 + kind: instance + action: delete + params: [] + - name: modify + id: 154 + kind: instance + action: modify + params: + - id: 477 + code: partitions + data_type: integer > 0 + required: false + descr: Число партиций на кластер ≤ 100 × (число брокеров) × (фактор репликации). Пример `50` + man: 'Обратите внимание: уменьшить количество партиций после создания невозможно, поэтому лучше закладывать небольшой запас на будущий рост нагрузки.' + - id: 478 + code: replicas + data_type: integer > 0 + required: false + descr: Количество партиций ≤ (число брокеров). Пример `2` diff --git a/universal_rebuild/resources_yaml/119_akhq.yaml b/universal_rebuild/resources_yaml/119_akhq.yaml new file mode 100644 index 0000000..7339976 --- /dev/null +++ b/universal_rebuild/resources_yaml/119_akhq.yaml @@ -0,0 +1,104 @@ +name: akhq +service_id: 119 +service_display_name: Akhq +service_short_name: Akhq +service_man: '# Инструкция по развертыванию Apache Kafka через платформу

---
## 1. Общая информация
AKHQ — это веб-интерфейс для работы с кластерами Apache Kafka.
Позволяет просматривать топики, consumer group''ы, сообщения, ACL, схему безопасности, состояние брокеров и метаданные.
Поддерживает работу с несколькими кластерами Kafka одновременно, включая внутренние и внешние (TLS) подключения.

## Доступные операции
- **create** — Создание ресурса AKHQ
- **suspend** — Остановка ресурса
- **resume** — Возобновление работы ресурса
- **modify** — Модификация настроек
- **delete** — Удаление ресурса

---
## 2. Параметры развертывания кластера
Ниже перечислены параметры, которые необходимо указать при создании ресурса Kafka:

* **Кластер Kafka к которому нужно настроить подключение**
Для использования AKHQ необходимо предварительно развернуть сервис Kafka. Пример: `kafka-external`.

* **Платформа для развертывания:**
Платформа для развертывания (среда, в которой будет развернут кластер). Пример: `k8s-3.ext.nubes.ru`.

* **Количество узлов в сети:**
Количество узлов для работы Akhq. Пример `2`

* **Квота (megabyte) памяти пода:**
Квота памяти пода в мегабайтах. Пример: `512`

* **Квота (millicore ) ядра пода:**
Квота CPU пода в миликорах (1000 mCPU = 1 vCPU). Пример: `500`

* **Имя домена:**
Dns Имя по которому будет происходить обращение к сервису. Указать необходимо уникальное имя до . ( то есть если необходимо получить dns ui) Пример: `kafka-external`

---
## 4. Выходные параметры
После успешного развертывания ресурса Kafka платформа возвращает ряд параметров.
Ключевым из них является:

**connectionUrl**
Это полное доменное имя (FQDN), по которому будет доступен интерфейс AKHQ. После развёртывания подождите несколько минут — UI станет доступен по HTTPS, как только все сервисы полностью запустятся.

**Учетные данные**
Здесь представлены поля для ввода логина и пароля, с помощью которых вы сможете авторизоваться в сервисе.

---
## 5. Дополнительная информация
Учётные записи, созданные для работы с AKHQ, предоставляют доступ только для просмотра данных. Для управления Kafka, пожалуйста, используйте возможности личного кабинета.
---' +lifecycle: + suspend_on_destroy_default: true + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 175 + kind: instance + action: create + params: + - id: 550 + code: resourceRealm + data_type: string + required: true + func: getAvailableResourceRealms + descr: Выбрать платформу из списка + sort: 10 + - id: 551 + code: resourceMemory + data_type: integer > 0 + required: true + default: "512" + minvalue: 512 + descr: 'Пример: `1024`' + sort: 20 + - id: 552 + code: resourceCPU + data_type: integer > 0 + required: true + default: "500" + minvalue: 500 + descr: 'Пример: `1000`' + sort: 30 + - id: 553 + code: domain + data_type: string + required: true + regex: ^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$ + descr: Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm + sort: 40 + - id: 554 + code: resourceInstances + data_type: integer > 0 + required: true + default: "1" + minvalue: 1 + sort: 11 + - id: 555 + code: kafkaUid + data_type: uuid + required: true + ref_svc_id: 116 + descr: Выбрать кластер из списка + sort: 5 + is_modifiable: true + - name: delete + id: 176 + kind: instance + action: delete + params: [] + - name: modify + id: 180 + kind: instance + action: modify + params: + - id: 565 + code: kafkaUid + data_type: uuid + required: true + ref_svc_id: 116 + descr: Выбрать кластер из списка + - name: resume + id: 178 + kind: instance + action: resume + params: [] + - name: suspend + id: 177 + kind: instance + action: suspend + params: [] diff --git a/universal_rebuild/resources_yaml/120_clickhouse.yaml b/universal_rebuild/resources_yaml/120_clickhouse.yaml new file mode 100644 index 0000000..e86e6be --- /dev/null +++ b/universal_rebuild/resources_yaml/120_clickhouse.yaml @@ -0,0 +1,211 @@ +name: clickhouse +service_id: 120 +service_display_name: ClickHouse +service_short_name: clickhouse +service_man: '# Инструкция по управлению кластерами и пользователями ClickHouse

---

## Общая информация

ClickHouse — это колоночная система управления базами данных, предназначенная для обработки аналитических запросов в реальном времени. Платформа позволяет создавать, удалять, приостанавливать и возобновлять кластеры ClickHouse, а также управлять базами данных и пользователями.

Кластер создаётся через оператор Altinity (`https://github.com/Altinity/clickhouse-operator`) в ресурсной платформе типа `k8s`

Кластеры могут создаваться шардированными с произвольным количеством реплик. Для обеспечения отказоустойчивости и координации обязательно включение параметра `Создать ClickHouse-Keeper`.

Кластера создаются с обязательным бекапом. Для этого необходимо предварительно создать услугу `S3 Object Storage`.
---

## Доступные операции

* **create** — Создание кластера
* **delete** — Удаление кластера
* **suspend** — Приостановка работы кластера
* **resume** — Возобновление работы кластера
* **create_database** — Создание базы данных
* **delete_database** — Удаление базы данных
* **create_user** — Создание пользователя и настройка прав доступа к базе данных
* **delete_user** — Удаление пользователя

## Процесс создания и первого подключения

1. Выполнить операцию `create` - будет создан кластер
2. Выполнить операцию `create_database` - будет создана база данных
3. Выполнить операцию `create_user` - будет создан пользователь (выбрать доступ подключения к БД, созданной в п.2)

## Примеры подключения

### Подключение через HTTP-интерфейс

```bash
# Проверка доступности кластера
curl "http://<username>:<password>@<host>:8123/" -d "SELECT 1"

# Выполнение запроса
curl "http://<username>:<password>@<host>:8123/" -d "SHOW DATABASES"
```

## Простые кейсы
*Создать БД и обычной таблицы*
```clickhouse
CREATE TABLE database.table
(
id UInt64,
value String
)
ENGINE = MergeTree()
ORDER BY id;

INSERT INTO database.table SELECT number, toString(number) FROM numbers(10000000);

SELECT count() FROM database.table;
```
Базы и таблицы не реплицируются без специального ключа (`ON CLUSTER ''clickhousek8s''`)
В случае создания в стиле, описанным выше, необходимо будет выполнять действия на всех шардах/репликах


*Создание БД и шардированной таблицы*
```clickhouse
CREATE TABLE database.localUsers
(
id UInt64,
value String
)
ENGINE = MergeTree()
ORDER BY id;

CREATE TABLE database.shardedTable
AS localUsers
ENGINE = Distributed(''clickhousek8s'', ''database'', ''localUsers'', id);

INSERT INTO shardedTable SELECT number, toString(number) FROM numbers(10000000);

SELECT count() FROM shardedTable;
```
Важный момент - в данном примере таблицы придётся также выполнять на всех нодах ClickHouse

*Создание реплицируемой и шардированной таблицы*
```clickhouse
CREATE TABLE IF NOT EXISTS database.localUsers ON cluster ''clickhousek8s''
(
user_id UInt64,
name String,
email String
)
ENGINE = ReplicatedMergeTree(
''/clickhouse/tables/{shard}/localUsers'',
''{replica}''
)
ORDER BY user_id;

CREATE TABLE IF NOT EXISTS database.users ON cluster ''clickhousek8s''
AS database.localUsers
ENGINE = Distributed(''clickhousek8s'', ''database'', ''localUsers'', rand());

INSERT INTO database.users VALUES (1, ''Alice'', ''alice@example.com'');
INSERT INTO database.users VALUES (2, ''Bob'', ''bob@example.com'');
INSERT INTO database.users VALUES (3, ''Charlie'', ''charlie@example.com'');
INSERT INTO database.users VALUES (4, ''David'', ''david@example.com'');
INSERT INTO database.users VALUES (5, ''Gosha'', ''gosha@example.com'');
INSERT INTO database.users VALUES (6, ''Eve'', ''eve@example.com'');
INSERT INTO database.users VALUES (7, ''Frank'', ''frank@example.com'');
INSERT INTO database.users VALUES (8, ''Grace'', ''grace@example.com'');
INSERT INTO database.users VALUES (9, ''Hannah'', ''hannah@example.com'');
INSERT INTO database.users VALUES (10, ''Ivan'', ''ivan@example.com'');

SELECT * FROM database.localUsers;
SELECT * FROM database.users;

SELECT COUNT(*) AS total_users FROM database.users;
SELECT COUNT(*) AS total_users_local FROM database.localUsers;
```' +lifecycle: + suspend_on_destroy_default: true + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 181 + kind: instance + action: create + man: Операция создания кластера ClickHouse

Для подключения к кластеру также необходимо создать базу данных через операцию `create_database` и создание пользователя через операцию `create_user` + params: + - id: 837 + code: startupConfiguration + data_type: map-fixed + required: true + sort: 10 + - id: 838 + code: clusterConfiguration + data_type: map-fixed + required: true + sort: 20 + is_modifiable: true + - id: 839 + code: accessConfiguration + data_type: map-fixed + required: true + sort: 40 + is_modifiable: true + - id: 840 + code: clickhouseKeeperConfiguration + data_type: map-fixed + required: true + sort: 30 + is_modifiable: true + - id: 841 + code: clickhouseConfiguration + data_type: map-fixed + required: true + sort: 50 + is_modifiable: true + - id: 842 + code: backupConfiguration + data_type: map-fixed + required: true + sort: 70 + is_modifiable: true + - id: 843 + code: autoscaleConfiguration + data_type: map-fixed + required: true + sort: 80 + is_modifiable: true + - id: 851 + code: clickhouseFilesConfiguration + data_type: array-map-fixed + required: false + man: 'Формат:
- path: somethings3.xml
xmlConfig: ''<clickhouse>......</clickhouse>''' + sort: 60 + is_modifiable: true + - name: create_database + id: 186 + kind: subresource + action: create + subresource: database + man: База данных создаётся на всех репликах кластера ClickHouse + params: + - id: 600 + code: dbName + data_type: string + required: true + default: db01 + regex: ^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$ + descr: 'Пример: `db01`' + sort: 10 + - name: create_user + id: 185 + kind: subresource + action: create + subresource: user + params: + - id: 596 + code: username + data_type: string + required: true + default: myusername + regex: ^(?![0-9_])[a-zA-Z_][a-zA-Z0-9_]{0,62}$ + maxlength: 62 + minlength: 2 + descr: 'Пример: `myusername`' + sort: 10 + - id: 598 + code: dbName + data_type: array-map-fixed + required: true + default: '[{}]' + sort: 30 + - id: 599 + code: accessHosts + data_type: string + required: true + default: ::/0 + descr: 'Пример: `::/0`' + sort: 40 + - name: delete + id: 184 + kind: instance + action: delete + man: Удаление возможно только после остановки сервиса через операцию `suspend`
После выполнения операции необходимо выждать определенное кол-во времени + params: [] + - name: delete_database + id: 188 + kind: subresource + action: delete + subresource: database + man: Необратимое действие!!!
Трижды подумай, заблокируй телефон тимлида и не выполняй это действие + params: + - id: 602 + code: dbName + data_type: string + required: true + descr: ВСЕ ДАННЫЕ ВНУТРИ БД БУДУТ УДАЛЕНЫ + - name: delete_user + id: 187 + kind: subresource + action: delete + subresource: user + params: + - id: 601 + code: username + required: true + descr: Имя пользователя, созданное через операцию `create_user` + - name: modify + id: 189 + kind: instance + action: modify + params: + - id: 844 + code: clusterConfiguration + data_type: map-fixed + required: true + sort: 10 + - id: 845 + code: clickhouseKeeperConfiguration + data_type: map-fixed + required: true + sort: 20 + - id: 846 + code: accessConfiguration + data_type: map-fixed + required: true + sort: 30 + - id: 847 + code: clickhouseConfiguration + data_type: map-fixed + required: true + sort: 40 + - id: 848 + code: backupConfiguration + data_type: map-fixed + required: true + sort: 60 + - id: 849 + code: autoscaleConfiguration + data_type: map-fixed + required: true + sort: 70 + - id: 850 + code: clickhouseFilesConfiguration + data_type: array-map-fixed + required: false + man: 'Формат:
- path: somethings3.xml
xmlConfig: ''<clickhouse>......</clickhouse>''' + sort: 50 + - name: reconcile + id: 276 + kind: action + action: reconcile + params: [] + - name: resume + id: 183 + kind: instance + action: resume + params: [] + - name: suspend + id: 182 + kind: instance + action: suspend + man: Операция приводит к удалению всех подов
Persistent Volume остаются нетронутыми + params: [] diff --git a/universal_rebuild/resources_yaml/12_s3.yaml b/universal_rebuild/resources_yaml/12_s3.yaml new file mode 100644 index 0000000..de81099 --- /dev/null +++ b/universal_rebuild/resources_yaml/12_s3.yaml @@ -0,0 +1,127 @@ +name: s3 +service_id: 12 +service_display_name: S3 Object Storage +service_short_name: s3 +service_man: Руководство доступно по ссылке:
https://docs.s3.msk-1.ngcloud.ru/s3/s3.description.html +lifecycle: + suspend_on_destroy_default: true + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 1 + kind: instance + action: create + params: + - id: 163 + code: resourceRealm + required: true + func: getAvailableResourceRealms + descr: Платформа для развертывания + man: кластер S3 для подключения + sort: 20 + - id: 320 + code: displayName + data_type: string + required: true + regex: ^[a-zA-Z0-9_]{1,61}[a-zA-Z0-9]$ + descr: Человеко-читаемое имя (транслит из Elma) - передаётся имя клиента + sort: 10 + - name: create_sub_user + id: 6 + kind: subresource + action: create + subresource: sub_user + params: + - id: 60 + code: subUserName + data_type: string + required: true + - id: 61 + code: subUserAccess + data_type: string + required: true + value_list: + - read + - write + - readwrite + - full + - name: delete + id: 5 + kind: instance + action: delete + params: [] + - name: delete_sub_user + id: 7 + kind: subresource + action: delete + subresource: sub_user + params: + - id: 63 + code: subUserName + data_type: string + required: true + descr: subUserName. По идее услуга уже создана - должен передаваться из ЛК + - name: modify + id: 4 + kind: instance + action: modify + man: При первичном вызове операции услуга Trial прекращает действие + params: + - id: 50 + code: maxSizeGbPerUser + data_type: integer > 0 + required: false + maxvalue: 50000 + minvalue: 1 + descr: maxSizeGb. Передаём руками + man: maxSizeGb. Передаём руками + - id: 51 + code: maxObjectsPerBucket + data_type: integer > 0 + required: false + maxvalue: 1e+07 + minvalue: 1 + descr: Квота на бакет. Кол-во объектов. Передаём руками + man: Квота на бакет. Кол-во объектов. Передаём руками + - id: 52 + code: maxBucketsPerUser + data_type: integer > 0 + required: false + maxvalue: 1000 + minvalue: 1 + descr: Квота на пользователя. Количество бакетов пользователя. Передаём руками + man: Квота на пользователя. Количество бакетов пользователя. Передаём руками + - name: reconcile + id: 407 + kind: action + action: reconcile + params: [] + - name: resume + id: 3 + kind: instance + action: resume + params: [] + - name: suspend + id: 2 + kind: instance + action: suspend + params: [] diff --git a/universal_rebuild/resources_yaml/13_s3bucket.yaml b/universal_rebuild/resources_yaml/13_s3bucket.yaml new file mode 100644 index 0000000..68e506c --- /dev/null +++ b/universal_rebuild/resources_yaml/13_s3bucket.yaml @@ -0,0 +1,90 @@ +name: s3bucket +service_id: 13 +service_display_name: S3 бакет +service_short_name: s3bucket +lifecycle: + suspend_on_destroy_default: false + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 51 + kind: instance + action: create + params: + - id: 124 + code: s3UserUid + data_type: uuid + required: true + ref_svc_id: 12 + descr: UID Корневой услуги S3. На основе него будет создан бакет. + - id: 125 + code: bucketName + data_type: string + required: true + descr: Имя бакета s3 + - id: 126 + code: maxSize + required: true + default: "-1" + descr: Максимальный размер бакета s3. Если поставить -1, будет безлимит. Размер указывается в Bytes + man: Максимальный размер бакета s3. Если поставить 0, будет безлимит. Размер указывается в Bytes + - id: 127 + code: readAll + data_type: boolean + required: true + default: "false" + value_list: + - "false" + - "true" + descr: Публичный доступ на чтение позволяет любому пользователю интернета скачивать объекты из бакета. + - id: 128 + code: listAll + data_type: boolean + required: true + default: "false" + value_list: + - "false" + - "true" + descr: Публичный доступ к листингу позволяет любому пользователю интернета получить список объектов в бакете. + - id: 129 + code: corsAll + data_type: boolean + required: true + default: "false" + value_list: + - "false" + - "true" + descr: Публичный доступ к чтению настроек позволяет любому пользователю интернета читать конфигурации CORS + - id: 130 + code: placement + data_type: string + required: true + default: HOT + value_list: + - HOT + - COLD + descr: Класс хранилища, который используется по умолчанию при загрузке объектов. Чем холоднее, тем дешевле место, но дороже обращения + - name: delete + id: 52 + kind: instance + action: delete + params: [] diff --git a/universal_rebuild/resources_yaml/149_valo_tenant.yaml b/universal_rebuild/resources_yaml/149_valo_tenant.yaml new file mode 100644 index 0000000..ddb0afb --- /dev/null +++ b/universal_rebuild/resources_yaml/149_valo_tenant.yaml @@ -0,0 +1,98 @@ +name: valo_tenant +service_id: 149 +service_display_name: VALO Cloud +service_short_name: valoTenant +service_man: '# Инструкция по развертыванию Тенант Valo через платформу

## 1. Общая информация
Тенант Valo — логический изолированный экземпляр сервиса VALO Cloud, предоставляющий защищённое облачное хранилище, синхронизацию данных и доступ к документам с различных устройств. При создании тенант автоматически разворачивается в тестовом режиме сроком на 2 месяца. Перевод в промышленную эксплуатацию выполняется отдельной операцией.

### Доступные операции

* **create** — создание нового тенанта в тестовом режиме сроком на 2 месяца.
* **modify** — изменение настроек тенанта, включая перевод из ТЕСТ в ПРОД.
* **suspend** — выключение (приостановка работы) тенанта.
* **resume** — включение ранее приостановленного тенанта.
* **delete** — удаление тенанта; доступно только после выполнения операции suspend.

---

## 2. Параметры развертывания

Параметры входных данных операции **create**:

### Платформа для развертывания
Описание: платформа, на которой будет создан тенант.
Пример значения: `valo.ngcloud.ru`.

### DNS-Префикс
Описание: Будет добавлено в зону valo.ngcloud.ru
Пример значения: `mysimpledomain`.

### Параметры хранилища
Описание: Сколько места будет выделено под использование в тенанте. Данные хранятся в S3. Размер указывается в Gigabytes
Пример значения: `10`.

### Настройки тенанта
Описание: Ряд конфигурации Valo-тенанта, такие как настройки пользователя и доступного функционала
Пример значения: `опционально в рамках настройки`.

---

## 3. Рекомендованные характеристики

### Тестовое окружение
Достаточно развернуть тенант в ТЕСТ. Срок предоставляется в 2 месяца. По истечению срока тенант автоматически переводится в `suspend` на стороне ресурсной платформы

### Промышленное окружение
Для разблокировки используйте операцию `modify`

---

## 4. Выходные параметры

Услуга предоставляет следующие выходные параметры (формируются в процессе операций):

### Статус тенанта
Отражает текущее состояние и параметры.

---

## 5. Дополнительная информация
Тенант всегда создаётся в режиме ТЕСТ и не может быть сразу переведён в ПРОД без операции modify.
Удаление тенанта возможно только после его выключения с помощью операции suspend.
Срок тестового режима составляет 2 месяца с момента создания.
Операции suspend и resume используются для временного управления доступностью сервиса без потери конфигурации.
' +lifecycle: + suspend_on_destroy_default: true + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 202 + kind: instance + action: create + params: + - id: 655 + code: resourceRealm + data_type: string + required: true + func: getAvailableResourceRealms + sort: 10 + - id: 656 + code: domain + data_type: string + required: true + regex: ^[a-z0-9](?:[a-z0-9-]{3,61}[a-z0-9])?$ + maxlength: 61 + minlength: 3 + descr: Будет добавлено в зону valo.ngcloud.ru + sort: 20 + - id: 657 + code: valoOptions + data_type: map-fixed + required: true + default: '{}' + sort: 40 + is_modifiable: true + - id: 658 + code: s3Storage + data_type: integer > 0 + required: true + default: "10" + maxvalue: 999 + minvalue: 1 + descr: Параметр указывается в `Gb` + man: Данные хранятся в отдельном экземпляре S3 + sort: 30 + is_modifiable: true + - name: delete + id: 206 + kind: instance + action: delete + params: [] + - name: modify + id: 203 + kind: instance + action: modify + params: + - id: 659 + code: valoOptions + data_type: map-fixed + required: true + sort: 10 + - id: 660 + code: s3Storage + data_type: integer > 0 + required: true + descr: Параметр указывается в `Gb` + man: Данные хранятся в отдельном экземпляре S3 + sort: 20 + - name: resume + id: 205 + kind: instance + action: resume + params: [] + - name: suspend + id: 204 + kind: instance + action: suspend + params: [] diff --git a/universal_rebuild/resources_yaml/150_k8s_sthutrval_cluster.yaml b/universal_rebuild/resources_yaml/150_k8s_sthutrval_cluster.yaml new file mode 100644 index 0000000..6352528 --- /dev/null +++ b/universal_rebuild/resources_yaml/150_k8s_sthutrval_cluster.yaml @@ -0,0 +1,132 @@ +name: k8s_sthutrval_cluster +service_id: 150 +service_display_name: Kubernetes кластер Штурвал +service_short_name: k8sSthutrvalCluster +service_man: Перед разворачиванием убедиться, что в облаке есть 2 свободных невыделенных IP в пуле +lifecycle: + suspend_on_destroy_default: true + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 108 + kind: instance + action: create + params: + - id: 750 + code: startupConfiguration + data_type: map-fixed + required: true + sort: 10 + - id: 751 + code: controlPlaneConfiguration + data_type: map-fixed + required: true + sort: 30 + is_modifiable: true + - id: 752 + code: workerConfiguration + data_type: array-map-fixed + required: true + default: '[{}]' + sort: 40 + is_modifiable: true + - id: 753 + code: accessConfiguration + data_type: map-fixed + required: true + sort: 50 + is_modifiable: true + - id: 755 + code: clusterConfiguration + data_type: map-fixed + required: true + sort: 20 + is_modifiable: true + - name: create_user + id: 281 + kind: subresource + action: create + subresource: user + params: + - id: 869 + code: admins + data_type: array-map-fixed + required: true + default: '[{}]' + sort: 10 + - name: delete + id: 109 + kind: instance + action: delete + params: [] + - name: delete_user + id: 282 + kind: subresource + action: delete + subresource: user + params: + - id: 870 + code: admins + data_type: array-map-fixed + required: true + sort: 10 + - name: modify + id: 110 + kind: instance + action: modify + man: При изменении кол-ва узлов, удаления группы, если в этой группе есть поды на которых есть PV с named-дисками, удаляться ноды сами НЕ БУДУТ.
Убедитесь, что они будут смигрированы на другие ноды в процессе прежде чем группа узлов/узлы будут удалены + params: + - id: 756 + code: clusterConfiguration + data_type: map-fixed + required: true + sort: 10 + - id: 757 + code: controlPlaneConfiguration + data_type: map-fixed + required: true + sort: 20 + - id: 758 + code: workerConfiguration + data_type: array-map-fixed + required: true + sort: 30 + - id: 759 + code: accessConfiguration + data_type: map-fixed + required: true + sort: 40 + - name: reconcile + id: 253 + kind: action + action: reconcile + params: [] + - name: resume + id: 174 + kind: instance + action: resume + params: [] + - name: suspend + id: 173 + kind: instance + action: suspend + params: [] diff --git a/universal_rebuild/resources_yaml/19_vc_org.yaml b/universal_rebuild/resources_yaml/19_vc_org.yaml new file mode 100644 index 0000000..c559f8f --- /dev/null +++ b/universal_rebuild/resources_yaml/19_vc_org.yaml @@ -0,0 +1,108 @@ +name: vc_org +service_id: 19 +service_display_name: Организация в Cloud Director +service_short_name: vcOrg +service_man: '# Инструкция по развертыванию Организация в Cloud Director через платформу

## 1. Общая информация

Сервис **Организация в Cloud Director** предназначен для управления изолированными организационными единицами (тенантами) внутри инфраструктуры Cloud Director. Он обеспечивает создание, модификацию и администрирование организаций, предоставление им облачных ресурсов, а также контроль уровня доступа. Сервис используется для организации рабочих окружений потребителей, управления жизненным циклом организаций и их сетевой инфраструктурой.

### Доступные операции

**create** — Создание новой организации с заданными характеристиками и типом доступа.
**delete** — Удаление организации после перехода в состояние suspend; приводит к удалению всех связанных сущностей.
**modify** — Изменение параметров внутри организации (на текущий момент поддерживается изменение ipSpaces).
**suspend** — Заморозка организации, ограничение возможности изменения и добавления сущностей.
**resume** — Разморозка организации, восстановление функций управления.

---

## 2. Параметры развертывания

Параметры входных данных операции **create**:

### Платформа для развертывания
Описание: платформа, на которой будет создана организация.
Пример значения: `ngcloud.ru`.

### Тип организации
Описание: тип создаваемой организации.
Варианты:
- `iaas` — организация с прямым доступом к облаку и авторизацией через Keycloak.
- `saas` — организация под управлением Nubes, без прямого доступа к облаку.

---

## 3. Рекомендованные характеристики

### Тестовое окружение
- Тип организации: `iaas` для обеспечения полного доступа к облачным ресурсам и самостоятельного управления.

### Промышленное окружение
- Тип организации: `saas` (упрощённая модель использования).

---

## 4. Выходные параметры

Услуга предоставляет следующие выходные параметры (формируются в процессе операций):

### Url организации
Для быстрого доступа в тенант

### Статус организации
Отражает текущее состояние и параметры.

---

## 5. Дополнительная информация

- Для конфигурации внешних IP внутри организации необходимо предварительно создать:
-- хотя бы один **Виртуальный датацентр (VDC)** привязанный к этой организации;
-- хотя бы один **Сетевой шлюз периметра (Edge Gateway)**, привязанный к vdc выше.
- Удаление организации возможно только через 14 дней после перевода в состояние suspend.
- Удаление организации приводит к удалению всех связанных сущностей: VDC, Edge Gateway, пулов IP, сетей, ВМок и иных объектов.
- Операция suspend не останавливает виртуальные машины, но блокирует любые изменения инфраструктуры внутри организации.
- Для iaas-организаций доступ осуществляется через Keycloak.' +lifecycle: + suspend_on_destroy_default: true + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 136 + kind: instance + action: create + man: '`IaaS` - Доступ в Keycloak предоставляется пользователю, который создаёт услугу
`SaaS` - Без доступа к Keycloak' + params: + - id: 418 + code: resourceRealm + data_type: string + required: true + func: getAvailableResourceRealms + descr: Облако в котором будет развёрнут тенант + man: Выбрать платформу из списка

**Если список не отображается**
обратитесь к сервис менеджеру с вопросом предоставления ресурсной платформы + sort: 10 + - id: 556 + code: organizationType + data_type: string + required: true + default: iaas + value_list: + - iaas + - saas + man: Доступ к организации предоставляется только при выборе `iaas` + sort: 20 + - name: create_user + id: 283 + kind: subresource + action: create + subresource: user + params: + - id: 871 + code: admins + data_type: array-map-fixed + required: true + default: '[{}]' + sort: 10 + - name: delete + id: 137 + kind: instance + action: delete + man: 'Удалить возможно только через 14 дней после выполнения операции `suspend`
**ОСТОРЖНО: Удаление приводит к полному удалению всех сущностей внутри этого тенанта**

Если существуют инстансы VDC, Edge, vApp, Vm или Kubernetes-кластер Штурвал, которые были созданы, из ЛК удалены не будут. Необходимо обратиться к `технической поддержке` для удаления инстансов' + params: [] + - name: delete_user + id: 284 + kind: subresource + action: delete + subresource: user + params: + - id: 872 + code: admins + data_type: array-map-fixed + required: true + sort: 10 + - name: modify + id: 207 + kind: instance + action: modify + man: Для возможности изменения параметров предварительно необходимо создать **Виртуальный датацентр (VDC)**, потом **Сетевой шлюз периметра (Edge Gateway)**
Также при удалении ipSpaces убедитесь, что все внешние IP удалены. В случае если хоть одна привязка vIP есть, данный ipSpace удалён **не будет** + params: + - id: 662 + code: vIPConfigure + data_type: array-map-fixed + required: true + sort: 10 + - name: reconcile + id: 249 + kind: action + action: reconcile + params: [] + - name: resume + id: 144 + kind: instance + action: resume + params: [] + - name: suspend + id: 143 + kind: instance + action: suspend + man: Операция **не останавливает ВМки** в облаке, но не даёт редактировать все сущности внутри тенанта + params: [] diff --git a/universal_rebuild/resources_yaml/1_dummy.yaml b/universal_rebuild/resources_yaml/1_dummy.yaml new file mode 100644 index 0000000..4282a36 --- /dev/null +++ b/universal_rebuild/resources_yaml/1_dummy.yaml @@ -0,0 +1,212 @@ +name: dummy +service_id: 1 +service_display_name: Болванка +service_short_name: dummy +service_man: '## Болванка для тестов.

А это просто много букв, чтобы посмотреть, как ведет себя текст при переполнении контейнера.


``` json
{
"iamServiceUrl": "https://auth-api-test.ngcloud.ru/api/v1/user",
"idpResponse": {
"prefix": {
"charset": "",
"status_text": "Request Time-out",
"text": true,
"errordetail": "",
"filecontent": "Connection Timeout",
"responseheader": {},
"header": "",
"statuscode": "408 Request Time-out",
"status_code": 408,
"mimetype": "Unable to determine MIME type of file."
}
}
}
```' +lifecycle: + suspend_on_destroy_default: true + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 18 + kind: instance + action: create + man: Это мануал к операции CREATE сервиса Болванка + params: + - id: 198 + code: durationMs + data_type: integer >= 0 + required: true + default: "0" + descr: перед завершением подождать (sleep) столько миллисекунд + man: перед завершением подождать (sleep) столько миллисекунд + sort: 20 + is_modifiable: true + - id: 199 + code: failAtStart + data_type: boolean + required: true + default: "false" + value_list: + - "false" + - "true" + descr: если true, завершить джоб, ничего не записывая в cmdb + man: '**Операция не будет завершена**
Она будет в постоянном статусе PENDING, однако в Jenkins Job завершится с ошибкой' + sort: 30 + is_modifiable: true + - id: 200 + code: failInProgress + data_type: boolean + required: true + default: "false" + value_list: + - "false" + - "true" + descr: если true, корректно начать выполнение джоба, но в процессе завершить аварийно, ничего не записывая в cmdb + man: если true, корректно начать выполнение джоба, но в процессе завершить аварийно, ничего не записывая в cmdb + sort: 40 + is_modifiable: true + - id: 201 + code: whereFail + data_type: integer > 0 + required: false + default: "1" + value_list: + - "1" + - "2" + - "3" + descr: принимает 1,2,3. Работает с failInProgress. 1 - упасть в prepare. 2 - упасть внутри блока Data fill. 3 - упасть после vault write + man: принимает 1,2,3. Работает с failInProgress. 1 - упасть в prepare. 2 - упасть внутри блока Data fill. 3 - упасть после vault write + sort: 50 + is_modifiable: true + - id: 242 + code: resourceRealm + data_type: string + required: true + func: getAvailableResourceRealms + descr: Платформа для развертывания + man: Наименование ресурсной платформы для подключения + sort: 10 + - id: 286 + code: bodymessage + data_type: string + required: false + descr: Дата, уходящая в Vault + man: Дата, уходящая в Vault + sort: 60 + is_modifiable: true + - id: 321 + code: mapExample + data_type: map + required: false + is_modifiable: true + - id: 322 + code: jsonExample + data_type: json + required: false + descr: Для тестирования интерфейса + man: Для тестирования интерфейса. Сюда можно писать корректный json + is_modifiable: true + - id: 396 + code: nestedRefExample + required: false + - id: 647 + code: mapFixed + data_type: map-fixed + required: false + descr: Новый тип поля + - id: 654 + code: arrayMapFixedExample + data_type: array-map-fixed + required: false + descr: Array of map-fixed + - id: 863 + code: arr + data_type: array + required: true + value_list: + - test + - "2" + - val1 + - name: delete + id: 71 + kind: instance + action: delete + params: [] + - name: modify + id: 92 + kind: instance + action: modify + params: + - id: 287 + code: durationMs + data_type: integer >= 0 + required: true + default: "0" + descr: перед завершением подождать (sleep) столько миллисекунд + man: перед завершением подождать (sleep) столько миллисекунд + sort: 10 + - id: 288 + code: failAtStart + data_type: boolean + required: true + default: "false" + value_list: + - "false" + - "true" + descr: если true, завершить джоб, ничего не записывая в cmdb + sort: 20 + - id: 289 + code: failInProgress + data_type: boolean + required: true + default: "false" + value_list: + - "false" + - "true" + descr: если true, корректно начать выполнение джоба, но в процессе завершить аварийно, ничего не записывая в cmdb + sort: 30 + - id: 290 + code: whereFail + data_type: integer > 0 + required: false + value_list: + - "1" + - "2" + - "3" + descr: принимает 1,2,3. Работает с failInProgress. 1 - упасть в prepare. 2 - упасть внутри блока Data fill. 3 - упасть после vault write + man: принимает 1,2,3. Работает с failInProgress. 1 - упасть в prepare. 2 - упасть внутри блока Data fill. 3 - упасть после vault write + sort: 40 + - id: 291 + code: bodymessage + required: false + descr: Дата, уходящая в Vault + man: Дата, уходящая в Vault + sort: 50 + - id: 373 + code: mapExample + data_type: map + required: false + default: '{}' + - id: 374 + code: jsonExample + required: false + - name: reconcile + id: 257 + kind: action + action: reconcile + params: [] + - name: redeploy + id: 240 + kind: action + action: redeploy + params: [] + - name: resume + id: 94 + kind: instance + action: resume + params: [] + - name: suspend + id: 93 + kind: instance + action: suspend + params: [] diff --git a/universal_rebuild/resources_yaml/20_vc_org_saas.yaml b/universal_rebuild/resources_yaml/20_vc_org_saas.yaml new file mode 100644 index 0000000..1520bdf --- /dev/null +++ b/universal_rebuild/resources_yaml/20_vc_org_saas.yaml @@ -0,0 +1,57 @@ +name: vc_org_saas +service_id: 20 +service_display_name: Организация [DEPRECATED] +service_short_name: vcOrgSaas +service_man: 'Верхнеуровневая сущность создаваемая для Заказчика.
Внутри Организация вы можете создать: Виртуальные датацентры, Каталоги.' +lifecycle: + suspend_on_destroy_default: true + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 8 + kind: instance + action: create + man: Создание организации в Cloud Director
Id в клауд директоре создаётся на основе contragentCode + params: + - id: 185 + code: resourceRealm + data_type: string + required: true + func: getAvailableResourceRealms + descr: Платформа для развертывания + man: Наименование ресурсной платформы + sort: 30 + - name: delete + id: 23 + kind: instance + action: delete + params: [] + - name: resume + id: 119 + kind: instance + action: resume + params: [] + - name: suspend + id: 118 + kind: instance + action: suspend + params: [] diff --git a/universal_rebuild/resources_yaml/21_vc_vdc.yaml b/universal_rebuild/resources_yaml/21_vc_vdc.yaml new file mode 100644 index 0000000..4870b3a --- /dev/null +++ b/universal_rebuild/resources_yaml/21_vc_vdc.yaml @@ -0,0 +1,142 @@ +name: vc_vdc +service_id: 21 +service_display_name: Виртуальный датацентр (vDC) +service_short_name: vc_vdc +service_man: '# Инструкция по развертыванию Виртуальный датацентр (vDC) через платформу

## 1. Общая информация

**Виртуальный датацентр (vDC)** — это логическая изолированная среда в VMware Cloud Director, предназначенная для размещения и управления виртуальными машинами, сетевой инфраструктурой и ресурсами хранения. Сервис обеспечивает выделение вычислительных ресурсов под конкретную организацию и предоставляет гибкие параметры масштабирования, контроля потребления и разделения ресурсов.

### Доступные операции

**create** — Создание виртуального датацентра (vDC) с заданными параметрами CPU, RAM, Storage и сетевой конфигурацией.
**delete** — Удаление vDC. Требуется предварительный перевод vDC в состояние *suspend*. Полное удаление возможно через 14 дней.
**modify** — Внесение изменений в параметры ресурсов (CPU/RAM/Storage), выделенных для vDC.
**suspend** — Заморозка vDC. Новые ресурсы или изменения внутри vDC становятся невозможными, работа ВМ не останавливается.
**resume** — Разморозка vDC. Восстановление возможности управления сущностями внутри vDC.

---

## 2. Параметры развертывания

Ниже перечислены входные параметры операции **create**.

### UUID Организации
Уникальный идентификатор организации в Cloud Director. Перед созданием vDC требуется предварительное создание услугу `Организация в Cloud Director`.

### Provider VDC в Cloud Director
Свойство ресурсной платформы, определяющее, какой пул физических ресурсов будет использоваться.

### Network Pool в Cloud Director
Определяет возможности сетевой конфигурации, включая создание Edge Gateway и выход во внешние сети.

### Гарантированная доля ЯДЕР
Гарантированный процент вычислительных ресурсов (CPU), закрепленный за vDC. Значение устанавливается разово при создании.

### Гарантированная доля ОЗУ
Гарантированный процент оперативной памяти, закрепленный за vDC. Также задаётся один раз при создании.

### Сколько аллоцировать на VDC ЯДЕР
Количество виртуальных ядер, выделяемых в рамках vDC.

### Сколько аллоцировать на VDC ОЗУ
Количество оперативной памяти в GB, выделяемой для vDC.

### Конфигурация Storage
Набор дисковых политик и их объёмов, выделяемых для vDC. Указывается имя политики и требуемый размер (GB).

---

## 3. Рекомендованные характеристики

### Тестовое окружение

- Количество ядер CPU: 10–20
- Аллоцируемая RAM: 16–64 GB
- Гарантированная доля CPU: 20%
- Гарантированная доля RAM: 20%
- Storage:
- Политика `SATA`
- Объём: 100–300 GB

### Промышленное окружение

- Количество ядер CPU: 16–128
- Аллоцируемая RAM: 64–512 GB
- Гарантированная доля CPU: 50%
- Гарантированная доля RAM: 50%
- Storage:
- Политики уровня `SSD`
- Объём: 1–10 TB, выбор несколько политик под разные нагрузки

---

## 4. Выходные параметры

Выходные параметры формируются операцией **create** (и используются другими операциями):

### Имя vDC
Уникальный идентификатор созданного виртуального датацентра. Используется в дальнейших операциях (modify, suspend, delete).

### Параметры выделенных ресурсов
Подтверждение итогового объёма CPU/RAM/Storage, выделенных vDC. Используются для учёта и контроля потребления.

---

## 5. Дополнительная информация

- Перед созданием vDC необходимо обеспечить наличие корректно созданной организации и доступных пула ресурсов ресурсной платформы.
- Гарантированная доля CPU и RAM задаётся **однократно при создании vDC**. После развертывания изменить эти значения невозможно; для изменения необходимо пересоздать vDC.
- Удаление vDC возможно только через **14 дней после перевода в состояние *suspend***. Удаление очищает все связанные сущности (ВМ, сети, каталоги), поэтому необходимо убедиться, что данные перенесены.
- Платформа Cloud Director использует концепцию изолированных организационных ресурсов, поэтому корректная настройка Provider VDC и Network Pool является обязательной при создании vDC.
- При изменении параметров (modify) изменяются только показатели CPU, RAM и Storage. Сетевые параметры и гарантированные доли остаются неизменными.
' +lifecycle: + suspend_on_destroy_default: true + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 9 + kind: instance + action: create + params: + - id: 30 + code: organizationUid + data_type: uuid + required: true + ref_svc_id: 19 + man: Для создания VDC предварительно необходимо создать организацию через услугу `Организация в Cloud Director` + sort: 10 + - id: 335 + code: providerVdc + data_type: string + required: true + man: Приписка `2.8` - Скорость vCPU - 2800MHz
И так далее...
+ sort: 30 + depends_on: organizationUid,networkProvider + - id: 361 + code: storageConfig + data_type: array-map-fixed + required: true + default: '[{}]' + man: Первая политика в списке указывается как "По умолчанию" + sort: 100 + is_modifiable: true + - id: 397 + code: cpuGuaranteed + data_type: integer > 0 + required: true + default: "20" + value_list: + - "20" + - "50" + - "80" + descr: '% резервирования' + man: Свойство нельзя изменить после создания + sort: 60 + - id: 557 + code: cpuAllocated + data_type: integer > 0 + required: true + default: "10" + maxvalue: 200 + minvalue: 1 + descr: шт + sort: 80 + is_modifiable: true + - id: 558 + code: memAllocated + data_type: integer > 0 + required: true + default: "20" + maxvalue: 4096 + minvalue: 1 + descr: gb + sort: 90 + is_modifiable: true + - id: 746 + code: networkProvider + data_type: string + required: true + sort: 20 + depends_on: organizationUid + - name: delete + id: 24 + kind: instance + action: delete + man: 'Удалить возможно только через 14 дней после выполнения операции `suspend`
**ОСТОРЖНО: Удаление приводит к полному удалению всех сущностей внутри этого VDC**

Если существуют инстансы Edge, vApp, Vm или Kubernetes-кластер Штурвал, которые были созданы, из ЛК удалены не будут. Необходимо обратиться к `технической поддержке` для удаления инстансов' + params: [] + - name: modify + id: 112 + kind: instance + action: modify + params: + - id: 560 + code: cpuAllocated + data_type: integer > 0 + required: true + maxvalue: 999 + minvalue: 1 + descr: шт + sort: 10 + - id: 561 + code: memAllocated + data_type: integer > 0 + required: true + maxvalue: 1024 + minvalue: 1 + descr: gb + sort: 20 + - id: 562 + code: storageConfig + data_type: array-map-fixed + required: true + man: Первая политика в списке указывается как "По умолчанию" + sort: 30 + - name: reconcile + id: 250 + kind: action + action: reconcile + params: [] + - name: resume + id: 172 + kind: instance + action: resume + params: [] + - name: suspend + id: 171 + kind: instance + action: suspend + man: Операция **не останавливает ВМки** в облаке, но не даёт редактировать все сущности внутри VDC + params: [] diff --git a/universal_rebuild/resources_yaml/22_vc_nsxt.yaml b/universal_rebuild/resources_yaml/22_vc_nsxt.yaml new file mode 100644 index 0000000..158f59e --- /dev/null +++ b/universal_rebuild/resources_yaml/22_vc_nsxt.yaml @@ -0,0 +1,139 @@ +name: vc_nsxt +service_id: 22 +service_display_name: Сетевой шлюз периметра (Edge) +service_short_name: vc_nsxt +service_man: '# Инструкция по развертыванию Сетевой шлюз периметра (Edge) через платформу

## 1. Общая информация

Сервис **Сетевой шлюз периметра (Edge)** предназначен для создания периметрового сетевого шлюза в рамках одного виртуального датацентра (vDC) или группы виртуальных датацентров (groupvDC).
При создании автоматически разворачивается routed-сеть с адресным пространством `10.10.102.0/24`.
Сервис обеспечивает сетевую изоляцию, маршрутизацию, а также может включать функциональность балансировщика нагрузки AVI (ALB) для последующей интеграции, включая поддержку кластеров Штурвал.

### Доступные операции

**create** — Создание нового Edge с привязкой к vDC или groupvDC и развёртыванием routed-сети.
**delete** — Удаление ранее созданного Edge. Недоступно при наличии зависимых услуг (vApp, VM, Кластер Штурвал).
**modify** — Изменение параметров и сетевых настроек существующего Edge.

---

## 2. Параметры развертывания

Ниже приведены параметры операции **create**.

### Тип родительской услуги
Определяет контекст размещения Edge.
Допустимые значения: `vdc`, `groupvdc`.
Использование:
- При выборе `vdc` обязателен параметр **UUID VDC**.
- При выборе `groupvdc` обязателен параметр **UUID Группы VDC**.

### UUID VDC
Идентификатор виртуального датацентра.
Необходимо предварительно создать vDC через услугу «Виртуальный датацентр (vDC)».
Указывается только при выборе родительского типа `vdc`.

### UUID Группы VDC
Идентификатор группы виртуальных датацентров.
Создаётся через услугу «Группа виртуальных датацентров (groupvDC)».
Используется при выборе родительского типа `groupvdc`.

### Включить ALB
Флаг активации AVI Load Balancer в Cloud Director.
Пример значения: `true`/`false`.
Нужен для развёртывания кластера Штурвал и возможности создания Virtual Services.

### Наименование segroup
Имя сервиса групп (segroup) на уровне ресурсной платформы.
Обязательно при включённом ALB.
Определяет группу, в которой будут выделяться пулы AVI для Virtual Services.

### virtualServicesCount
Количество резервируемых виртуальных сервисов (Virtual Services) на AVI.
Типичное значение: 1–10 в зависимости от нагрузки.

---

## 3. Рекомендованные характеристики

### Тестовое окружение (Test)

- Тип родительской услуги: `vdc`
- virtualServicesCount: 1–2
- ALB: выключен по умолчанию (включать только при необходимости тестирования Штурвала)
- segroup: задаётся только при включённом ALB
- VDC гарантии CPU/RAM: минимальные (поскольку изменить их после создания нельзя)

### Промышленное окружение (Production)

- Тип родительской услуги: `groupvdc` при работе с распределёнными нагрузками или `vdc` для локализованных проектов
- virtualServicesCount: 3–10, исходя из предполагаемого количества публикаций
- ALB: включён, если требуется высокая доступность сервисов или используется кластер Штурвал
- segroup: рекомендуется использовать выделенную группу под проект
- VDC гарантии CPU/RAM: повышенные, с учётом того, что изменить их после создания невозможно без пересоздания vDC

---

## 4. Выходные параметры

### Имя Edge
Уникальное название созданного сетевого шлюза, используется для ссылок и дальнейших операций.

### Имя routed-сети
Автоматически созданная routed-сеть с подсетью `10.10.102.0/24`, используется для подключения ресурсов.

### Параметры Edge
Набор параметров, указанных пользователем при создании: тип услуги, ALB, segroup, virtualServicesCount и др.
Используются в операциях modify и для анализа состояния сервиса.

---

## 5. Дополнительная информация

- В рамках сервиса **один раз при создании задаётся гарантированная доля CPU/RAM vDC**. Изменить её невозможно — требуется пересоздание vDC.
- Удаление Edge недоступно при наличии зависимых ресурсов (vApp, VM, кластер Штурвал).
- При использовании ALB необходимо корректно указывать segroup, чтобы обеспечить корректное выделение пулов AVI.
- При выборе режима `groupvdc` следует учитывать распределение нагрузки между несколькими vDC.
- SNAT для всех ресурсов внутри Edge можно активировать через операцию modify (параметры выделения VIP и ipSpace).

' +lifecycle: + suspend_on_destroy_default: false + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 10 + kind: instance + action: create + params: + - id: 8 + code: vdcUid + data_type: string + required: false + ref_svc_id: 21 + descr: UUID Услуги `Виртуальный датацентр (vDC)` + man: 'Необходимо, если выбран тип подключаемой VDC: `vdc`' + sort: 10 + - id: 340 + code: needEnableAVI + data_type: boolean + required: true + default: "false" + value_list: + - "false" + - "true" + descr: Включение Load Balancer + man: Параметры `Наименование segroup`, `Кол-во VS на AVI` необходимо также указать + sort: 30 + is_modifiable: true + - id: 341 + code: virtualServicesCount + data_type: integer > 0 + required: false + default: "1" + maxvalue: 4 + minvalue: 1 + descr: Кол-во виртуальных сервисов, которые **резервируются** на AVI + man: Во избежании коллапса пока выделяется до 4
Необходимо указывать, если включён параметр `Включить ALB` + sort: 50 + is_modifiable: true + - id: 621 + code: vdcType + data_type: string + required: true + default: vdc + value_list: + - vdc + - vdcGroup + descr: Тип родительской услуги + man: При выборе vdc обеспечивает сетевую доступность только в рамках этого vdc
При выборе groupvdc обеспечивает сетевую доступность между всеми vdc, которые включены в groupvdc + sort: 0 + - id: 622 + code: vdcGroupUid + data_type: string + required: false + ref_svc_id: 29 + descr: UUID Услуги `Группа виртуальных датацентров (groupvDC)` + man: 'Необходимо, если выбран тип подключаемой VDC: `groupvdc`' + sort: 20 + - id: 825 + code: qosProfile + required: false + descr: Параметр пока не работает
Должен возвращать поле из ресурсной платформы типа 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

Регулярку могу написать (Виталя) + man: if (vdcUid != '") {
наборфункций1
}

elif (vdcGroupUid != "") {
наборфункций2
}

else {
return "Необходимо выбрать vdc или vdcgroup"
} + sort: 60 + depends_on: vdcUid,vdcGroupUid + is_modifiable: true + - name: delete + id: 25 + kind: instance + action: delete + man: Невозможно удалить если routed-сеть в Edge связана с:
- vApp
- VM
- Kubernetes-кластер Штурвал

Запустить операцию возможно, но будет ошибка + params: [] + - name: modify + id: 111 + kind: instance + action: modify + man: Для создания SNAT Правила убедитесь, что в организации есть свободные IP
Редактировать кол-во свободных IP можно в услуге `Организация в Cloud Director` -> `modify` + params: + - id: 368 + code: needEnableAVI + data_type: boolean + required: false + value_list: + - "false" + - "true" + descr: Включение Load Balancer + man: Параметры `Наименование segroup`, `Кол-во VS на AVI` необходимо также указать + sort: 10 + - id: 369 + code: virtualServicesCount + data_type: integer > 0 + required: false + maxvalue: 4 + minvalue: 1 + descr: Кол-во виртуальных сервисов, которые выделяются на AVI + man: Во избежании коллапса пока выделяется до 4
Необходимо указывать, если включён параметр `Включить ALB` + sort: 30 + - id: 372 + code: ipSpaceName + data_type: string + required: false + descr: Имя ip Space для внешнего IP + man: Необходимо указывать, если включён параметр `Выделить VIP для SNAT` + sort: 50 + - id: 856 + code: qosProfile + data_type: string + required: false + sort: 40 + - name: reconcile + id: 252 + kind: action + action: reconcile + params: [] diff --git a/universal_rebuild/resources_yaml/23_vc_vm.yaml b/universal_rebuild/resources_yaml/23_vc_vm.yaml new file mode 100644 index 0000000..470873d --- /dev/null +++ b/universal_rebuild/resources_yaml/23_vc_vm.yaml @@ -0,0 +1,153 @@ +name: vc_vm +service_id: 23 +service_display_name: VM в Cloud Director (старый формат) (vc_vm) +service_short_name: vc_vm +service_man: Виртуальная машина без каких-либо особенностей.
Данная услуга запускает vc_ip для выдачи белого адреса и подключения к ВМ +lifecycle: + suspend_on_destroy_default: true + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 26 + kind: instance + action: create + params: + - id: 33 + code: vappUid + data_type: uuid + required: true + ref_svc_id: 26 + descr: InstanceUid vApp. vApp должен быть создан + man: InstanceUid vApp. vApp должен быть создан + sort: 10 + - id: 35 + code: vmName + data_type: string + required: true + default: vm0001 + regex: ^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$ + descr: Имя ВМ. Не поддерживает спец-символы + man: Имя ВМ. Не поддерживает спец-символы + sort: 20 + - id: 36 + code: vmCpu + data_type: string + required: true + default: "2" + descr: CPU + man: CPU + sort: 30 + is_modifiable: true + - id: 37 + code: vmRam + data_type: string + required: true + default: "2" + descr: RAM (GB) + man: RAM (GB) + sort: 40 + is_modifiable: true + - id: 38 + code: vmDisksize + data_type: string + required: true + default: "10" + descr: Размер диска под сервис (GB) + man: Размер диска под сервис (GB) + sort: 50 + is_modifiable: true + - id: 176 + code: templateName + data_type: string + required: true + default: RockyLinux_9-16G + value_list: + - RockyLinux_9-16G + - OracleLinux_9-16G + - Ubuntu_24-20G + - Ubuntu_22-20G + descr: ОС + man: ОС + sort: 60 + - id: 177 + code: accessIpList + data_type: string + required: true + default: 79.111.119.78/32 185.247.187.1/32 + descr: Вставить содержимое из 'curl ifconfig.io'
IP адреса в CIDR формате через пробел для настройки аксес листов + man: IP адреса в CIDR формате через пробел для настройки аксес листов + sort: 70 + is_modifiable: true + - id: 406 + code: ipSpaceName + data_type: string + required: true + descr: Брать из свойств ресурсной платформы
.vcd.ipSpace

Имя IP-space для выделения внешнего IP (IP выделяется обязательно) + - name: delete + id: 29 + kind: instance + action: delete + params: [] + - name: modify + id: 68 + kind: instance + action: modify + man: Операция автоматически останавливает ВМ. + params: + - id: 186 + code: vmCpu + data_type: integer > 0 + required: false + descr: CPU + man: CPU + sort: 10 + - id: 187 + code: vmRam + data_type: integer > 0 + required: false + descr: RAM + man: RAM + sort: 20 + - id: 188 + code: vmDisksize + data_type: integer > 0 + required: false + descr: Размер диска под сервис (GB) + man: Размер диска под сервис (GB) + sort: 30 + - id: 189 + code: accessIpList + data_type: string + required: false + descr: IP адреса в CIDR формате через пробел для настройки аксес листов + man: IP адреса в CIDR формате через пробел для настройки аксес листов + sort: 40 + - name: resume + id: 28 + kind: instance + action: resume + params: [] + - name: suspend + id: 27 + kind: instance + action: suspend + params: [] diff --git a/universal_rebuild/resources_yaml/25_vcexternalip.yaml b/universal_rebuild/resources_yaml/25_vcexternalip.yaml new file mode 100644 index 0000000..eaa29b4 --- /dev/null +++ b/universal_rebuild/resources_yaml/25_vcexternalip.yaml @@ -0,0 +1,162 @@ +name: vcexternalip +service_id: 25 +service_display_name: Публичные IP адреса +service_short_name: vcexternalip +service_man: '**Обычно эта услуга разворачивается автоматически при развёртывании более сложных услуг**
Вручную создавать можно, однако услуга требует много параметров, о которых можно просто не знать

Резервировать публичные IP адреса можно только после создания NGcloud Organization, NGcloud Виртуальный датацентр, NGcloud Каталог, Сетевой шлюз периметра.
Вы можете назначить IP адрес для существующего экземпляра сервиса.' +lifecycle: + suspend_on_destroy_default: false + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 56 + kind: instance + action: create + params: + - id: 140 + code: serviceUid + data_type: string + required: true + sort: 20 + is_modifiable: true + - id: 141 + code: dnatCreate + data_type: boolean + required: true + default: "true" + descr: Необходимо создать DNAT-правило. + sort: 40 + is_modifiable: true + - id: 142 + code: internalPortAccess + data_type: string + required: true + default: "5432" + descr: Порт для связки ната с адресом. Несколько значений можно передавать через пробел. Если передать all, то dnat будет на все порты. Требует dnatCreate=true
+ sort: 60 + is_modifiable: true + - id: 143 + code: snatCreate + data_type: boolean + required: false + descr: Необходимо создать SNAT-правило.
+ sort: 50 + is_modifiable: true + - id: 144 + code: internalAddrAccess + data_type: string + required: true + descr: Внутренний адрес для связки ната. Требует dnatCreate=true или snatCreate=true + sort: 70 + is_modifiable: true + - id: 324 + code: fromServiceNamespace + data_type: string + required: true + descr: Namespace составной услуги (для отображения правил FW) + sort: 80 + - id: 325 + code: fromServiceCloudEdgeName + data_type: string + required: false + descr: Edge составной услуги (указывать, если resourceRealm связан с CloudDirector) + sort: 90 + - id: 326 + code: fromServiceCloudVdcName + required: false + descr: Vdc составной услуги (указывать, если resourceRealm связан с CloudDirector) + sort: 100 + - id: 327 + code: fromServiceCloudOrgName + data_type: string + required: false + descr: Org составной услуги (указывать, если resourceRealm связан с CloudDirector) + sort: 110 + - id: 328 + code: fromServiceCloudVmwareUrl + data_type: string + required: false + descr: Api-Url составной услуги (указывать, если resourceRealm связан с CloudDirector) + sort: 120 + - id: 336 + code: ipSpaceName + data_type: string + required: true + descr: Тип ip Space (internet-ipv4-v1 / internet-antiddos-v1 / internet-no-antiddos-v1) + sort: 30 + is_modifiable: true + - id: 488 + code: resourceRealm + data_type: string + required: true + func: getAvailableResourceRealms + descr: Платформа для развертывания + sort: 200 + - id: 624 + code: fromServiceCloudEdgeScope + data_type: string + required: false + sort: 130 + - id: 626 + code: fromServiceVdcGroupName + data_type: string + required: false + sort: 140 + - name: delete + id: 57 + kind: instance + action: delete + params: [] + - name: modify + id: 197 + kind: instance + action: modify + params: + - id: 627 + code: serviceUid + data_type: string + required: true + sort: 20 + - id: 628 + code: ipSpaceName + data_type: string + required: true + sort: 30 + - id: 629 + code: dnatCreate + data_type: boolean + required: true + sort: 40 + - id: 630 + code: snatCreate + data_type: boolean + required: true + sort: 50 + - id: 631 + code: internalPortAccess + data_type: string + required: true + sort: 60 + - id: 632 + code: internalAddrAccess + data_type: string + required: true + sort: 70 diff --git a/universal_rebuild/resources_yaml/26_vapp.yaml b/universal_rebuild/resources_yaml/26_vapp.yaml new file mode 100644 index 0000000..b48cabd --- /dev/null +++ b/universal_rebuild/resources_yaml/26_vapp.yaml @@ -0,0 +1,73 @@ +name: vapp +service_id: 26 +service_display_name: Виртуальный каталог ВМ (vApp) +service_short_name: vapp +service_man: '# Инструкция по развертыванию Виртуальный каталог ВМ (vApp) через платформу

## 1. Общая информация

**Виртуальный каталог ВМ (vApp)** — это логическая каталог, объединённая в единый управляемый объект. Сервис предназначен для совместного развертывания, управления жизненным циклом и сетевого взаимодействия набора связанных ВМ. Основные функции включают:
- централизованное управление включением, выключением и приостановкой группы ВМ;
- унифицированный сетевой доступ через привязанный Edge-шлюз;

### Доступные операции

**create** — Создание нового vApp с привязкой к заданным услугам VDC и Edge
**delete** — Удаление существующего vApp. Требуется предварительный перевод vApp в состояние *suspend*. Полное удаление возможно через 14 дней.
**suspend** — Остановка vApp (выключение всех ВМ внутри каталога)
**resume** — Запуск vApp (включение всех ВМ внутри каталога)

---

## 2. Параметры развертывания

Параметры берутся из операции **create**.

### UUID Услуги VDC
Уникальный идентификатор услуги «Виртуальный датацентр (vDC)».
Используется для определения вычислительной инфраструктуры, в которой будет развернут vApp.

### UUID Услуги Edge
Уникальный идентификатор услуги «Сетевой шлюз периметра (Edge)».
Обеспечивает маршрутизацию и сетевое подключение. Может быть привязан к группе VDC, поэтому выбирается отдельно.

### Имя vApp
Строгое имя, соответствующее маске формирования DNS-имени.
Используется как часть в DNS-имени при создании виртуальных машин.

---

## 3. Рекомендованные характеристики
Так как является промежуточным этапом для создания ВМ, все характеристики описываются в услуге `Виртуальная машина`

---

## 4. Выходные параметры

### Параметры vApp
Заданные при создании vApp

---

## 5. Дополнительная информация

- Привязка к VDC и Edge осуществляется раздельно, поскольку один Edge может обслуживать несколько VDC (через услугу `Группа виртуальных датацентров (groupvDC)`). Это гарантирует возможность развернуть ВМ в единой сети, но в разных виртуальных датацентрах.
- Перед удалением vApp необходимо вручную перевести его в состояние **suspend**. Удаление возможно только спустя 14 дней после этого шага.
- Операция delete удаляет весь vApp полностью, включая все виртуальные машины, связанные с ним.
- Рекомендуется заранее планировать сетевые связи и назначения DNS-имён для обеспечения корректной работы сервисов, развернутых внутри vApp.
- При проектировании инфраструктуры следует учитывать зависимости между Edge-шлюзом, VDC и размещаемыми ВМ, чтобы избежать ограничений при масштабировании и интеграции.' +lifecycle: + suspend_on_destroy_default: true + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 69 + kind: instance + action: create + params: + - id: 190 + code: nsxtUid + data_type: uuid + required: true + ref_svc_id: 22 + descr: UUID Услуги `Сетевой шлюз периметра (Edge)` + man: К какой сети может быть подключен виртуальный каталог + sort: 20 + - id: 191 + code: vappName + data_type: string + required: true + regex: ^[a-z0-9][a-z0-9.-]{3,61}[a-z0-9]$ + minlength: 3 + descr: Уникальное в рамках организации + man: Имеет строгую маску `^[a-z0-9][a-z0-9.-]{3,61}[a-z0-9]$`

Используется в формировании DNS-Имени (для виртуальных машин) + sort: 30 + - id: 623 + code: vdcUid + data_type: uuid + required: true + ref_svc_id: 21 + descr: UUID Услуги `Виртуальный датацентр (vDC)` + man: В каких ресурсах будет создан виртуальный каталог + sort: 10 + - name: delete + id: 70 + kind: instance + action: delete + params: [] + - name: resume + id: 209 + kind: instance + action: resume + params: [] + - name: suspend + id: 208 + kind: instance + action: suspend + params: [] diff --git a/universal_rebuild/resources_yaml/27_vc_vm_v2.yaml b/universal_rebuild/resources_yaml/27_vc_vm_v2.yaml new file mode 100644 index 0000000..2b1d235 --- /dev/null +++ b/universal_rebuild/resources_yaml/27_vc_vm_v2.yaml @@ -0,0 +1,71 @@ +name: vc_vm_v2 +service_id: 27 +service_display_name: VM в Cloud Director (vc_vmV2) +service_short_name: vc_vm_v2 +lifecycle: + suspend_on_destroy_default: true + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 100 + kind: instance + action: create + params: + - id: 301 + code: serverListJson + data_type: json + required: true + default: '[{}]' + maxlength: 65536 + minlength: 10 + descr: Необходимо изменить vappUid и ipSpaceName
Пример конфигурации взять по ссылке ниже

Объект серверов вида [{}] (Map(Object))
Более подробнее смотреть в README.md (https://gitea.mgmt.nubes.ru/cloudServices/vcVmV2) + sort: 20 + is_modifiable: true + - name: delete + id: 101 + kind: instance + action: delete + params: [] + - name: modify + id: 104 + kind: instance + action: modify + params: + - id: 302 + code: serverListJson + data_type: json + required: true + default: '[{}]' + maxlength: 65536 + minlength: 10 + descr: Объект серверов вида [{}] (Map(Object))
Более подробнее смотреть в README.md (https://gitea.mgmt.nubes.ru/cloudServices/vcVmV2) + sort: 10 + - name: resume + id: 102 + kind: instance + action: resume + params: [] + - name: suspend + id: 103 + kind: instance + action: suspend + params: [] diff --git a/universal_rebuild/resources_yaml/28_vc_vm_v3.yaml b/universal_rebuild/resources_yaml/28_vc_vm_v3.yaml new file mode 100644 index 0000000..f05d808 --- /dev/null +++ b/universal_rebuild/resources_yaml/28_vc_vm_v3.yaml @@ -0,0 +1,278 @@ +name: vc_vm_v3 +service_id: 28 +service_display_name: Виртуальная машина +service_short_name: vc_vm_v3 +service_man: '# Документация по разворачиванию ВМ в Личном кабинете

Данный сервис позволяет разворачивать виртуальные машины (ВМ) непосредственно из Личного кабинета (ЛК).

## Доступные операции
- **create** — Создание виртуальной машины
- **delete** — Удаление виртуальной машины
- **resume** — Запуск виртуальной машины
- **suspend** — Остановка виртуальной машины

---

## Параметры для развёртывания ВМ

### Обязательные параметры:

**vappUid**
UUID vApp, в которой будет создаваться виртуальная машина.
*Пример: WZ00400*

**vmName**
Уникальное имя виртуальной машины.
*Пример: test-vm*

**vmCpu**
Количество ядер процессора. Должно быть больше 0.
*Пример: 1*

**vmRam**
Объем оперативной памяти в гигабайтах. Должно быть больше 0.
*Пример: 1*


**accessPortList**
Белый список портов для доступа к виртуальной машине извне в формате JSON.
*Пример: ["22","10050]*

**imageVm**
Образ операционной системы для развёртывания.
*Доступные значения: RockyLinux_9-16G-cloudinit, Ubuntu_22-20G, Debian_13-20G*

**userLogin**
Логин пользователя для SSH-доступа.
*По умолчанию: myuser*

**userPublicKey**
Публичный SSH-ключ пользователя в формате OpenSSH.
*Пример: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC...*

**needAddZabbixTemplate**
Значение истина\ложь, определяющее будет ли добавлен хост в zabbix.
*Пример: true*

### Опциональные параметры:

**accessIpList**
Белый список IP-адресов для доступа к виртуальной машине в формате JSON. Если оставить пустым, вм будет доступена со всех адресов.
*Пример: ["192.168.1.1", "10.0.0.5"]*

**vmDisk**
Размер дополнительного диска в гигабайтах. Должно быть больше 0.
*Пример: 50*

**ipSpaceName**
Внешний IP-адрес. Если не требуется — укажите значение "no-needed" (В нем не будет работать сбор метрик zabbix).

**cloudInit**
YAML-скрипт для кастомизации системы через cloud-init.

---

## Доступные образы операционных систем

### RockyLinux_9-16G-cloudinit
Базовый образ на основе Rocky Linux 9 (Rocky-9.6-x86_64-minimal) для серверных задач.
- Размер системного диска: 16 ГБ
- Поддержка cloud-init
- Минимальные требования: 1 vCPU, 1 ГБ RAM

### Ubuntu_22-20G
Образ на основе Ubuntu Server 22.04 LTS для облачного окружения.
- Размер системного диска: 20 ГБ
- Поддержка cloud-init
- Минимальные требования: 1 vCPU, 1 ГБ RAM

### Debian_13-20G
Образ на основе Debian 13 (Trixie) для стабильной серверной работы.
- Размер системного диска: 20 ГБ
- Поддержка cloud-init
- Минимальные требования: 1 vCPU, 1 ГБ RAM

---

## Пример использования Cloud-init

```yaml
users:
- name: myuser
sudo: ALL=(ALL) NOPASSWD:ALL
groups: sudo
shell: /bin/bash
ssh-authorized-keys:
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC...

packages:
- nginx

runcmd:
- systemctl enable --now nginx
- systemctl restart nginx
```

---

## Результат выполнения

После успешного развёртывания в Личном кабинете появится новая виртуальная машина с заданными характеристиками. SSH-доступ осуществляется с использованием указанных логина и публичного ключа.

---

## Важные примечания

### Особенности работы:
- Для отключения внешнего IP укажите значение "no-needed" в параметре ipSpaceName
- Параметр cloudInit является опциональным

### Ограничения:
- Не рекомендуется использовать продолжительные инструкции в cloud-init (таймаут 5 минут)
- Не поддерживается запись файлов через cloud-init
- Не реализовано создание резервных копий ВМ

---

## Диагностика проблем с cloud-init

### Основной лог-файл:
```bash
/var/log/cloud-init.log
```

### Пакет не устанавливается:
- Проверьте доступность пакета в репозитории
- Убедитесь в правильности написания имени пакета
- Попробуйте установить вручную: `apt install <имя_пакета>` (Debian/Ubuntu) или `dnf install <имя_пакета>` (Rocky Linux)

### Команда не выполняется:
- Проверьте наличие требуемой утилиты в системе
- Убедитесь в правильности синтаксиса команды
- Попробуйте выполнить команду вручную для диагностики' +lifecycle: + suspend_on_destroy_default: true + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 132 + kind: instance + action: create + man: '# Документация по использованию услуги развёртывания ВМ в Личном кабинете

Данный сервис позволяет разворачивать виртуальные машины (ВМ) непосредственно из Личного кабинета (ЛК).

## Доступные операции
- **create** — Создание виртуальной машины
- **delete** — Удаление виртуальной машины
- **resume** — Запуск виртуальной машины
- **suspend** — Остановка виртуальной машины

---

## Параметры для развёртывания ВМ

### Обязательные параметры:

**vappUid**
UUID vApp, в которой будет создаваться виртуальная машина.
*Пример: WZ00400*

**vmName**
Уникальное имя виртуальной машины.
*Пример: test-vm*

**vmCpu**
Количество ядер процессора. Должно быть больше 0.
*Пример: 1*

**vmRam**
Объем оперативной памяти в гигабайтах. Должно быть больше 0.
*Пример: 1*


**accessPortList**
Белый список портов для доступа к виртуальной машине извне в формате JSON.
*Пример: ["22","10050]*

**imageVm**
Образ операционной системы для развёртывания.
*Доступные значения: RockyLinux_9-16G-cloudinit, Ubuntu_22-20G, Debian_13-20G*

**userLogin**
Логин пользователя для SSH-доступа.
*По умолчанию: myuser*

**userPublicKey**
Публичный SSH-ключ пользователя в формате OpenSSH.
*Пример: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC...*

**needAddZabbixTemplate**
Значение истина\лож определяющее будет ли добавлен хост в zabbix.
*Пример: true*

### Опциональные параметры:

**accessIpList**
Белый список IP-адресов для доступа к виртуальной машине в формате JSON. Если оставить пустым, вм будет доступена со всех адресов.
*Пример: ["192.168.1.1", "10.0.0.5"]*

**vmDisk**
Размер дополнительного диска в гигабайтах. Должно быть больше 0.
*Пример: 50*

**ipSpaceName**
Внешний IP-адрес. Если не требуется — укажите значение "no-needed".

**cloudInit**
YAML-скрипт для кастомизации системы через cloud-init.

---

## Доступные образы операционных систем

### RockyLinux_9-16G-cloudinit
Базовый образ на основе Rocky Linux 9 (Rocky-9.6-x86_64-minimal) для серверных задач.
- Размер системного диска: 16 ГБ
- Поддержка cloud-init
- Минимальные требования: 1 vCPU, 1 ГБ RAM

### Ubuntu_22-20G
Образ на основе Ubuntu Server 22.04 LTS для облачного окружения.
- Размер системного диска: 20 ГБ
- Поддержка cloud-init
- Минимальные требования: 1 vCPU, 1 ГБ RAM

### Debian_13-20G
Образ на основе Debian 13 (Trixie) для стабильной серверной работы.
- Размер системного диска: 20 ГБ
- Поддержка cloud-init
- Минимальные требования: 1 vCPU, 1 ГБ RAM

---

## Пример использования Cloud-init

```yaml
users:
- name: myuser
sudo: ALL=(ALL) NOPASSWD:ALL
groups: sudo
shell: /bin/bash
ssh-authorized-keys:
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC...

packages:
- nginx

runcmd:
- systemctl enable --now nginx
- systemctl restart nginx
```

---

## Результат выполнения

После успешного развёртывания в Личном кабинете появится новая виртуальная машина с заданными характеристиками. SSH-доступ осуществляется с использованием указанных логина и публичного ключа.

---

## Важные примечания

### Особенности работы:
- Для отключения внешнего IP укажите значение "no-needed" в параметре ipSpaceName
- Параметр cloudInit является опциональным

### Ограничения:
- Не рекомендуется использовать продолжительные инструкции в cloud-init (таймаут 5 минут)
- Не поддерживается запись файлов через cloud-init
- Не реализовано создание резервных копий ВМ

---

## Диагностика проблем с cloud-init

### Основной лог-файл:
```bash
/var/log/cloud-init.log
```

### Пакет не устанавливается:
- Проверьте доступность пакета в репозитории
- Убедитесь в правильности написания имени пакета
- Попробуйте установить вручную: `apt install <имя_пакета>` (Debian/Ubuntu) или `dnf install <имя_пакета>` (Rocky Linux)

### Команда не выполняется:
- Проверьте наличие требуемой утилиты в системе
- Убедитесь в правильности синтаксиса команды
- Попробуйте выполнить команду вручную для диагностики' + params: + - id: 407 + code: vappUid + data_type: uuid + required: true + ref_svc_id: 26 + sort: 10 + - id: 408 + code: vmName + data_type: string + required: true + default: web01 + regex: ^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$ + sort: 20 + - id: 409 + code: vmCpu + data_type: integer > 0 + required: true + default: "1" + maxvalue: 64 + minvalue: 1 + descr: шт + sort: 30 + is_modifiable: true + - id: 410 + code: vmRam + data_type: integer > 0 + required: true + default: "1" + maxvalue: 256 + minvalue: 1 + descr: Gb + sort: 40 + is_modifiable: true + - id: 411 + code: vmDisk + data_type: integer > 0 + required: false + regex: ^(?:$|(?:0|[1-9]\d{0,2}|1\d{3}|20(?:[0-3]\d|4[0-8])))$ + descr: Основной диск у vm зависит от образа (см. документацию)
Если необходимо дополнительное место для vm, введите колличество Gb в эту форму + sort: 50 + is_modifiable: true + - id: 412 + code: ipSpaceName + data_type: string + required: true + sort: 60 + depends_on: vappUid + is_modifiable: true + - id: 413 + code: accessIpList + data_type: json + required: false + regex: ^(?:$|\[\s*(?:"(?:\d{1,3}\.){3}\d{1,3}(?:\/\d{1,2})?"(?:\s*,\s*"(?:\d{1,3}\.){3}\d{1,3}(?:\/\d{1,2})?")*)\s*\])$ + descr: Формат массива
Каким адресам будет предоставлен доступ до вм

Требует выделенного внешнего IP
Если ничего не передано, то выставлено ["0.0.0.0/0"] + sort: 70 + is_modifiable: true + - id: 414 + code: imageVm + data_type: string + required: true + sort: 90 + depends_on: vappUid + - id: 415 + code: cloudInit + data_type: yaml + required: false + descr: Можно передать создание дополнительных пользователей.
На текущий момент не реализовано выполнения в cloud-init, установки пакетов, выполнения команд и записи файлов. + sort: 200 + - id: 416 + code: userLogin + required: true + default: myuser + descr: Учётка SSH + sort: 180 + - id: 417 + code: userPublicKey + data_type: string + required: true + descr: Публичная часть SSH-Ключа для пользователя + sort: 190 + - id: 448 + code: accessPortList + data_type: string + required: true + default: '["22"]' + descr: 'По каким портам открыть доступ к ВМ.
Порты 22, 10050 проставляются автоматически
Пока доступно:
22 - SSH,
80 - HTTP,
443 - HTTPS,
10050 - ZABBIX' + sort: 80 + is_modifiable: true + - name: delete + id: 133 + kind: instance + action: delete + params: [] + - name: modify + id: 159 + kind: instance + action: modify + params: + - id: 493 + code: vmCpu + data_type: integer > 0 + required: false + maxvalue: 64 + minvalue: 1 + descr: шт + sort: 20 + - id: 494 + code: vmRam + data_type: integer > 0 + required: false + maxvalue: 256 + minvalue: 1 + descr: Gb + sort: 30 + - id: 495 + code: vmDisk + data_type: integer > 0 + required: false + regex: ^(?:$|(?:0|[1-9]\d{0,2}|1\d{3}|20(?:[0-3]\d|4[0-8])))$ + descr: Основной диск у vm зависит от образа (см. документацию)
Если необходимо дополнительное место для vm, введите колличество Gb в эту форму + sort: 40 + - id: 496 + code: ipSpaceName + data_type: string + required: false + value_list: + - no-needed + - internet-antiddos-v1 + - internet-no-antiddos-v1 + - internet-ipv4-v1 + descr: no-needed, без внешнего ip
internet-antiddos-v1, с функцией защиты от ddos атак
internet-no-antiddos-v1, с функцией защиты от ddos атак
internet-ipv4-v1 тестовое простанство dev + sort: 50 + - id: 497 + code: accessIpList + data_type: json + required: false + descr: Формат массива
Каким адресам будет предоставлен доступ до вм

Требует выделенного внешнего IP
Если ничего не передано, то выставлено ["0.0.0.0/0"] + sort: 60 + - id: 498 + code: accessPortList + data_type: json + required: false + descr: Формат массива
По каким портам открыть доступ к ВМ.
Пока доступно:
22 - SSH,
80 - HTTP,
443 - HTTPS,
10050 - ZABBIX + sort: 70 + - name: reconcile + id: 286 + kind: action + action: reconcile + params: [] + - name: redeploy + id: 164 + kind: action + action: redeploy + params: + - id: 509 + code: vmName + data_type: string + required: true + regex: ^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$ + sort: 10 + - id: 510 + code: vmCpu + data_type: integer > 0 + required: true + maxvalue: 64 + minvalue: 1 + descr: шт + sort: 20 + - id: 511 + code: vmRam + data_type: integer > 0 + required: true + maxvalue: 256 + minvalue: 1 + descr: Gb + sort: 30 + - id: 512 + code: vmDisk + data_type: integer > 0 + required: false + regex: ^(?:$|(?:0|[1-9]\d{0,2}|1\d{3}|20(?:[0-3]\d|4[0-8])))$ + descr: Основной диск у vm зависит от образа (см. документацию)
Если необходимо дополнительное место для vm, введите колличество Gb в эту форму + sort: 40 + - id: 513 + code: ipSpaceName + data_type: string + required: false + value_list: + - no-needed + - internet-antiddos-v1 + - internet-no-antiddos-v1 + - internet-ipv4-v1 + descr: no-needed, без внешнего ip
internet-antiddos-v1, с функцией защиты от ddos атак
internet-no-antiddos-v1, с функцией защиты от ddos атак
internet-ipv4-v1 тестовое простанство dev + sort: 50 + - id: 514 + code: accessIpList + data_type: json + required: false + regex: ^(?:$|\[\s*(?:"(?:\d{1,3}\.){3}\d{1,3}(?:\/\d{1,2})?"(?:\s*,\s*"(?:\d{1,3}\.){3}\d{1,3}(?:\/\d{1,2})?")*)\s*\])$ + descr: Формат массива
Каким адресам будет предоставлен доступ до вм

Требует выделенного внешнего IP
Если ничего не передано, то выставлено ["0.0.0.0/0"] + sort: 60 + - id: 515 + code: imageVm + data_type: string + required: true + value_list: + - RockyLinux_9-16G-cloudinit + - Ubuntu_22-20G + - Debian_13-20G + sort: 80 + - id: 516 + code: userLogin + data_type: string + required: true + descr: Учётка SSH + sort: 180 + - id: 517 + code: userPublicKey + data_type: string + required: true + descr: Публичная часть SSH-Ключа для пользователя + sort: 190 + - id: 518 + code: cloudInit + data_type: yaml + required: false + descr: Можно передать создание дополнительных пользователей.
На текущий момент не реализовано выполнения в cloud-init, установки пакетов, выполнения команд и записи файлов. + sort: 200 + - id: 520 + code: accessPortList + data_type: json + required: false + descr: По каким портам открыть доступ к ВМ.
Порты 22, 10050 проставляются автоматически
Пока доступно:
22 - SSH,
80 - HTTP,
443 - HTTPS,
10050 - ZABBIX + sort: 70 + - name: resume + id: 135 + kind: instance + action: resume + params: [] + - name: suspend + id: 134 + kind: instance + action: suspend + params: [] diff --git a/universal_rebuild/resources_yaml/29_vc_vdc_group.yaml b/universal_rebuild/resources_yaml/29_vc_vdc_group.yaml new file mode 100644 index 0000000..21afd35 --- /dev/null +++ b/universal_rebuild/resources_yaml/29_vc_vdc_group.yaml @@ -0,0 +1,74 @@ +name: vc_vdc_group +service_id: 29 +service_display_name: Группа датацентров +service_short_name: vcVdcGroup +service_man: '# Инструкция по развертыванию Группа виртуальных датацентров (groupvDC) через платформу

## 1. Общая информация

Сервис **Группа виртуальных датацентров (groupvDC)** предназначен для логического объединения нескольких виртуальных датацентров (vDC) в единую группу. Это позволяет растягивать SDN между разными vDC и централизованно управлять сетевой связностью. Управление инфраструктурой осуществляется через Cloud Director.
Сервис применяется в случаях, когда требуется обеспечить единое сетевое пространство для распределенных виртуальных датацентров.

### Доступные операции

**create** — Создание новой группы виртуальных датацентров.
**delete** — Удаление существующей группы vDC.
**add_vdc** — Добавление дополнительного виртуального датацентра в группу.
**remove_vdc** — Удаление ранее добавленного виртуального датацентра из группы.

---

## 2. Параметры развертывания

Параметры операции **create**:

### UUID VDC
Идентификатор виртуального датацентра, который будет являться первичным в группе.
Описание: требуется предварительно создать виртуальный датацентр с помощью услуги `Виртуальный датацентр (vDC)`.

---

## 3. Рекомендованные характеристики

### Тестовое окружение
- Для тестового окружения создавать группу VDC нет необходимости, так как это создаёт дополнительный слой абстракции в Cloud Director

### Промышленное окружение
- Для промышленного окружения, если в тенанте существуют разные VDC, создание groupvdc позволит снизить сетевые издержки (за счёт общей сети)

---

## 4. Выходные параметры

### Параметры groupvdc
Набор параметров, указанных пользователем при создании

---

## 5. Дополнительная информация
- Группа vDC используется для растягивания SDN между несколькими виртуальными датацентрами. Управление всеми сетевыми элементами внутри группы выполняется Cloud Director.
- Удалить первичный vDC (тот, что был указан при операции **create**) невозможно. Для его удаления необходимо удалить всю группу и создать новую.
- Перед добавлением любого vDC в группу он должен быть предварительно развернут через сервис `Виртуальный датацентр (vDC)`.
- Сети создаются не в этой услуге, а в услуге `Сетевой шлюз периметра (Edge)`. В ней необходимо будет выбрать тип `groupvdc`' +lifecycle: + suspend_on_destroy_default: false + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: add_vdc + id: 192 + kind: subresource + action: add + subresource: vdc + man: '**Не нужно** выбирать UUID VDC, который уже был добавлен в группу' + params: + - id: 619 + code: vdcUid + data_type: uuid + required: true + ref_svc_id: 21 + sort: 10 + - name: create + id: 191 + kind: instance + action: create + man: '**VDC Группа** нужна для объединения всех VDC в организации в одну логическую группу.
Это позволит, к примеру, создать **общую сеть** для всех VDC в организации

- Для создания группы VDC необходимо указать **инстанс** услуги VDC (`Виртуальный датацентр`)
- **Важный момент** - удалить эту VDC из группы нельзя. Если такая потребность появится, необходимо будет **ПОЛНОСТЬЮ** удалить группу VDC и уже потом создать новую' + params: + - id: 618 + code: vdcUid + data_type: uuid + required: true + ref_svc_id: 21 + sort: 10 + - name: delete + id: 195 + kind: instance + action: delete + params: [] + - name: reconcile + id: 251 + kind: action + action: reconcile + params: [] + - name: remove_vdc + id: 193 + kind: subresource + action: remove + subresource: vdc + params: + - id: 620 + code: vdcName + data_type: string + required: true + sort: 10 diff --git a/universal_rebuild/resources_yaml/2_template.yaml b/universal_rebuild/resources_yaml/2_template.yaml new file mode 100644 index 0000000..601652a --- /dev/null +++ b/universal_rebuild/resources_yaml/2_template.yaml @@ -0,0 +1,81 @@ +name: template +service_id: 2 +service_display_name: Темплейт k8s +service_short_name: template +lifecycle: + suspend_on_destroy_default: true + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 74 + kind: instance + action: create + params: + - id: 813 + code: clusterConfiguration + data_type: map-fixed + required: true + sort: 20 + is_modifiable: true + - id: 814 + code: startupConfiguration + data_type: map-fixed + required: true + sort: 10 + - id: 815 + code: accessConfiguration + data_type: map-fixed + required: true + is_modifiable: true + - name: delete + id: 75 + kind: instance + action: delete + params: [] + - name: modify + id: 76 + kind: instance + action: modify + params: + - id: 816 + code: clusterConfiguration + data_type: map-fixed + required: true + - id: 817 + code: accessConfiguration + data_type: map-fixed + required: true + - name: reconcile + id: 298 + kind: action + action: reconcile + params: [] + - name: resume + id: 98 + kind: instance + action: resume + params: [] + - name: suspend + id: 97 + kind: instance + action: suspend + params: [] diff --git a/universal_rebuild/resources_yaml/50_nextcloud.yaml b/universal_rebuild/resources_yaml/50_nextcloud.yaml new file mode 100644 index 0000000..665cf0b --- /dev/null +++ b/universal_rebuild/resources_yaml/50_nextcloud.yaml @@ -0,0 +1,135 @@ +name: nextcloud +service_id: 50 +service_display_name: Nextcloud +service_short_name: nextcloud +service_man: 'Ресурсная платформа: k8s. Автоматизация: helm chart' +lifecycle: + suspend_on_destroy_default: true + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 16 + kind: instance + action: create + man: Для установки необходимо:
- ресурсная платформа, где разворачивается Nextcloud, обязана совпадать с ресурсными платформами, где развёрнут Redis и PostgreSQL + params: + - id: 1055 + code: startupConfiguration + data_type: map-fixed + required: true + sort: 10 + - id: 1056 + code: clusterConfiguration + data_type: map-fixed + required: true + sort: 20 + is_modifiable: true + - id: 1057 + code: accessConfiguration + data_type: map-fixed + required: true + sort: 30 + - id: 1058 + code: appConfiguration + data_type: map-fixed + required: true + sort: 40 + is_modifiable: true + - name: create_backup + id: 404 + kind: subresource + action: create + subresource: backup + man: Перед запуском обязательно надо перевести экземпляр Nextcloud в статус 'suspended' + params: + - id: 1077 + code: startupConfiguration + data_type: map-fixed + required: true + sort: 10 + - name: delete + id: 17 + kind: instance + action: delete + man: DEV-Ветка удаления Nextcloud + params: [] + - name: modify + id: 67 + kind: instance + action: modify + params: + - id: 1059 + code: clusterConfiguration + data_type: map-fixed + required: true + sort: 10 + - id: 1060 + code: appConfiguration + data_type: map-fixed + required: true + sort: 20 + - name: reconcile + id: 390 + kind: action + action: reconcile + params: [] + - name: recovery + id: 406 + kind: action + action: recovery + params: + - id: 1078 + code: startupConfiguration + data_type: map-fixed + required: true + sort: 10 + - id: 1079 + code: clusterConfiguration + data_type: map-fixed + required: true + sort: 20 + - id: 1080 + code: accessConfiguration + data_type: map-fixed + required: true + sort: 30 + - id: 1081 + code: appConfiguration + data_type: map-fixed + required: true + man: Версия Nextcloud будет взята из бэкапа + sort: 40 + - name: restart + id: 66 + kind: action + action: restart + params: [] + - name: resume + id: 129 + kind: instance + action: resume + params: [] + - name: suspend + id: 128 + kind: instance + action: suspend + params: [] diff --git a/universal_rebuild/resources_yaml/81_superset.yaml b/universal_rebuild/resources_yaml/81_superset.yaml new file mode 100644 index 0000000..e935cbe --- /dev/null +++ b/universal_rebuild/resources_yaml/81_superset.yaml @@ -0,0 +1,104 @@ +name: superset +service_id: 81 +service_display_name: Apache Superset +service_short_name: superset +service_man: 'Apache Superset как сервис.
Ресурсная платформа: k8s.
Автоматизация: k8s operator.
Авторизация: 2FA keycloak (для доменного логина @mgmt.nubes.ru)' +lifecycle: + suspend_on_destroy_default: false + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 12 + kind: instance + action: create + man: DEV-Ветка сборки Superset + params: + - id: 14 + code: domain + data_type: string + required: true + default: web01 + regex: ^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$ + descr: Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm + man: Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm + sort: 10 + - id: 20 + code: emails + data_type: string + required: true + default: testuser1 + descr: Учётная запись, которой будут выданы админские права. В формате email [ну не совсем. Для доменных учетных записей - без доменного суффикса]. Обязана находиться в Keycloak. Можно передавать несколько через пробел + man: Учётная запись, которой будут выданы админские права. В формате email [ну не совсем. Для доменных учетных записей - без доменного суффикса]. Обязана находиться в Keycloak. Можно передавать несколько через пробел + sort: 70 + - id: 96 + code: resourceCPU + data_type: integer > 0 + required: true + default: "500" + maxvalue: 4000 + minvalue: 100 + descr: Квота ядра пода (Указывается в mili). 1000 Mili = 1 ядро + man: Квота ядра пода (Указывается в mili). 1000 Mili = 1 ядро + sort: 30 + - id: 97 + code: resourceMemory + data_type: integer > 0 + required: true + default: "512" + maxvalue: 8192 + minvalue: 128 + descr: Квота памяти пода (Указывается в Megabytes) + man: Квота памяти пода (Указывается в Megabytes) + sort: 40 + - id: 98 + code: resourceDisk + data_type: integer > 0 + required: true + default: "1" + maxvalue: 999 + minvalue: 1 + descr: Квота диска (Указывается в GIGAbytes) + man: Квота диска (Указывается в GIGAbytes) + sort: 50 + - id: 109 + code: resourceRealm + data_type: string + required: true + func: getAvailableResourceRealms + descr: Платформа для развертывания + man: Наименование кластера Kubernetes для подключения + sort: 20 + - id: 227 + code: resourceInstances + data_type: integer > 0 + required: true + default: "1" + maxvalue: 7 + minvalue: 1 + descr: Количество узлов + sort: 60 + - name: delete + id: 13 + kind: instance + action: delete + man: DEV-Ветка удаления Superset + params: [] diff --git a/universal_rebuild/resources_yaml/82_harbor.yaml b/universal_rebuild/resources_yaml/82_harbor.yaml new file mode 100644 index 0000000..e082c02 --- /dev/null +++ b/universal_rebuild/resources_yaml/82_harbor.yaml @@ -0,0 +1,95 @@ +name: harbor +service_id: 82 +service_display_name: Container Registry +service_short_name: harbor +service_man: 'Расширенный реестр для управления Docker-образами с enterprise-функциями безопасности, репликации и контроля доступа.
Container Registry позволяет хранить, развертывать и управлять Docker-образами контейнеров в инфраструктуре NGcloud.
Сервис можно использовать в процессе разработки контейнеров. Например, вы можете настроить систему автоматизации на работу с реестром в Container Registry в рамках процесса непрерывной интеграции. Контейнеры идеально подходят для запуска небольших, независимых и легко изменяемых сервисов.
Чтобы начать работу, создайте реестр и загрузите в него Docker-образ
Авторизация: keycloak' +lifecycle: + suspend_on_destroy_default: false + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 14 + kind: instance + action: create + man: DEV-Ветка сборки Harbor + params: + - id: 16 + code: domain + data_type: string + required: true + default: web01 + regex: ^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$ + descr: Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm + man: Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm + sort: 20 + - id: 110 + code: resourceRealm + data_type: string + required: true + func: getAvailableResourceRealms + descr: Платформа для развертывания + man: Наименование кластера Kubernetes для подключения + sort: 30 + - id: 228 + code: resourceCPU + data_type: integer > 0 + required: true + default: "100" + maxvalue: 4000 + minvalue: 100 + descr: Квота ядра пода (Указывается в mili). 1000Mili = 1 Ядро + man: Квота ядра пода (Указывается в mili). 1000Mili = 1 Ядро + sort: 40 + - id: 229 + code: resourceMemory + data_type: integer > 0 + required: true + default: "200" + maxvalue: 8192 + minvalue: 128 + descr: Квота памяти пода (Указывается в Megabytes) + man: Квота памяти пода (Указывается в Megabytes) + sort: 50 + - id: 230 + code: resourceInstances + data_type: integer > 0 + required: true + default: "1" + maxvalue: 7 + minvalue: 1 + descr: Количество узлов + man: Количество узлов в сети + sort: 60 + - id: 231 + code: s3Uid + data_type: uuid + required: true + ref_svc_id: 12 + descr: UUID экземпляра услуги S3 + man: UUID экземпляра услуги S3 + sort: 10 + - name: delete + id: 15 + kind: instance + action: delete + man: DEV-Ветка удаления Harbor + params: [] diff --git a/universal_rebuild/resources_yaml/88_k8s_ziti_controller.yaml b/universal_rebuild/resources_yaml/88_k8s_ziti_controller.yaml new file mode 100644 index 0000000..7270f20 --- /dev/null +++ b/universal_rebuild/resources_yaml/88_k8s_ziti_controller.yaml @@ -0,0 +1,90 @@ +name: k8s_ziti_controller +service_id: 88 +service_display_name: k8sZitiController +service_short_name: k8sZitiController +lifecycle: + suspend_on_destroy_default: false + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 99 + kind: instance + action: create + params: + - id: 294 + code: resourceCPU + data_type: integer > 0 + required: true + default: "300" + maxvalue: 4000 + minvalue: 100 + descr: Квота ядра пода (milicores) + sort: 30 + - id: 295 + code: resourceMemory + data_type: integer > 0 + required: true + default: "512" + maxvalue: 8192 + minvalue: 128 + descr: Квота памяти пода (MBytes) + sort: 40 + - id: 296 + code: resourceDisk + data_type: integer > 0 + required: true + default: "1" + maxvalue: 999 + minvalue: 1 + descr: Квота диска (Указывается в GIGAbytes) + man: Квота диска (Указывается в GIGAbytes) + sort: 50 + - id: 297 + code: resourceInstances + data_type: integer > 0 + required: true + default: "1" + maxvalue: 7 + minvalue: 1 + descr: Количество узлов + sort: 60 + - id: 298 + code: resourceRealm + data_type: string + required: true + func: getAvailableResourceRealms + descr: Платформа для развертывания + sort: 20 + - id: 299 + code: domain + data_type: string + required: true + default: web01 + regex: ^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$ + unique_scope: provider + descr: Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm + sort: 10 + - name: delete + id: 105 + kind: instance + action: delete + params: [] diff --git a/universal_rebuild/resources_yaml/89_flask.yaml b/universal_rebuild/resources_yaml/89_flask.yaml new file mode 100644 index 0000000..c1fa5e9 --- /dev/null +++ b/universal_rebuild/resources_yaml/89_flask.yaml @@ -0,0 +1,112 @@ +name: flask +service_id: 89 +service_display_name: Web-сервер с фреймворком Flask +service_short_name: flask +service_man: '# Инструкция по развертыванию Web-сервер с фреймворком Flask через платформу

## 1. Общая информация

Сервис **Web-сервер с фреймворком Flask** предназначен для развертывания веб-приложений на базе Flask.

### Доступные операции

create — включить сервис, развернуть веб-приложение с указанными параметрами.
delete — удалить сервис, только после suspend.
modify — изменить настройки сервиса и переразвернуть.
suspend — выключить веб-приложение, остановить обслуживание.
resume — включить веб-приложение после suspend.

---

## 2. Параметры развертывания

Параметры входных данных операции **create**:

### Имя домена
Описание: доменное имя для доступа к веб-приложению. Если указан FQDN, происходит проверка DNS на валидность А-записи. А-запись должна указывать на балансировщик ресурсной платформы. Если указано имя в рамках одного октета, оно будет добавлено в публичную DNS-зону.
Пример значения: строка, допустимые FQDN или имя в одном октете. Пример: example.com или myapp.

### Платформа для развертывания
Описание: кластер Kubernetes, в котором будет развернуто приложение.
Пример значения: идентификатор кластера Kubernetes.

### Квота ядра пода
Описание: количество CPU, выделяемое на под, в Milicores.
Пример значения: целое число, например 500 для 0.5 ядра.

### Квота памяти пода
Описание: количество памяти, выделяемое на под, в Megabytes.
Пример значения: целое число, например 512 для 512 MB.

### Кол-во реплик пода
Описание: количество инстансов пода для масштабирования.
Пример значения: целое число, шт, например 2.

### URL Проекта (gitpath)
Описание: полный HTTPS путь до проекта в Git-репозитории.
Пример значения: URL в формате https://github.com/myuser/myproject.git. Доступ к репозиторию должен быть публичным.

### URL HealthCheck
Описание: путь для проверки здоровья приложения внутри пода.
Пример значения: строка, путь, например /api/v1/health.

### Переменные окружения пода
Описание: структура данных, которая будет передана как переменные окружения в под.
Пример значения: ключ-значение, например {"ENV": "production"}.

---

## 3. Рекомендованные характеристики

### Тестовое окружение
- Квота ядра пода: 300m (0.3 ядра)
- Квота памяти пода: 256 MB
- Кол-во реплик пода: 1

### Промышленное окружение
- Квота ядра пода: от 1000m (1 ядро) в зависимости от нагрузки
- Квота памяти пода: от 1024 MB (1 GB) с учетом потребностей приложения
- Кол-во реплик пода: от 2 для обеспечения отказоустойчивости

---

## 4. Выходные параметры
Операция create не возвращает явных выходных параметров. Успешное выполнение операции приводит к развертыванию сервиса, доступного по указанному домену.

---

## 5. Дополнительная информация
- В корне проекта должен находиться файл requirements.txt со списком зависимостей Python.
- Исходный код веб-приложения должен располагаться в директории site.
- Запуск веб-сервера осуществляется командой python app.py.
- Используется версия Python: python:3.12-slim.
- Git-репозиторий должен быть публично доступен для платформы.
- Операция delete возможна только после suspend.
- Операция modify приводит к переразвертыванию с новыми параметрами.' +lifecycle: + suspend_on_destroy_default: true + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 376 + kind: instance + action: create + params: + - id: 1037 + code: startupConfiguration + data_type: map-fixed + required: true + sort: 10 + - id: 1038 + code: clusterConfiguration + data_type: map-fixed + required: true + sort: 20 + is_modifiable: true + - id: 1039 + code: accessConfiguration + data_type: map-fixed + required: true + sort: 30 + - id: 1040 + code: appConfiguration + data_type: map-fixed + required: true + sort: 40 + is_modifiable: true + - id: 1041 + code: jsonEnv + data_type: map + required: false + default: '{}' + sort: 50 + is_modifiable: true + - name: delete + id: 379 + kind: instance + action: delete + params: [] + - name: modify + id: 375 + kind: instance + action: modify + params: + - id: 1042 + code: clusterConfiguration + data_type: map-fixed + required: true + sort: 10 + - id: 1043 + code: appConfiguration + data_type: map-fixed + required: true + sort: 20 + - id: 1044 + code: jsonEnv + data_type: map + required: false + sort: 30 + - name: reconcile + id: 381 + kind: action + action: reconcile + params: [] + - name: redeploy + id: 380 + kind: action + action: redeploy + params: + - id: 1045 + code: startupConfiguration + data_type: map-fixed + required: true + sort: 10 + - name: resume + id: 378 + kind: instance + action: resume + params: [] + - name: suspend + id: 377 + kind: instance + action: suspend + params: [] diff --git a/universal_rebuild/resources_yaml/90_postgres.yaml b/universal_rebuild/resources_yaml/90_postgres.yaml new file mode 100644 index 0000000..4a5f60e --- /dev/null +++ b/universal_rebuild/resources_yaml/90_postgres.yaml @@ -0,0 +1,250 @@ +name: postgres +service_id: 90 +service_display_name: PostgreSQL +service_short_name: postgres +service_man: '# Инструкция по развертыванию PostgreSQL через платформу

---
## 1. Общая информация

PostgreSQL — это надёжная объектно-реляционная СУБД с открытым исходным кодом, поддерживающая расширенный набор функций, транзакции и хранение сложных типов данных. Развёртывание полностью автоматизировано платформой на базе Zalando Postgres Operator с использованием Kubernetes.

## Доступные операции

- **create** — Создание кластера
- **delete** — Удаление кластера
- **modify** — Модификация кластера
- **suspend** — Остановка кластера
- **resume** — Запуск кластера
- **restart** — Перезапуск кластера
- **recovery** — Восстановление из резервной копии

- **create_user** — Создание пользователя
- **delete_user** — Удаление пользователя

- **create_database** — Создание базы данных
- **delete_database** — Удаление базы данных


---
## 2. Параметры развертывания кластера

Ниже перечислены параметры, которые необходимо указать при создании ресурса PostgreSQL:

* **Платформа для развертывания:**
Платформа для развертывания (среда, в которой будет развернут кластер). Пример: `k8s-3.ext.nubes.ru`.

* **Количество узлов в сети:**
Количество инстансов (реплик) в кластере PostgreSQL. Пример: `2` (master + slave).

* **Квота (megabyte) памяти пода:**
Квота памяти пода в мегабайтах. Пример: `2048`

* **Квота (millicore) ядра пода:**
Квота CPU пода в миликорах (1000 mCPU = 1 vCPU). Пример: `1000`

* **Размер (gigabyte) диска для базы данных:**
Размер диска для хранения данных в гигабайтах. Пример: `20`

* **Выделяет внешний IP для доступа к master:**
Флаг, определяющий, нужно ли выделять внешний IP для доступа к master. Пример: `true` или `false`

* **Имя ipSpace для публикации внешнего IP master:**
Если включен предыдущий параметр, то поле необходимо заполнить. Пример: `internet-no-antiddos-v1`

* **Выделяет внешний IP для доступа к slave:**
Флаг для выделения внешнего IP для slave (реплики чтения). Работает, если slave присутствует в кластере.

* **Имя ipSpace для публикации внешнего IP slave:**
Если включен параметр для slave, то поле необходимо заполнить.

* **UUID экземпляра услуги S3:**
UUID экземпляра S3 для хранения резервных копий.

* **Версия PostgreSQL:**
Версия PostgreSQL для развертывания. Пример: `16`

* **Расписание бэкапов (ext_BACKUP_SCHEDULE):**
Необязательный. Cron-выражение для времени запуска бэкапов. По умолчанию: `0 0 * * *` (ежедневно в полночь).

* **Количество сохраняемых бэкапов (ext_BACKUP_NUM_TO_RETAIN):**
Необязательный. Количество сохраняемых бэкапов. По умолчанию: `7`

* **PgPooler (master / slave):**
Флаги включения PgBouncer для подключений на запись и чтение. При включении создаётся отдельный под.

* **Разрешить подключение без SSL (allowNoSSL):**
По умолчанию подключение с sslmode=require. При `true` разрешается подключение без SSL.

* **Автоскейлинг PV (autoScale):**
Включает автоскейлинг тома. Требует настройки autoScalePercentage, autoScaleTechWindow, autoScaleQuotaGb.

* **Дополнительные параметры PostgreSQL (jsonParameters):**
JSON с переменными конфигурации PostgreSQL. Документация: https://www.postgresql.org/docs/current/runtime-config.html

---
## 3. Выходные параметры

После успешного развертывания ресурса PostgreSQL платформа возвращает ряд параметров.

#### externalConnect

Внешние эндпоинты для подключения к кластеру:
- **master** — для операций записи (host: `write.${namespace}`, port: 5432)
- **slave** — для операций чтения (host: `read.${namespace}`, port: 5432)

Пример ответа платформы:

```json
{
"slave": {
"ip": "",
"fqdn": "",
"uuid": "",
"isExternal": false
},
"master": {
"ip": "<ip>",
"fqdn": "write.<namespace>.<cluster-domain>",
"uuid": "<uuid>",
"isExternal": false
}
}
```

Поля: `ip` — внешний IP; `fqdn` — полное имя хоста; `uuid` — идентификатор; `isExternal` — признак внешнего доступа.

#### internalConnect

Внутренние эндпоинты (FQDN) для подключения из других сервисов внутри той же сети Kubernetes.

Пример ответа платформы:

```json
{
"slave": "",
"master": {
"slave": "",
"master": {
"slave": "",
"master": "postgresqlk8s-master.<namespace>.svc.cluster.local"
}
}
}
```

Для подключения используйте значение `master.master.master` (host для записи) или соответствующее значение slave (host для чтения).

Для подключения используется порт **5432**.

---
## 4. Управление пользователями

### create_user — Создание пользователя

* **Имя пользователя (username):**
Имя создаваемого пользователя. Используется для подключения к базе и настройки прав.
Пример: `myapp_user`

* **Роль (role):**
Тип роли пользователя. Допустимые значения:
- `app_user` — базовые права для DML-операций (SELECT, INSERT, UPDATE, DELETE)
- `ddl_user` — расширенные права: createdb, createrole (управление базами и ролями)

Защищённые имена, которые нельзя использовать: `admin`, `postgres`, `standby`.

### delete_user — Удаление пользователя

* **Имя пользователя (username):**
Имя удаляемого пользователя. Пример: `myapp_user`

Пользователя нельзя удалить, если он является владельцем базы данных. Сначала удалите или передайте владение базами данных другому пользователю.

---
## 5. Управление базами данных

### create_database — Создание базы данных

* **Имя базы данных (dbName):**
Название создаваемой базы данных. Пример: `myapp_db`

* **Владелец базы данных (dbOwner):**
Имя пользователя-владельца базы. Должен существовать в кластере (создан через create_user). Пример: `myapp_user`

### delete_database — Удаление базы данных

* **Имя базы данных (dbName):**
Название удаляемой базы данных. Пример: `myapp_db`
' +lifecycle: + suspend_on_destroy_default: true + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 19 + kind: instance + action: create + man: руководство + params: + - id: 788 + code: clusterConfiguration + data_type: map-fixed + required: true + sort: 20 + is_modifiable: true + - id: 789 + code: startupConfiguration + data_type: map-fixed + required: true + sort: 10 + - id: 790 + code: accessConfiguration + data_type: map-fixed + required: true + sort: 30 + is_modifiable: true + - id: 791 + code: postgresConfiguration + data_type: map-fixed + required: true + sort: 40 + is_modifiable: true + - id: 792 + code: postgresConf + data_type: array-map-fixed + required: false + sort: 50 + is_modifiable: true + - id: 793 + code: backupConfiguration + data_type: map-fixed + required: true + sort: 60 + is_modifiable: true + - id: 794 + code: autoscaleConfiguration + data_type: map-fixed + required: true + sort: 70 + is_modifiable: true + - name: create_backup + id: 264 + kind: subresource + action: create + subresource: backup + params: [] + - name: create_database + id: 245 + kind: subresource + action: create + subresource: database + params: + - id: 741 + code: dbName + data_type: string + required: true + regex: ^[A-Za-z0-9]+$ + - id: 742 + code: dbOwner + data_type: string + required: true + - name: create_user + id: 241 + kind: subresource + action: create + subresource: user + params: + - id: 733 + code: username + data_type: string + required: true + regex: ^[A-Za-z0-9]+$ + maxlength: 62 + minlength: 2 + - id: 740 + code: role + data_type: string + required: true + value_list: + - app_user + - ddl_user + descr: Выбрать роль из списка + man: '`app_user` — стандартный пользователь с базовым набором прав
`ddl_user` — пользователь с расширенными правами, включая управление ролями и базами данных' + - name: delete + id: 20 + kind: instance + action: delete + params: + - id: 457 + code: deleteS3Bucket + data_type: boolean + required: true + default: "true" + value_list: + - "false" + - "true" + descr: Также удалить технический S3 Bucket с бэкапами? + sort: 10 + - name: delete_database + id: 246 + kind: subresource + action: delete + subresource: database + params: + - id: 743 + code: dbName + data_type: string + required: true + - name: delete_user + id: 244 + kind: subresource + action: delete + subresource: user + params: + - id: 739 + code: username + data_type: string + required: true + descr: 'Пример: `username-admin`' + - name: modify + id: 48 + kind: instance + action: modify + params: + - id: 795 + code: clusterConfiguration + data_type: map-fixed + required: true + sort: 10 + - id: 796 + code: accessConfiguration + data_type: map-fixed + required: true + sort: 20 + - id: 797 + code: postgresConfiguration + data_type: map-fixed + required: true + sort: 30 + - id: 798 + code: postgresConf + data_type: array-map-fixed + required: false + sort: 40 + - id: 799 + code: backupConfiguration + data_type: map-fixed + required: true + sort: 50 + - id: 800 + code: autoscaleConfiguration + data_type: map-fixed + required: true + sort: 60 + - name: reconcile + id: 263 + kind: action + action: reconcile + params: [] + - name: recovery + id: 47 + kind: action + action: recovery + params: + - id: 801 + code: startupConfiguration + data_type: map-fixed + required: true + sort: 10 + - id: 802 + code: clusterConfiguration + data_type: map-fixed + required: true + sort: 20 + - id: 803 + code: accessConfiguration + data_type: map-fixed + required: true + sort: 30 + - id: 804 + code: postgresConfiguration + data_type: map-fixed + required: true + sort: 40 + - id: 805 + code: postgresConf + data_type: array-map-fixed + required: false + sort: 50 + - id: 806 + code: backupConfiguration + data_type: map-fixed + required: true + sort: 60 + - id: 807 + code: autoscaleConfiguration + data_type: map-fixed + required: true + sort: 70 + - name: restart + id: 54 + kind: action + action: restart + man: ВНИМАНИЕ - Рестарт предполагает УДАЛЕНИЕ подов.
Может быть полная поломка. Крайне осторожно запускать при работе + params: [] + - name: resume + id: 114 + kind: instance + action: resume + params: [] + - name: suspend + id: 115 + kind: instance + action: suspend + params: [] diff --git a/universal_rebuild/resources_yaml/91_redis.yaml b/universal_rebuild/resources_yaml/91_redis.yaml new file mode 100644 index 0000000..2efb3d8 --- /dev/null +++ b/universal_rebuild/resources_yaml/91_redis.yaml @@ -0,0 +1,85 @@ +name: redis +service_id: 91 +service_display_name: Redis +service_short_name: redis +service_man: 'Redis кластер. Под кеши.
Ресурсная платформа: k8s.
Автоматизация: k8s operator
port 6379' +lifecycle: + suspend_on_destroy_default: true + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 21 + kind: instance + action: create + params: + - id: 1061 + code: startupConfiguration + data_type: map-fixed + required: true + sort: 10 + - id: 1062 + code: clusterConfiguration + data_type: map-fixed + required: true + sort: 20 + is_modifiable: true + - id: 1063 + code: accessConfiguration + data_type: map-fixed + required: true + sort: 30 + is_modifiable: true + - name: delete + id: 22 + kind: instance + action: delete + params: [] + - name: modify + id: 394 + kind: instance + action: modify + params: + - id: 1064 + code: clusterConfiguration + data_type: map-fixed + required: true + sort: 10 + - id: 1065 + code: accessConfiguration + data_type: map-fixed + required: true + sort: 20 + - name: reconcile + id: 391 + kind: action + action: reconcile + params: [] + - name: resume + id: 393 + kind: instance + action: resume + params: [] + - name: suspend + id: 392 + kind: instance + action: suspend + params: [] diff --git a/universal_rebuild/resources_yaml/92_mongodb.yaml b/universal_rebuild/resources_yaml/92_mongodb.yaml new file mode 100644 index 0000000..067b1bb --- /dev/null +++ b/universal_rebuild/resources_yaml/92_mongodb.yaml @@ -0,0 +1,191 @@ +name: mongodb +service_id: 92 +service_display_name: MongoDB +service_short_name: mongodb +service_man: '# Инструкция по развертыванию MongoDB через платформу

## 1. Общая информация

MongoDB — документоориентированная распределённая база данных, обеспечивающая высокую доступность, горизонтальное масштабирование и гибкую модель хранения.

### Доступные операции с кластером

* **create** — создание базы
* **delete** — удаление базы данных
* **suspend** — остановка базы данных
* **resume** — запуск базы данных
* **modify** — изменение базы данных

### Доступные операции с пользователями

* **create_user** — создание пользователя
* **modify_user** — модификация пользователя
* **delete_user** — удаление пользователя

---

## 2. Параметры развертывания базы данных

Эти параметры необходимо указать при создании базы данных.

### Платформа для развертывания

Среда, в которой будет развернут кластер. Выбирается из предложенного списка.

### Количество узлов

Количество реплик в кластере MongoDB.
Оптимально выбирать нечётное значение, так как это обеспечивает корректный процесс выбора ведущего узла и сохраняет работоспособность базы данных даже при отказе одной из нод.

> **Примечание:** уменьшение количества узлов после создания кластера не рекомендуется.

### Квота памяти пода (MB)

Объём памяти для одного пода в мегабайтах.
Пример: `2048`

### Квота CPU пода (mCPU)

CPU-квота для пода в миликорах (1000 mCPU = 1 vCPU).
Пример: `1000`

### Размер диска для базы данных (GB)

Объём диска для хранения данных в гигабайтах.
Пример: `20`

### Имя ipSpace для публикации внешнего IP

Имя ipSpace, через который будет выделен внешний IP для доступа к master-узлу.
Укажите `no-needed`, если внешний адрес не требуется.
Пример: `internet-no-antiddos-v1`

### Список разрешённых IP-адресов

Список IP-адресов или подсетей, с которых разрешён доступ к кластеру.
Если оставить пустым — доступ открыт для всех источников.
Пример: `["10.0.0.0/24", "192.168.1.10/32"]`

---

## 3. Параметры модификации базы данных

Все параметры, которые вы указываете при модификации, являются необязательными — система автоматически подставит ранее сохранённые значения, и при необходимости вы можете изменить их на нужные.

### Количество узлов

Количество реплик в кластере MongoDB.
Оптимально выбирать нечётное значение, так как это обеспечивает корректный процесс выбора ведущего узла и сохраняет работоспособность базы данных даже при отказе одной из нод.

> **Примечание:** уменьшение количества узлов после создания кластера не рекомендуется.

### Квота памяти пода (MB)

Объём памяти для одного пода в мегабайтах.
Пример: `2048`

### Квота CPU пода (mCPU)

CPU-квота для пода в миликорах (1000 mCPU = 1 vCPU).
Пример: `1000`

### Размер диска для базы данных (GB)

Объём диска для хранения данных указывается в гигабайтах. Его можно увеличить при необходимости, однако уменьшить уже выделенный размер невозможно.
Пример: `20`

### Имя ipSpace для публикации внешнего IP

Имя ipSpace, через который будет выделен внешний IP для доступа к master-узлу.
Укажите `no-needed`, если внешний адрес не требуется.
Пример: `internet-no-antiddos-v1`

### Список разрешённых IP-адресов

Список IP-адресов или подсетей, с которых разрешён доступ к кластеру.
Если оставить пустым — доступ открыт для всех источников.
Пример: `["10.0.0.0/24", "192.168.1.10/32"]`

---

## 3. Параметры создания пользователя

### Имя пользователя

Указывается имя пользователя (любой регистр).
Пример: `readApp`

### Имя базы данных

Название базы, в которой будет создан пользователь.
Если база отсутствует, она будет создана автоматически.
Пример: `Apps_9392`

### Имя роли

* **read** — доступ только на чтение
* **readWrite** — чтение и изменение данных
* **dbOwner** — полный контроль над базой данных

---

## 4. Параметры модификации пользователя

### Имя пользователя

Необходимо выбрать существующего пользователя.
Если список пуст, требуется предварительно создать пользователя.

### Имя базы данных

База данных, к которой относится пользователь.
Если база отсутствует, она будет создана автоматически.
Пример: `Apps_9392`

### Имя роли

* **read**
* **readWrite**
* **dbOwner**

---

## 5. Параметры удаления пользователя

### Имя пользователя

Выберите пользователя из списка для удаления.

---

## 6. Выходные параметры

После успешного развертывания базы данных платформа возвращает ключевые параметры.

### internalConnect

Внутренний эндпоинт для подключения к кластеру из сервисов, работающих в той же сети.
Используется для внутренних интеграций и системных сервисов.

### externalConnect

Внешний FQDN для подключения к кластеру извне.
Порт подключения — **27017**.

---

## 7. Пример подключения

Перед подключением необходимо установить **mongosh**.

Команда подключения к базе данных:

```
mongosh "mongodb://<username>:<password>@<endpoint_db>:27017/<dbName>"
```

Где:

* `<username>` — имя пользователя
* `<password>` — пароль
* `<endpoint_db>` — internalConnect или externalConnect
* `<dbName>` — имя базы данных

## 8. Дополнительная информация

Сейчас соединение с базой работает без шифрования, и доступ к кластеру осуществляется только по логину и паролю.' +lifecycle: + suspend_on_destroy_default: true + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 37 + kind: instance + action: create + params: + - id: 487 + code: resourceInstances + data_type: integer > 0 + required: true + maxvalue: 7 + minvalue: 1 + descr: 'Пример: `2`' + sort: 10 + is_modifiable: true + - id: 588 + code: resourceMemory + data_type: integer > 0 + required: true + minvalue: 512 + descr: 'Пример: `2048`' + is_modifiable: true + - id: 589 + code: resourceCPU + data_type: integer > 0 + required: true + descr: 'Пример: `1000`' + is_modifiable: true + - id: 590 + code: resourceDisk + data_type: integer > 0 + required: true + descr: 'Пример: `20`' + is_modifiable: true + - id: 595 + code: resourceRealm + data_type: string + required: true + func: getAvailableResourceRealms + descr: Выбрать платформу из списка + sort: 10 + - id: 646 + code: ipSpaceNameMaster + data_type: string + required: false + descr: 'Если включен предыдущий параметр, то поле необходимо заполнить. Пример: `internet-no-antiddos-v1`' + depends_on: resourceRealm + is_modifiable: true + - id: 889 + code: masterAccessList + data_type: json + required: true + default: '[]' + descr: Список IP-адресов, с которых разрешён доступ. Если оставить пустым — доступ открыт всем. + man: 'Передаётся как массив строк. Пример: ["192.168.0.0", "192.168.0.1"]' + is_modifiable: true + - name: create_user + id: 198 + kind: subresource + action: create + subresource: user + params: + - id: 635 + code: username + data_type: string + required: true + - id: 636 + code: dbName + data_type: string + required: true + - id: 643 + code: role + data_type: string + required: true + value_list: + - dbOwner + - read + - readWrite + - name: delete + id: 190 + kind: instance + action: delete + params: [] + - name: delete_user + id: 199 + kind: subresource + action: delete + subresource: user + params: + - id: 644 + code: username + data_type: string + required: true + - name: modify + id: 201 + kind: instance + action: modify + params: + - id: 648 + code: resourceInstances + data_type: integer > 0 + required: false + descr: 'Пример: `2`' + - id: 649 + code: resourceMemory + data_type: integer > 0 + required: false + minvalue: 512 + descr: 'Пример: `2048`' + - id: 650 + code: resourceCPU + data_type: integer > 0 + required: false + minvalue: 500 + descr: 'Пример: `1000`' + - id: 651 + code: resourceDisk + data_type: integer > 0 + required: false + descr: 'Пример: `20`' + - id: 653 + code: ipSpaceNameMaster + data_type: string + required: false + descr: 'Если включен предыдущий параметр, то поле необходимо заполнить. Пример: `internet-no-antiddos-v1`' + - id: 890 + code: masterAccessList + data_type: json + required: false + default: '[]' + descr: Список IP-адресов, с которых разрешён доступ. Если оставить пустым — доступ открыт всем. + man: 'Передаётся как массив строк. Пример: ["192.168.0.0", "192.168.0.1"]' + - name: modify_user + id: 200 + kind: subresource + action: modify + subresource: user + params: + - id: 640 + code: username + data_type: string + required: true + - id: 641 + code: dbName + data_type: string + required: true + - id: 642 + code: role + data_type: string + required: true + value_list: + - dbOwner + - read + - readWrite + - name: resume + id: 196 + kind: instance + action: resume + params: [] + - name: suspend + id: 194 + kind: instance + action: suspend + params: [] diff --git a/universal_rebuild/resources_yaml/93_rabbitmq.yaml b/universal_rebuild/resources_yaml/93_rabbitmq.yaml new file mode 100644 index 0000000..50da006 --- /dev/null +++ b/universal_rebuild/resources_yaml/93_rabbitmq.yaml @@ -0,0 +1,164 @@ +name: rabbitmq +service_id: 93 +service_display_name: RabbitMQ +service_short_name: rabbitmq +service_man: '# Инструкция по развертыванию RabbitMQ через платформу

---
## 1. Общая информация
RabbitMQ — это брокер сообщений с открытым исходным кодом, который обеспечивает надёжную доставку сообщений между приложениями.
Позволяет надёжно передавать сообщения между сервисами и системами, интегрироваться с внешними решениями и использовать как внутренние, так и защищённые TLS-подключения. Развёртывание полностью автоматизировано платформой.

## Доступные операции
- **create** — Создание кластера
- **modify** — Изменение параметров кластера
- **delete** — Удаление кластера
- **suspend** — Остановка кластера
- **resume** — Запуск кластера


---
## 2. Параметры развертывания кластера
Ниже перечислены параметры, которые необходимо указать при создании ресурса RabbitMQ:

* **Платформа для развертывания:**
Платформа для развертывания (среда, в которой будет развернут кластер). Пример: `k8s-3.ext.nubes.ru`.

* **Количество узлов в сети:**
Количество реплик в кластере RabbitMQ. При значении больше 1 создаётся кластер с репликацией для обеспечения высокой доступности.
Пример: `1` (single node) или `3` (кластер с репликацией)

* **Квота (megabyte) памяти пода:**
Квота памяти пода в мегабайтах. Пример: `2048`

* **Квота (millicore) ядра пода:**
Квота CPU пода в миликорах (1000 mCPU = 1 vCPU). Пример: `1000`

* **Размер (gigabyte) диска для базы данных:**
Размер диска для хранения данных RabbitMQ в гигабайтах. Пример: `50`

* **Выделяет внешний IP для доступа к master:**
Флаг, определяющий, нужно ли выделять внешний IP для доступа к master-узлу. При включении создаётся Service типа LoadBalancer и настраивается VIP на порту 5672.
Пример: `true` или `false`

* **Имя ipSpace для публикации внешнего IP master:**
Если включен предыдущий параметр, то поле необходимо заполнить. Пример: `internet-no-antiddos-v1`


---
## 3. Модификация кластера
Операция **modify** позволяет изменить параметры существующего кластера RabbitMQ.

**Изменяемые параметры:**
* **Квота (megabyte) памяти пода** — можно увеличить или уменьшить
* **Квота (millicore) ядра пода** — можно увеличить или уменьшить
* **Размер (gigabyte) диска для базы данных** — можно только увеличить (уменьшение не поддерживается)
* **Количество узлов в сети** — можно только увеличить (уменьшение не поддерживается)
* **Выделяет внешний IP для доступа к master** — можно включить или выключить
* **Имя ipSpace для публикации внешнего IP master** — можно изменить при включенном внешнем IP

**Ограничения:**
* Уменьшение размера диска не поддерживается
* Уменьшение количества реплик не поддерживается

---
## 4. Выходные параметры
После успешного развертывания ресурса RabbitMQ платформа возвращает ряд параметров в разделе "Поля состояния (сгенерированные значения)":

**clusterDomain**
Домен кластера Kubernetes, в котором развернут RabbitMQ.
Пример: `cluster.local`

**inernalConnect**
Внутренние эндпоинты для подключения к кластеру RabbitMQ из других сервисов, развернутых внутри той же сети.
Содержит:
* **master** — FQDN для подключения к кластеру. Пример: `rabbitmqk8s.{namespace}.svc.{clusterDomain}`

**externalConnect**
Внешние эндпоинты для подключения к кластеру RabbitMQ извне.
Содержит объекты с полями:
* **admin** — доступ к веб-интерфейсу управления:
* `fqdn` — полное доменное имя для веб-интерфейса. Пример: `admin.{namespace}.{domain}`
* `ip` — IP-адрес
* **master** — доступ к AMQP протоколу (если включен внешний IP):
* `fqdn` — полное доменное имя для AMQP. Пример: `write.{namespace}.{domain}`
* `ip` — IP-адрес

**monitoring**
Информация о дашбордах мониторинга:
* **allDashboards** — URL для доступа ко всем дашбордам Grafana

Для подключения используются следующие порты:

* **5672** — AMQP протокол (внутренний и внешний доступ, если включен внешний IP)
* **15672** — веб-интерфейс управления (доступен через Ingress по HTTPS)

**Учётные данные**
Данные для подключения к RabbitMQ доступны в разделе "Учётные данные":
* **adminUser** — имя пользователя администратора
* **adminPass** — пароль администратора


---
## 5. Примеры подключения через консоль (rabbitmqadmin)

### Работа с очередями через rabbitmqadmin

Для работы с RabbitMQ через консоль используется утилита `rabbitmqadmin`. Установите её или используйте образ с предустановленной утилитой.

Задайте переменные окружения с учётными данными:

```bash
export RABBITMQ_USERNAME=<adminUser>
export RABBITMQ_PASSWORD=<adminPass>
export RABBIT_HOST=<host>
```

Где:
* `<adminUser>` и `<adminPass>` — учётные данные из раздела "Учётные данные"
* `<host>` — адрес для подключения:
* Для внешнего подключения: `externalConnect.admin.fqdn` или `externalConnect.master.fqdn`

**Создание очереди:**

```bash
rabbitmqadmin \
-H "$RABBIT_HOST" -P 15672 \
-u "$RABBITMQ_USERNAME" -p "$RABBITMQ_PASSWORD" \
declare queue name=healthcheck durable=false
```

**Листинг очередей:**

```bash
rabbitmqadmin \
-H "$RABBIT_HOST" -P 15672 \
-u "$RABBITMQ_USERNAME" -p "$RABBITMQ_PASSWORD" \
list queues
```

**Отправка сообщения:**

```bash
rabbitmqadmin \
-H "$RABBIT_HOST" -P 15672 \
-u "$RABBITMQ_USERNAME" -p "$RABBITMQ_PASSWORD" \
publish exchange=amq.default routing_key=healthcheck payload="ping-$(date +%s)"
```

**Чтение сообщения:**

```bash
rabbitmqadmin \
-H "$RABBIT_HOST" -P 15672 \
-u "$RABBITMQ_USERNAME" -p "$RABBITMQ_PASSWORD" \
get queue=healthcheck ackmode=ack_requeue_false
```

---
## 6. Дополнительная информация
Для подключения к кластеру RabbitMQ используйте полученные эндпоинты из раздела "Поля состояния (сгенерированные значения)" и учётные данные из раздела "Учётные данные".

Веб-интерфейс управления доступен по адресу, указанному в параметре `externalConnect.admin.fqdn` через HTTPS.

Для внутреннего подключения используйте адрес из `inernalConnect.master` на порту 5672.

---' +lifecycle: + suspend_on_destroy_default: true + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 38 + kind: instance + action: create + params: + - id: 116 + code: resourceCPU + data_type: integer > 0 + required: true + default: "1000" + maxvalue: 4000 + minvalue: 500 + descr: 'Пример: `2000`' + sort: 20 + is_modifiable: true + - id: 117 + code: resourceMemory + data_type: integer > 0 + required: true + default: "1124" + maxvalue: 8192 + minvalue: 512 + descr: 'Пример: `2048`' + sort: 30 + is_modifiable: true + - id: 118 + code: resourceDisk + data_type: integer > 0 + required: true + default: "30" + maxvalue: 999 + minvalue: 5 + descr: 'Пример: `20`' + sort: 40 + is_modifiable: true + - id: 119 + code: resourceRealm + data_type: string + required: true + func: getAvailableResourceRealms + descr: Выбрать платформу из списка + sort: 10 + - id: 224 + code: resourceInstances + data_type: integer > 0 + required: true + default: "1" + maxvalue: 7 + minvalue: 1 + descr: 'Пример: `1`' + sort: 50 + is_modifiable: true + - id: 225 + code: needExternalAddressMaster + data_type: boolean + required: true + default: "false" + value_list: + - "false" + - "true" + descr: 'Флаг, определяющий, нужно ли выделять внешний IP для доступа к master. Пример: `true` или `false`' + sort: 60 + is_modifiable: true + - id: 543 + code: ipSpaceNameMaster + data_type: string + required: false + descr: 'Пример: `internet-no-antiddos-v1`' + is_modifiable: true + - name: delete + id: 39 + kind: instance + action: delete + params: [] + - name: modify + id: 106 + kind: instance + action: modify + params: + - id: 304 + code: resourceCPU + data_type: integer > 0 + required: true + maxvalue: 4000 + minvalue: 500 + descr: 'Пример: `2000`' + sort: 10 + - id: 305 + code: resourceMemory + data_type: integer > 0 + required: true + maxvalue: 8192 + minvalue: 512 + descr: 'Пример: `2048`' + sort: 20 + - id: 306 + code: resourceDisk + data_type: integer > 0 + required: true + maxvalue: 999 + minvalue: 1 + descr: 'Пример: `20`' + sort: 10 + - id: 307 + code: resourceInstances + data_type: integer > 0 + required: true + maxvalue: 7 + minvalue: 1 + descr: 'Пример: `1`' + sort: 40 + - id: 308 + code: needExternalAddressMaster + data_type: boolean + required: true + value_list: + - "false" + - "true" + descr: Выделяет внешний (белый) IP для доступа к master + sort: 50 + - id: 545 + code: ipSpaceNameMaster + data_type: string + required: false + descr: 'Пример: `internet-no-antiddos-v1`' + - name: resume + id: 117 + kind: instance + action: resume + params: [] + - name: suspend + id: 116 + kind: instance + action: suspend + man: Пока не работает из за того что RabbitmqCluster нельзя занулить + params: [] diff --git a/universal_rebuild/resources_yaml/94_lucee.yaml b/universal_rebuild/resources_yaml/94_lucee.yaml new file mode 100644 index 0000000..ca0f8bf --- /dev/null +++ b/universal_rebuild/resources_yaml/94_lucee.yaml @@ -0,0 +1,118 @@ +name: lucee +service_id: 94 +service_display_name: Lucee +service_short_name: lucee +lifecycle: + suspend_on_destroy_default: true + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 49 + kind: instance + action: create + params: + - id: 1019 + code: startupConfiguration + data_type: map-fixed + required: true + sort: 10 + - id: 1020 + code: clusterConfiguration + data_type: map-fixed + required: true + sort: 20 + is_modifiable: true + - id: 1021 + code: accessConfiguration + data_type: map-fixed + required: true + sort: 30 + - id: 1022 + code: appConfiguration + data_type: map-fixed + required: true + sort: 40 + is_modifiable: true + - id: 1023 + code: jsonEnv + data_type: map + required: false + default: '{}' + man: Передаётся в формате КЛЮЧ:ЗНАЧЕНИЕ + sort: 50 + is_modifiable: true + - name: delete + id: 50 + kind: instance + action: delete + params: [] + - name: modify + id: 55 + kind: instance + action: modify + params: + - id: 1024 + code: clusterConfiguration + data_type: map-fixed + required: true + sort: 10 + - id: 1025 + code: appConfiguration + data_type: map-fixed + required: true + sort: 20 + - id: 1026 + code: jsonEnv + data_type: map + required: false + man: Передаётся в формате КЛЮЧ:ЗНАЧЕНИЕ + sort: 30 + - name: reconcile + id: 373 + kind: action + action: reconcile + params: [] + - name: redeploy + id: 53 + kind: action + action: redeploy + params: + - id: 1027 + code: startupConfiguration + data_type: map-fixed + required: true + sort: 10 + - name: restart + id: 163 + kind: action + action: restart + params: [] + - name: resume + id: 95 + kind: instance + action: resume + params: [] + - name: suspend + id: 96 + kind: instance + action: suspend + params: [] diff --git a/universal_rebuild/resources_yaml/95_nodejs.yaml b/universal_rebuild/resources_yaml/95_nodejs.yaml new file mode 100644 index 0000000..6ac319c --- /dev/null +++ b/universal_rebuild/resources_yaml/95_nodejs.yaml @@ -0,0 +1,114 @@ +name: nodejs +service_id: 95 +service_display_name: NodeJS +service_short_name: nodejs +service_man: Развертывает контейнер в k8s кластере с node, который можно использовать для запуска своих приложений.
Приложение должно быть доступно по ссылки в git репозитории, которая является входным параметром. +lifecycle: + suspend_on_destroy_default: true + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 58 + kind: instance + action: create + params: + - id: 1028 + code: startupConfiguration + data_type: map-fixed + required: true + sort: 10 + - id: 1029 + code: clusterConfiguration + data_type: map-fixed + required: true + sort: 20 + is_modifiable: true + - id: 1030 + code: accessConfiguration + data_type: map-fixed + required: true + sort: 30 + - id: 1031 + code: appConfiguration + data_type: map-fixed + required: true + sort: 40 + is_modifiable: true + - id: 1032 + code: jsonEnv + data_type: map + required: false + default: '{}' + man: Передаётся в формате КЛЮЧ:ЗНАЧЕНИЕ + sort: 50 + is_modifiable: true + - name: delete + id: 61 + kind: instance + action: delete + params: [] + - name: modify + id: 59 + kind: instance + action: modify + params: + - id: 1033 + code: clusterConfiguration + data_type: map-fixed + required: true + sort: 10 + - id: 1034 + code: appConfiguration + data_type: map-fixed + required: true + sort: 20 + - id: 1035 + code: jsonEnv + data_type: map + required: false + man: Передаётся в формате КЛЮЧ:ЗНАЧЕНИЕ + sort: 30 + - name: reconcile + id: 374 + kind: action + action: reconcile + params: [] + - name: redeploy + id: 60 + kind: action + action: redeploy + params: + - id: 1036 + code: startupConfiguration + data_type: map-fixed + required: true + sort: 10 + - name: resume + id: 121 + kind: instance + action: resume + params: [] + - name: suspend + id: 120 + kind: instance + action: suspend + params: [] diff --git a/universal_rebuild/resources_yaml/96_pgadmin.yaml b/universal_rebuild/resources_yaml/96_pgadmin.yaml new file mode 100644 index 0000000..0a3efa3 --- /dev/null +++ b/universal_rebuild/resources_yaml/96_pgadmin.yaml @@ -0,0 +1,158 @@ +name: pgadmin +service_id: 96 +service_display_name: pgAdmin +service_short_name: pgadmin +service_man: Инструмент с графическим интерфейсом для управления базами данных PostgreSQL +lifecycle: + suspend_on_destroy_default: true + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 62 + kind: instance + action: create + params: + - id: 164 + code: domain + data_type: string + required: true + default: web01 + regex: ^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$ + unique_scope: provider + descr: Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm + man: Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm + sort: 10 + - id: 165 + code: resourceCPU + data_type: integer > 0 + required: true + default: "500" + maxvalue: 4000 + minvalue: 500 + descr: Квота ядра пода (milicores). 1000Mili = 1 Ядро + man: Квота ядра пода (milicores). 1000Mili = 1 Ядро + sort: 30 + is_modifiable: true + - id: 166 + code: resourceMemory + data_type: integer > 0 + required: true + default: "512" + maxvalue: 8192 + minvalue: 512 + descr: Квота памяти пода (MBytes) + man: Квота памяти пода (MBytes) + sort: 40 + is_modifiable: true + - id: 167 + code: resourceDisk + data_type: integer > 0 + required: true + default: "1" + maxvalue: 999 + minvalue: 1 + descr: Квота диска (Указывается в GIGAbytes) + man: Квота диска (Указывается в GIGAbytes) + sort: 50 + is_modifiable: true + - id: 169 + code: resourceRealm + data_type: string + required: true + func: getAvailableResourceRealms + descr: Платформа для развертывания + man: Наименование кластера Kubernetes для подключения + sort: 20 + - id: 170 + code: login + data_type: string + required: true + regex: ^([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)$ + maxlength: 255 + minlength: 3 + descr: Логин для подключения к pgAdmin, в формате email (login@email.net) + man: Логин для подключения к pgAdmin, в формате email (login@email.net) + sort: 60 + - id: 171 + code: password + data_type: string + required: true + descr: Пароль для подключения к pgAdmin + man: Пароль для подключения к pgAdmin + sort: 70 + is_sensitive: true + - name: delete + id: 63 + kind: instance + action: delete + params: [] + - name: modify + id: 64 + kind: instance + action: modify + params: + - id: 172 + code: resourceCPU + data_type: integer > 0 + required: false + maxvalue: 4000 + minvalue: 500 + descr: Квота ядра пода (Указывается в mili). 1000Mili = 1 Ядро + man: Квота ядра пода (Указывается в mili). 1000Mili = 1 Ядро + - id: 173 + code: resourceMemory + data_type: integer > 0 + required: false + maxvalue: 8192 + minvalue: 512 + descr: Квота памяти пода (MBytes) + man: Квота памяти пода (MBytes) + - id: 174 + code: resourceDisk + data_type: integer > 0 + required: false + default: "1" + maxvalue: 999 + minvalue: 1 + descr: Квота диска (Указывается в GIGAbytes) + man: Квота диска (Указывается в GIGAbytes) + - name: redeploy + id: 65 + kind: action + action: redeploy + params: + - id: 277 + code: resourceRealm + data_type: string + required: true + func: getAvailableResourceRealms + descr: Платформа для развертывания + - name: resume + id: 123 + kind: instance + action: resume + params: [] + - name: suspend + id: 122 + kind: instance + action: suspend + params: [] diff --git a/universal_rebuild/resources_yaml/97_nodered.yaml b/universal_rebuild/resources_yaml/97_nodered.yaml new file mode 100644 index 0000000..4edaed8 --- /dev/null +++ b/universal_rebuild/resources_yaml/97_nodered.yaml @@ -0,0 +1,91 @@ +name: nodered +service_id: 97 +service_display_name: NodeRed +service_short_name: nodered +lifecycle: + suspend_on_destroy_default: false + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 72 + kind: instance + action: create + params: + - id: 193 + code: resourceCPU + data_type: integer > 0 + required: true + default: "500" + maxvalue: 4000 + minvalue: 100 + descr: Квота ядра пода (Указывается в mili). 1000Mili = 1 Ядро + sort: 30 + - id: 194 + code: resourceMemory + data_type: integer > 0 + required: true + default: "512" + maxvalue: 8192 + minvalue: 128 + descr: Квота памяти пода (Указывается в Megabytes) + sort: 40 + - id: 195 + code: resourceDisk + data_type: integer > 0 + required: true + default: "1" + maxvalue: 999 + minvalue: 1 + descr: Квота диска (Указывается в GIGAbytes) + man: Квота диска (Указывается в GIGAbytes) + sort: 60 + - id: 196 + code: resourceRealm + data_type: string + required: true + func: getAvailableResourceRealms + descr: Платформа для развертывания + man: Наименование кластера Kubernetes для подключения + sort: 20 + - id: 197 + code: domain + data_type: string + required: true + regex: ^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$ + unique_scope: provider + descr: Наименование поддомена, будет добавлено в k8stemplate.dev.nubes.ru + man: Наименование поддомена, будет добавлено в k8stemplate.dev.nubes.ru + sort: 10 + - id: 323 + code: resourceInstances + data_type: integer > 0 + required: true + default: "1" + maxvalue: 7 + minvalue: 1 + descr: Количество узлов + sort: 50 + - name: delete + id: 73 + kind: instance + action: delete + params: [] diff --git a/universal_rebuild/resources_yaml/98_http.yaml b/universal_rebuild/resources_yaml/98_http.yaml new file mode 100644 index 0000000..11cbd0f --- /dev/null +++ b/universal_rebuild/resources_yaml/98_http.yaml @@ -0,0 +1,155 @@ +name: http +service_id: 98 +service_display_name: Простой HTTP контейнер +service_short_name: http +lifecycle: + suspend_on_destroy_default: true + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 77 + kind: instance + action: create + params: + - id: 212 + code: domain + data_type: string + required: true + default: web01 + regex: ^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$ + unique_scope: provider + descr: Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm + man: Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm + sort: 10 + - id: 213 + code: resourceCPU + data_type: integer > 0 + required: true + default: "100" + maxvalue: 4000 + minvalue: 100 + descr: Квота ядра пода (Указывается в mili) + man: Квота ядра пода (Указывается в mili) + sort: 30 + is_modifiable: true + - id: 214 + code: resourceMemory + data_type: integer > 0 + required: true + default: "200" + maxvalue: 8192 + minvalue: 128 + descr: Квота памяти пода (Указывается в Megabytes) + man: Квота памяти пода (Указывается в Megabytes) + sort: 40 + is_modifiable: true + - id: 215 + code: resourceInstances + data_type: integer > 0 + required: true + default: "1" + maxvalue: 7 + minvalue: 1 + descr: Количество узлов + man: Количество реплик приложения. Трафик будет распределяться между подами в режиме Round-Robin + sort: 50 + is_modifiable: true + - id: 216 + code: resourceRealm + data_type: string + required: true + func: getAvailableResourceRealms + descr: Платформа для развертывания + man: Наименование кластера Kubernetes для подключения + sort: 20 + - id: 217 + code: registryPath + data_type: string + required: true + default: nexus-sa.tst.nubes.ru/docker-nubes/jolt + descr: Путь до image, который будет установлен как deployment + man: Путь до image, который будет установлен как deployment + sort: 60 + - id: 241 + code: jsonEnv + data_type: map + required: false + man: Массив переменных, будут переданы в env приложения + sort: 70 + - id: 782 + code: ingressAnnotations + data_type: map + required: false + default: '{}' + man: Формат ключ/значение
Поддерживает пока простые аннотации + sort: 80 + - name: delete + id: 78 + kind: instance + action: delete + params: [] + - name: modify + id: 81 + kind: instance + action: modify + params: + - id: 218 + code: resourceCPU + data_type: integer > 0 + required: true + maxvalue: 4000 + minvalue: 100 + - id: 219 + code: resourceMemory + data_type: integer > 0 + required: true + maxvalue: 8192 + minvalue: 128 + - id: 220 + code: resourceInstances + data_type: integer > 0 + required: true + maxvalue: 7 + minvalue: 1 + descr: Количество узлов + - name: redeploy + id: 79 + kind: action + action: redeploy + params: + - id: 272 + code: resourceRealm + data_type: string + required: true + func: getAvailableResourceRealms + descr: Платформа для развертывания + sort: 10 + - name: resume + id: 130 + kind: instance + action: resume + params: [] + - name: suspend + id: 131 + kind: instance + action: suspend + params: [] diff --git a/universal_rebuild/resources_yaml/99_gitea.yaml b/universal_rebuild/resources_yaml/99_gitea.yaml new file mode 100644 index 0000000..7170b8a --- /dev/null +++ b/universal_rebuild/resources_yaml/99_gitea.yaml @@ -0,0 +1,169 @@ +name: gitea +service_id: 99 +service_display_name: Gitea +service_short_name: gitea +service_man: '# Инструкция по развертыванию Gitea через платформу

---
## 1. Общая информация
Gitea — это лёгкий, быстрый и самодостаточный сервис хостинга Git-репозиториев с веб-интерфейсом.
Позволяет создавать и управлять репозиториями, организациями, командами, а также настраивать CI/CD через интеграции.
Развёртывание полностью автоматизировано платформой.

## Доступные операции
- **create** — Создание сервиса Gitea
- **delete** — Удаление сервиса Gitea
- **modify** — Модификация сервиса Gitea
- **suspend** — Остановка сервиса Gitea
- **resume** — Запуск сервиса Gitea

---
## 2. Параметры развертывания
Ниже перечислены параметры, которые необходимо указать при создании ресурса Gitea:

* **Платформа для развертывания:**
Платформа для развертывания (среда, в которой будет развёрнут сервис). Пример: `k8s-3.ext.nubes.ru`.

* **Количество узлов в сети:**
Количество реплик подов Gitea. Пример: `1`

* **Квота (megabyte) памяти пода:**
Квота памяти пода в мегабайтах. Пример: `1024`

* **Квота (millicore) ядра пода:**
Квота CPU пода в миликорах (1000 mCPU = 1 vCPU). Пример: `500`

* **Размер (gigabyte) диска для базы данных:**
Размер PVC для хранения репозиториев и данных Gitea в гигабайтах. Пример: `10`

* **FQDN / Домен:**
Доменное имя, по которому будет доступен сервис. Пример: `git.example.com`

* **Инстанс услуги PostgreSQL:**
UUID услуги k8sPgsql, в которой будет создана база данных `gitea` и пользователь `techgitea`.

* **Дополнительные параметры сервера (iniServerParams):**
JSON-объект с дополнительными параметрами секции `[server]` в `app.ini`. Пример:
```json
{"MAX_CONTENT_LENGTH": "524288000"}
```

* **Дополнительные annotations для Ingress (ingressExtraAnnotations):**
JSON-объект с дополнительными annotations для Ingress-контроллера. Пример:
```json
{"nginx.ingress.kubernetes.io/proxy-body-size": "500m"}
```

---
## 3. Особенности подключения и работы с репозиториями

### 3.1. Доступ только по HTTPS
Сервис развёртывается с **rootless-образом** Gitea. SSH-сервер внутри и он **доступен только внутри кластера Kubernetes**.

**Внешний доступ к репозиториям осуществляется исключительно по HTTPS через Ingress.**

```bash
# Пример клонирования
https://<домен>/<пользователь>/<репозиторий>.git
```

### 3.2. Настройка git для передачи логина и пароля
Для работы с приватными репозиториями через HTTPS необходимо передать учётные данные. Рекомендуется использовать один из следующих способов:

**Встроить креды в URL remote**
```bash
git remote set-url origin https://<логин>:<пароль>@<домен>/<пользователь>/<репозиторий>.git
```
> **Важно:** Логин и пароль администратора создаются автоматически при развертывании и хранятся в Vault.

### 3.3. Работа с большими файлами
По умолчанию `MAX_CONTENT_LENGTH` установлен в **500 МБ**, а Ingress пропускает pack-файлы до **500 МБ**.

Если необходимо запушить файлы близкие к лимиту, увеличьте HTTP-буфер git на стороне клиента:
```bash
git config http.postBuffer 524288000
```

Для файлов **более 500 МБ** рекомендуется использовать **Git LFS**.

---
## 4. Выходные параметры
После успешного развертывания ресурса Gitea платформа возвращает ряд параметров.

Ключевым из них является:

**connectionUrl**
Это внешний URL для доступа к веб-интерфейсу и Git-репозиториям по HTTPS. Пример:
```
https://git.example.com
```

**internalConnect**
Внутренний эндпоинт для подключения к PostgreSQL из других сервисов внутри кластера.

---
## 5. Дополнительная информация
- База данных `gitea` и пользователь `techgitea` создаются автоматически в указанной услуге PostgreSQL.
- DNS-запись и TLS-сертификат (Let''s Encrypt) настраиваются автоматически на основе указанного домена.
- Для изменения параметров сервера Gitea (`app.ini`) используйте операцию **modify** с полем `iniServerParams`.
- Для добавления дополнительных Ingress-annotations используйте операцию **modify** с полем `ingressExtraAnnotations`.
---' +lifecycle: + suspend_on_destroy_default: true + adopt_existing_on_create_default: false +outputs: + params: + - code: state_params + type: map + - code: state_out + type: map + - code: state_params_flat + type: map + - code: state_out_flat + type: map + - code: vault_secrets + type: map + sensitive: true + - code: vault_url + type: string + - code: vault_user_path + type: string + - code: vault_fields + type: list +operations: + - name: create + id: 82 + kind: instance + action: create + params: + - id: 232 + code: resourceCPU + data_type: integer > 0 + required: true + default: "500" + maxvalue: 4000 + minvalue: 300 + descr: Квота ядра пода (Указывается в mili). 1000Mili = 1 Ядро + man: Квота ядра пода (Указывается в mili). 1000Mili = 1 Ядро + sort: 40 + is_modifiable: true + - id: 233 + code: resourceMemory + data_type: integer > 0 + required: true + default: "512" + maxvalue: 8192 + minvalue: 256 + descr: Квота памяти пода (Указывается в Megabytes) + man: Квота памяти пода (Указывается в Megabytes) + sort: 50 + is_modifiable: true + - id: 234 + code: resourceDisk + data_type: integer > 0 + required: true + default: "1" + maxvalue: 999 + minvalue: 1 + descr: Квота диска (Указывается в GIGAbytes) + man: Квота диска (Указывается в GIGAbytes) + sort: 60 + is_modifiable: true + - id: 235 + code: resourceInstances + data_type: integer > 0 + required: true + default: "1" + maxvalue: 7 + minvalue: 1 + descr: Количество узлов + man: Количество узлов в сети + sort: 70 + is_modifiable: true + - id: 237 + code: domain + data_type: string + required: true + default: web01 + regex: ^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$ + unique_scope: provider + descr: Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm + man: Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm + sort: 20 + - id: 238 + code: psqlUid + data_type: uuid + required: true + ref_svc_id: 90 + descr: UUID Услуги k8sPgsql. Пользователя techGitea, базу данных gitea создаёт автоматически + man: UUID Услуги k8sPgsql. Пользователя techGitea, базу данных gitea создаёт автоматически + sort: 10 + - id: 818 + code: ingressExtraAnnotations + data_type: map + required: false + is_modifiable: true + - id: 821 + code: iniServerParams + data_type: map + required: false + is_modifiable: true + - name: delete + id: 83 + kind: instance + action: delete + params: [] + - name: modify + id: 91 + kind: instance + action: modify + params: + - id: 259 + code: resourceCPU + data_type: integer > 0 + required: true + maxvalue: 4000 + minvalue: 500 + descr: Квота ядра пода (Указывается в mili). 1000Mili = 1 Ядро + sort: 10 + - id: 260 + code: resourceMemory + data_type: integer > 0 + required: true + maxvalue: 8192 + minvalue: 256 + descr: Квота памяти пода (Указывается в Megabytes) + sort: 20 + - id: 261 + code: resourceDisk + data_type: integer > 0 + required: true + maxvalue: 999 + minvalue: 1 + descr: Квота диска (Указывается в GIGAbytes) + man: Квота диска (Указывается в GIGAbytes) + sort: 30 + - id: 262 + code: resourceInstances + data_type: integer > 0 + required: true + maxvalue: 7 + minvalue: 1 + descr: Количество узлов + man: Количество узлов в сети + sort: 40 + - id: 819 + code: ingressExtraAnnotations + data_type: map + required: false + man: 'Примеры:
- `nginx.ingress.kubernetes.io/proxy-body-size: 500m` (Размер пакета)
- `nginx.ingress.kubernetes.io/proxy-read-timeout: "600"` (Время ожидания при чтении)
- `nginx.ingress.kubernetes.io/proxy-send-timeout: "600"` (Время ожидания отправке)' + - id: 820 + code: iniServerParams + data_type: map + required: false + man: 'Примеры:
- `MAX_CONTENT_LENGTH: 524288000(~500мб)` (Размер пакета в байтах)' + - name: resume + id: 127 + kind: instance + action: resume + params: [] + - name: suspend + id: 126 + kind: instance + action: suspend + params: []