优化结构
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
integrationbiz "kra/internal/biz/integration"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"kra/internal/integration/runtimeconfig"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ConfigPO struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Kind string `gorm:"size:32;not null;uniqueIndex:idx_integration_kind_provider"`
|
||||
Provider string `gorm:"size:64;not null;uniqueIndex:idx_integration_kind_provider"`
|
||||
Enabled bool
|
||||
Config string `gorm:"type:text;not null"`
|
||||
}
|
||||
|
||||
func (ConfigPO) TableName() string { return "sys_integration_configs" }
|
||||
|
||||
type integrationConfigRepo struct{ data Provider }
|
||||
|
||||
type integrationRuntimeProvider interface {
|
||||
IntegrationRuntime() *runtimeconfig.Store
|
||||
}
|
||||
|
||||
func NewIntegrationConfigRepo(data Provider) integrationbiz.IntegrationConfigRepo {
|
||||
return &integrationConfigRepo{data: data}
|
||||
}
|
||||
|
||||
func (r *integrationConfigRepo) ListIntegrationConfigs(ctx context.Context, kind string) ([]*integrationbiz.IntegrationConfig, error) {
|
||||
var rows []ConfigPO
|
||||
if err := r.data.DB().WithContext(ctx).Where("kind = ?", kind).Order("provider ASC").Find(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]*integrationbiz.IntegrationConfig, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
result = append(result, integrationConfigFromPO(row))
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *integrationConfigRepo) FindIntegrationConfig(ctx context.Context, kind, provider string) (*integrationbiz.IntegrationConfig, error) {
|
||||
var row ConfigPO
|
||||
if err := r.data.DB().WithContext(ctx).Where("kind = ? AND provider = ?", kind, provider).First(&row).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("集成配置不存在")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return integrationConfigFromPO(row), nil
|
||||
}
|
||||
|
||||
func (r *integrationConfigRepo) SaveIntegrationConfig(ctx context.Context, config *integrationbiz.IntegrationConfig) error {
|
||||
db := r.data.DB().WithContext(ctx)
|
||||
var row ConfigPO
|
||||
err := db.Where("kind = ? AND provider = ?", config.Kind, config.Provider).First(&row).Error
|
||||
values := integrationObject(config.Values)
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
if config.Enabled {
|
||||
if err = integrationbiz.ValidateIntegrationConfig(config.Kind, config.Provider, values); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
encoded, _ := json.Marshal(values)
|
||||
if err := db.Create(&ConfigPO{Kind: config.Kind, Provider: config.Provider, Enabled: config.Enabled, Config: string(encoded)}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
r.publish(config.Kind, config.Provider, config.Enabled, encoded)
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mergeIntegrationSecrets(config.Kind, config.Provider, values, integrationObject(json.RawMessage(row.Config)))
|
||||
if config.Enabled {
|
||||
if err = integrationbiz.ValidateIntegrationConfig(config.Kind, config.Provider, values); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
encoded, _ := json.Marshal(values)
|
||||
if err := db.Model(&row).Updates(map[string]any{"enabled": config.Enabled, "config": string(encoded)}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
r.publish(config.Kind, config.Provider, config.Enabled, encoded)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *integrationConfigRepo) DeleteIntegrationConfig(ctx context.Context, kind, provider string) error {
|
||||
if err := r.data.DB().WithContext(ctx).Where("kind = ? AND provider = ?", kind, provider).Delete(&ConfigPO{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if runtime := integrationRuntime(r.data); runtime != nil {
|
||||
runtime.Delete(kind, provider)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *integrationConfigRepo) publish(kind, provider string, enabled bool, values []byte) {
|
||||
if runtime := integrationRuntime(r.data); runtime != nil {
|
||||
runtime.Set(runtimeconfig.Config{Kind: kind, Provider: provider, Enabled: enabled, Values: values})
|
||||
}
|
||||
}
|
||||
|
||||
func integrationRuntime(provider Provider) *runtimeconfig.Store {
|
||||
if value, ok := provider.(integrationRuntimeProvider); ok {
|
||||
return value.IntegrationRuntime()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func integrationConfigFromPO(row ConfigPO) *integrationbiz.IntegrationConfig {
|
||||
values := integrationObject(json.RawMessage(row.Config))
|
||||
maskIntegrationSecrets(row.Kind, row.Provider, values)
|
||||
encoded, _ := json.Marshal(values)
|
||||
return &integrationbiz.IntegrationConfig{Kind: row.Kind, Provider: row.Provider, Enabled: row.Enabled, Values: encoded}
|
||||
}
|
||||
|
||||
func integrationObject(raw json.RawMessage) map[string]any {
|
||||
values := map[string]any{}
|
||||
_ = json.Unmarshal(raw, &values)
|
||||
return values
|
||||
}
|
||||
|
||||
func maskIntegrationSecrets(kind, provider string, values map[string]any) {
|
||||
secretFields := integrationSecretFields(kind, provider)
|
||||
for key, value := range values {
|
||||
if secretFields[key] || likelyIntegrationSecret(key) {
|
||||
if text, ok := value.(string); ok && text != "" {
|
||||
values[key] = "******"
|
||||
}
|
||||
continue
|
||||
}
|
||||
if nested, ok := value.(map[string]any); ok {
|
||||
maskIntegrationSecrets(kind, provider, nested)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mergeIntegrationSecrets(kind, provider string, values, old map[string]any) {
|
||||
secretFields := integrationSecretFields(kind, provider)
|
||||
for key, value := range values {
|
||||
if secretFields[key] || likelyIntegrationSecret(key) {
|
||||
if text, ok := value.(string); ok && text == "******" {
|
||||
if prior, exists := old[key]; exists {
|
||||
values[key] = prior
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if nested, ok := value.(map[string]any); ok {
|
||||
if prior, ok := old[key].(map[string]any); ok {
|
||||
mergeIntegrationSecrets(kind, provider, nested, prior)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func integrationSecretFields(kind, provider string) map[string]bool {
|
||||
result := map[string]bool{}
|
||||
if definition, ok := integrationbiz.IntegrationDefinition(kind, provider); ok {
|
||||
for _, field := range definition.Fields {
|
||||
if field.Secret {
|
||||
result[field.Key] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func likelyIntegrationSecret(key string) bool {
|
||||
normalized := strings.ToLower(strings.ReplaceAll(key, "-", "_"))
|
||||
if strings.Contains(normalized, "secret") || strings.Contains(normalized, "private") || strings.Contains(normalized, "password") || strings.Contains(normalized, "credential") {
|
||||
return true
|
||||
}
|
||||
for _, item := range []string{"key", "token", "access_token", "api_key", "mch_key", "client_key", "certificate", "cert", "p12", "pkcs12", "public_key", "platform_cert", "root_cert"} {
|
||||
if normalized == item || strings.HasSuffix(normalized, "_"+item) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
integrationbiz "kra/internal/biz/integration"
|
||||
"testing"
|
||||
|
||||
"kra/internal/integration/runtimeconfig"
|
||||
)
|
||||
|
||||
type integrationRuntimeTestProvider struct {
|
||||
*Data
|
||||
store *runtimeconfig.Store
|
||||
}
|
||||
|
||||
func (p *integrationRuntimeTestProvider) IntegrationRuntime() *runtimeconfig.Store { return p.store }
|
||||
|
||||
func TestIntegrationConfigSavePublishesUnmaskedRuntimeValues(t *testing.T) {
|
||||
db, err := openWithDriver("sqlite", "file:"+t.Name()+"?mode=memory&cache=shared")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = db.AutoMigrate(&ConfigPO{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
provider := &integrationRuntimeTestProvider{Data: &Data{gormDB: newReloadableDB(db, nil)}, store: runtimeconfig.NewStore()}
|
||||
repo := &integrationConfigRepo{data: provider}
|
||||
|
||||
values := integrationbiz.DefaultIntegrationConfig(integrationbiz.IntegrationKindMQ, "rabbitmq")
|
||||
values["password"] = "runtime-secret"
|
||||
raw, _ := json.Marshal(values)
|
||||
if err = repo.SaveIntegrationConfig(context.Background(), &integrationbiz.IntegrationConfig{Kind: integrationbiz.IntegrationKindMQ, Provider: "rabbitmq", Enabled: true, Values: raw}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
values["password"] = "******"
|
||||
raw, _ = json.Marshal(values)
|
||||
if err = repo.SaveIntegrationConfig(context.Background(), &integrationbiz.IntegrationConfig{Kind: integrationbiz.IntegrationKindMQ, Provider: "rabbitmq", Enabled: true, Values: raw}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
active, ok := provider.store.Get(integrationbiz.IntegrationKindMQ, "rabbitmq")
|
||||
if !ok || !active.Enabled {
|
||||
t.Fatalf("runtime config = %#v, ok=%v", active, ok)
|
||||
}
|
||||
stored := map[string]any{}
|
||||
if err = json.Unmarshal(active.Values, &stored); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stored["password"] != "runtime-secret" {
|
||||
t.Fatalf("runtime password = %#v", stored["password"])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
integrationbiz "kra/internal/biz/integration"
|
||||
"kra/pkg/database/migration"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func Migrations() []migration.Step {
|
||||
return []migration.Step{
|
||||
{ID: "202608200001_data_infrastructure", Migrate: func(db *gorm.DB) error {
|
||||
return migration.CreateMissingTables(db, &ConfigPO{})
|
||||
}},
|
||||
{ID: "202608210001_communication_integration_defaults", Migrate: ensureCommunicationIntegrationConfigs},
|
||||
}
|
||||
}
|
||||
|
||||
func ensureCommunicationIntegrationConfigs(db *gorm.DB) error {
|
||||
defaults := []struct{ kind, provider string }{
|
||||
{integrationbiz.IntegrationKindMQ, "emqx"},
|
||||
{integrationbiz.IntegrationKindMQ, "rabbitmq"},
|
||||
{integrationbiz.IntegrationKindWebSocket, "melody"},
|
||||
}
|
||||
for _, item := range defaults {
|
||||
var row ConfigPO
|
||||
err := db.Where("kind = ? AND provider = ?", item.kind, item.provider).First(&row).Error
|
||||
if gorm.ErrRecordNotFound == err {
|
||||
values, marshalErr := json.Marshal(integrationbiz.DefaultIntegrationConfig(item.kind, item.provider))
|
||||
if marshalErr != nil {
|
||||
return marshalErr
|
||||
}
|
||||
if err = db.Create(&ConfigPO{Kind: item.kind, Provider: item.provider, Enabled: false, Config: string(values)}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"kra/internal/integration/runtimeconfig"
|
||||
)
|
||||
|
||||
// Provider is the minimal database/runtime seam for integration configuration.
|
||||
type Provider interface {
|
||||
DB() *gorm.DB
|
||||
IntegrationRuntime() *runtimeconfig.Store
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package integration
|
||||
|
||||
import "github.com/google/wire"
|
||||
|
||||
var ProviderSet = wire.NewSet(NewIntegrationConfigRepo)
|
||||
@@ -0,0 +1,29 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"kra/internal/integration/runtimeconfig"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ReadRuntime returns the communication integration snapshot used by the
|
||||
// long-lived MQ/WebSocket adapters.
|
||||
func ReadRuntime(db *gorm.DB) ([]runtimeconfig.Config, error) {
|
||||
if db == nil || !db.Migrator().HasTable(&ConfigPO{}) {
|
||||
return nil, nil
|
||||
}
|
||||
var rows []ConfigPO
|
||||
if err := db.Session(&gorm.Session{NewDB: true}).
|
||||
Where("kind IN ?", []string{"mq", "websocket"}).
|
||||
Order("kind ASC, provider ASC").
|
||||
Find(&rows).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
configs := make([]runtimeconfig.Config, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
configs = append(configs, runtimeconfig.Config{Kind: row.Kind, Provider: row.Provider, Enabled: row.Enabled, Values: []byte(row.Config)})
|
||||
}
|
||||
return configs, nil
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"kra/internal/integration/runtimeconfig"
|
||||
)
|
||||
|
||||
type Data struct {
|
||||
gormDB *reloadableDB
|
||||
store *runtimeconfig.Store
|
||||
}
|
||||
|
||||
func (d *Data) DB() *gorm.DB {
|
||||
if d == nil || d.gormDB == nil {
|
||||
return nil
|
||||
}
|
||||
return d.gormDB.DB()
|
||||
}
|
||||
|
||||
func (d *Data) IntegrationRuntime() *runtimeconfig.Store { return d.store }
|
||||
|
||||
type reloadableDB struct{ db *gorm.DB }
|
||||
|
||||
func newReloadableDB(db *gorm.DB, _ ...any) *reloadableDB { return &reloadableDB{db: db} }
|
||||
func (r *reloadableDB) DB() *gorm.DB {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return r.db
|
||||
}
|
||||
func (r *reloadableDB) WithContext(ctx context.Context) *gorm.DB { return r.db.WithContext(ctx) }
|
||||
|
||||
func openWithDriver(driver, dsn string) (*gorm.DB, error) {
|
||||
if driver != "sqlite" {
|
||||
return nil, fmt.Errorf("unsupported test database driver %q", driver)
|
||||
}
|
||||
return gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
}
|
||||
Reference in New Issue
Block a user