Files
oci-portal/internal/service/tenantdelete.go
T
Wang Defa dbba1f4905
CI / test (push) Successful in 30s
Release / release (push) Successful in 49s
发布 0.1.0:通知渠道、告警规则、令牌版本与安全加固
2026-07-10 17:38:34 +08:00

458 lines
13 KiB
Go

package service
import (
"context"
"encoding/json"
"fmt"
"strings"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"oci-portal/internal/model"
)
type tenantDeleteResult struct {
config model.OciConfig
deletedTaskIDs []uint
alertRuleIDs []uint
channelsGone bool
}
type tenantTaskAction struct {
task model.Task
deleteTask bool
payload string
}
type tenancyCacheInvalidator interface {
InvalidateTenancy(tenancyOCID string)
}
func (s *OciConfigService) deleteTenant(ctx context.Context, id uint) (*tenantDeleteResult, error) {
result := &tenantDeleteResult{}
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
return s.deleteTenantInTx(tx, id, result)
})
if err != nil {
return nil, fmt.Errorf("delete oci config %d: %w", id, err)
}
return result, nil
}
func (s *OciConfigService) deleteTenantInTx(tx *gorm.DB, id uint, result *tenantDeleteResult) error {
if err := lockTenant(tx, id, &result.config); err != nil {
return err
}
if err := s.deleteTenantTasks(tx, id, result); err != nil {
return err
}
if err := deleteTenantEvents(tx, id, result); err != nil {
return err
}
if err := deleteTenantAI(tx, id, result); err != nil {
return err
}
if err := deleteTenantSnapshots(tx, id); err != nil {
return err
}
return deleteTenantConfig(tx, id)
}
func lockTenant(tx *gorm.DB, id uint, cfg *model.OciConfig) error {
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(cfg, id).Error
if err != nil {
return fmt.Errorf("load tenant: %w", err)
}
return nil
}
func (s *OciConfigService) deleteTenantTasks(tx *gorm.DB, id uint, result *tenantDeleteResult) error {
var tasks []model.Task
err := tx.Where("type IN ?", []string{
model.TaskTypeSnatch, model.TaskTypeHealthCheck, model.TaskTypeCost,
}).Find(&tasks).Error
if err != nil {
return fmt.Errorf("load tenant tasks: %w", err)
}
for i := range tasks {
action, ok, err := planTenantTask(tasks[i], id)
if err != nil {
return err
}
if !ok {
continue
}
if err := applyTenantTask(tx, action, result); err != nil {
return err
}
}
return nil
}
func planTenantTask(task model.Task, id uint) (tenantTaskAction, bool, error) {
switch task.Type {
case model.TaskTypeSnatch:
return planSnatchTask(task, id)
case model.TaskTypeHealthCheck, model.TaskTypeCost:
return planMultiTenantTask(task, id)
default:
return tenantTaskAction{}, false, nil
}
}
func planSnatchTask(task model.Task, id uint) (tenantTaskAction, bool, error) {
var payload snatchPayload
if err := json.Unmarshal([]byte(task.Payload), &payload); err != nil {
return tenantTaskAction{}, false, fmt.Errorf("parse task %d payload: %w", task.ID, err)
}
if payload.OciConfigID != id {
return tenantTaskAction{}, false, nil
}
return tenantTaskAction{task: task, deleteTask: true}, true, nil
}
func planMultiTenantTask(task model.Task, id uint) (tenantTaskAction, bool, error) {
payload, err := decodeConfigIDs(task)
if err != nil {
return tenantTaskAction{}, false, err
}
if len(payload.OciConfigIDs) == 0 {
return tenantTaskAction{task: task, payload: task.Payload}, true, nil
}
remaining, found := removeConfigID(payload.OciConfigIDs, id)
if !found {
return tenantTaskAction{}, false, nil
}
if len(remaining) == 0 {
return tenantTaskAction{task: task, deleteTask: true}, true, nil
}
payload.OciConfigIDs = remaining
raw, err := json.Marshal(payload)
if err != nil {
return tenantTaskAction{}, false, fmt.Errorf("encode task %d payload: %w", task.ID, err)
}
return tenantTaskAction{task: task, payload: string(raw)}, true, nil
}
func decodeConfigIDs(task model.Task) (healthCheckPayload, error) {
var payload healthCheckPayload
if strings.TrimSpace(task.Payload) == "" {
return payload, nil
}
if err := json.Unmarshal([]byte(task.Payload), &payload); err != nil {
return payload, fmt.Errorf("parse task %d payload: %w", task.ID, err)
}
return payload, nil
}
func removeConfigID(ids []uint, id uint) ([]uint, bool) {
remaining := make([]uint, 0, len(ids))
found := false
for _, candidate := range ids {
if candidate == id {
found = true
continue
}
remaining = append(remaining, candidate)
}
return remaining, found
}
func applyTenantTask(tx *gorm.DB, action tenantTaskAction, result *tenantDeleteResult) error {
if err := deleteTaskHistory(tx, action.task.ID); err != nil {
return err
}
if action.deleteTask {
if err := tx.Delete(&model.Task{}, action.task.ID).Error; err != nil {
return fmt.Errorf("delete task %d: %w", action.task.ID, err)
}
result.deletedTaskIDs = append(result.deletedTaskIDs, action.task.ID)
return nil
}
return resetTenantTask(tx, action)
}
func deleteTaskHistory(tx *gorm.DB, taskID uint) error {
if err := tx.Where("task_id = ?", taskID).Delete(&model.TaskLog{}).Error; err != nil {
return fmt.Errorf("delete task %d logs: %w", taskID, err)
}
key := deadAliasKey(taskID)
if err := tx.Where("key = ?", key).Delete(&model.Setting{}).Error; err != nil {
return fmt.Errorf("delete task %d state: %w", taskID, err)
}
return nil
}
func resetTenantTask(tx *gorm.DB, action tenantTaskAction) error {
updates := map[string]any{
"payload": action.payload, "last_run_at": nil,
"last_error": "", "run_count": 0,
}
err := tx.Model(&model.Task{}).Where("id = ?", action.task.ID).Updates(updates).Error
if err != nil {
return fmt.Errorf("reset task %d: %w", action.task.ID, err)
}
return nil
}
func deleteTenantEvents(tx *gorm.DB, id uint, result *tenantDeleteResult) error {
ruleIDs, eventIDs, affectedRules, err := loadTenantEventRefs(tx, id)
if err != nil {
return err
}
if err := deleteAlertHits(tx, ruleIDs, eventIDs); err != nil {
return err
}
result.alertRuleIDs = mergeIDs(ruleIDs, affectedRules)
if err := deleteWhere(tx, &model.AlertRule{}, "oci_config_id = ?", id); err != nil {
return fmt.Errorf("delete tenant alert rules: %w", err)
}
if err := deleteWhere(tx, &model.LogEvent{}, "oci_config_id = ?", id); err != nil {
return fmt.Errorf("delete tenant log events: %w", err)
}
return nil
}
func loadTenantEventRefs(tx *gorm.DB, id uint) ([]uint, []uint, []uint, error) {
ruleIDs, err := lockedTenantRuleIDs(tx, id)
if err != nil {
return nil, nil, nil, fmt.Errorf("load tenant alert rules: %w", err)
}
eventIDs, err := lockedTenantEventIDs(tx, id)
if err != nil {
return nil, nil, nil, fmt.Errorf("load tenant log events: %w", err)
}
affected, err := alertHitRuleIDs(tx, eventIDs)
if err != nil {
return nil, nil, nil, err
}
return ruleIDs, eventIDs, affected, nil
}
func lockedTenantRuleIDs(tx *gorm.DB, id uint) ([]uint, error) {
var rows []model.AlertRule
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Select("id").
Where("oci_config_id = ?", id).Order("id").Find(&rows).Error
ids := make([]uint, 0, len(rows))
for _, row := range rows {
ids = append(ids, row.ID)
}
return ids, err
}
func lockedTenantEventIDs(tx *gorm.DB, id uint) ([]uint, error) {
var rows []model.LogEvent
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Select("id").
Where("oci_config_id = ?", id).Order("id").Find(&rows).Error
ids := make([]uint, 0, len(rows))
for _, row := range rows {
ids = append(ids, row.ID)
}
return ids, err
}
func alertHitRuleIDs(tx *gorm.DB, eventIDs []uint) ([]uint, error) {
if len(eventIDs) == 0 {
return nil, nil
}
ids := make([]uint, 0)
err := tx.Model(&model.AlertRuleHit{}).Where("log_event_id IN ?", eventIDs).
Distinct().Pluck("rule_id", &ids).Error
if err != nil {
return nil, fmt.Errorf("load affected alert rules: %w", err)
}
return ids, nil
}
func mergeIDs(groups ...[]uint) []uint {
seen := make(map[uint]struct{})
out := make([]uint, 0)
for _, ids := range groups {
for _, id := range ids {
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
out = append(out, id)
}
}
return out
}
func deleteAlertHits(tx *gorm.DB, ruleIDs, eventIDs []uint) error {
query := tx.Model(&model.AlertRuleHit{})
switch {
case len(ruleIDs) > 0 && len(eventIDs) > 0:
query = query.Where("rule_id IN ? OR log_event_id IN ?", ruleIDs, eventIDs)
case len(ruleIDs) > 0:
query = query.Where("rule_id IN ?", ruleIDs)
case len(eventIDs) > 0:
query = query.Where("log_event_id IN ?", eventIDs)
default:
return nil
}
if err := query.Delete(&model.AlertRuleHit{}).Error; err != nil {
return fmt.Errorf("delete tenant alert hits: %w", err)
}
return nil
}
func deleteTenantAI(tx *gorm.DB, id uint, result *tenantDeleteResult) error {
channelIDs, err := lockedAiChannelIDs(tx, id)
if err != nil {
return fmt.Errorf("load tenant AI channels: %w", err)
}
if len(channelIDs) == 0 {
return nil
}
callIDs, err := lockedAiCallIDs(tx, channelIDs)
if err != nil {
return fmt.Errorf("load tenant AI calls: %w", err)
}
if err := deleteTenantAIRows(tx, channelIDs, callIDs); err != nil {
return err
}
result.channelsGone = true
return reconcileAiProbeRows(tx, result)
}
func lockedAiChannelIDs(tx *gorm.DB, id uint) ([]uint, error) {
var rows []model.AiChannel
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Select("id").
Where("oci_config_id = ?", id).Order("id").Find(&rows).Error
ids := make([]uint, 0, len(rows))
for _, row := range rows {
ids = append(ids, row.ID)
}
return ids, err
}
func lockedAiCallIDs(tx *gorm.DB, channelIDs []uint) ([]uint, error) {
var rows []model.AiCallLog
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Select("id").
Where("channel_id IN ?", channelIDs).Order("id").Find(&rows).Error
ids := make([]uint, 0, len(rows))
for _, row := range rows {
ids = append(ids, row.ID)
}
return ids, err
}
func deleteTenantAIRows(tx *gorm.DB, channelIDs, callIDs []uint) error {
if len(callIDs) > 0 {
if err := deleteWhere(tx, &model.AiContentLog{}, "call_log_id IN ?", callIDs); err != nil {
return fmt.Errorf("delete tenant AI content logs: %w", err)
}
}
steps := []struct {
name string
value any
}{
{"AI call logs", &model.AiCallLog{}},
{"AI model cache", &model.AiModelCache{}},
{"AI channels", &model.AiChannel{}},
}
for _, step := range steps {
if err := deleteAIChannelRows(tx, step.value, channelIDs); err != nil {
return fmt.Errorf("delete tenant %s: %w", step.name, err)
}
}
return nil
}
func deleteAIChannelRows(tx *gorm.DB, value any, channelIDs []uint) error {
column := "channel_id"
if _, ok := value.(*model.AiChannel); ok {
column = "id"
}
return deleteWhere(tx, value, column+" IN ?", channelIDs)
}
func reconcileAiProbeRows(tx *gorm.DB, result *tenantDeleteResult) error {
var channelCount int64
if err := tx.Model(&model.AiChannel{}).Count(&channelCount).Error; err != nil {
return fmt.Errorf("count remaining AI channels: %w", err)
}
var ids []uint
err := tx.Model(&model.Task{}).Where("type = ?", model.TaskTypeAiProbe).Pluck("id", &ids).Error
if err != nil {
return fmt.Errorf("load AI probe task: %w", err)
}
for _, id := range ids {
if err := deleteTaskHistory(tx, id); err != nil {
return err
}
if channelCount == 0 {
if err := tx.Delete(&model.Task{}, id).Error; err != nil {
return fmt.Errorf("delete AI probe task %d: %w", id, err)
}
result.deletedTaskIDs = append(result.deletedTaskIDs, id)
continue
}
if err := resetTaskHistoryFields(tx, id); err != nil {
return err
}
}
return nil
}
func resetTaskHistoryFields(tx *gorm.DB, id uint) error {
updates := map[string]any{"last_run_at": nil, "last_error": "", "run_count": 0}
if err := tx.Model(&model.Task{}).Where("id = ?", id).Updates(updates).Error; err != nil {
return fmt.Errorf("reset task %d history: %w", id, err)
}
return nil
}
func deleteTenantSnapshots(tx *gorm.DB, id uint) error {
steps := []struct {
name string
value any
}{
{"check snapshots", &model.CheckSnapshot{}},
{"cost snapshots", &model.CostSnapshot{}},
{"region cache", &model.RegionCache{}},
{"compartment cache", &model.CompartmentCache{}},
}
for _, step := range steps {
if err := deleteWhere(tx, step.value, "oci_config_id = ?", id); err != nil {
return fmt.Errorf("delete tenant %s: %w", step.name, err)
}
}
if err := tx.Where("key = ?", secretKey(id)).Delete(&model.Setting{}).Error; err != nil {
return fmt.Errorf("delete tenant webhook secret: %w", err)
}
return nil
}
func deleteTenantConfig(tx *gorm.DB, id uint) error {
res := tx.Delete(&model.OciConfig{}, id)
if res.Error != nil {
return fmt.Errorf("delete tenant row: %w", res.Error)
}
if res.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
func deleteWhere(tx *gorm.DB, value any, query string, args ...any) error {
return tx.Where(query, args...).Delete(value).Error
}
func (s *OciConfigService) afterTenantDelete(ctx context.Context, result *tenantDeleteResult) {
// 数据已提交,客户端断开不应中断必需的运行时对齐。
ctx = context.WithoutCancel(ctx)
s.InvalidateAuditCache(result.config.ID)
if client, ok := s.client.(tenancyCacheInvalidator); ok {
client.InvalidateTenancy(result.config.TenancyOCID)
}
if s.cleanupEvents != nil {
s.cleanupEvents.ClearAlertCooldown(result.alertRuleIDs)
}
if s.cleanupTasks != nil {
s.cleanupTasks.ApplyTenantCleanup(ctx, result.deletedTaskIDs, result.channelsGone)
}
}