发布 0.1.0:通知渠道、告警规则、令牌版本与安全加固
This commit is contained in:
@@ -0,0 +1,457 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user