优化结构
This commit is contained in:
+53
-235
@@ -3,144 +3,24 @@ package data
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"kra/internal/config"
|
||||
dataintegration "kra/internal/data/integration"
|
||||
"kra/internal/integration/storage"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func configMap(input any) (map[string]any, error) {
|
||||
raw, err := yaml.Marshal(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var output map[string]any
|
||||
if err = yaml.Unmarshal(raw, &output); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
// Writing config.yaml goes through config.Document, which owns the file format
|
||||
// for both directions. This file only decides which sections to write.
|
||||
|
||||
func setYAMLMapping(node *yaml.Node, key string, value any) error {
|
||||
if node.Kind == yaml.DocumentNode {
|
||||
node = node.Content[0]
|
||||
// cloneAdminConfig copies an admin section, normalizing nil to an empty value so
|
||||
// callers can edit the result without a nil check.
|
||||
func cloneAdminConfig(value *config.Admin) *config.Admin {
|
||||
if value == nil {
|
||||
return &config.Admin{}
|
||||
}
|
||||
if node.Kind != yaml.MappingNode {
|
||||
return fmt.Errorf("configuration root is not a mapping")
|
||||
}
|
||||
raw, err := yaml.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var replacement yaml.Node
|
||||
if err = yaml.Unmarshal(raw, &replacement); err != nil {
|
||||
return err
|
||||
}
|
||||
for i := 0; i < len(node.Content); i += 2 {
|
||||
if node.Content[i].Value == key {
|
||||
mergeYAMLNode(node.Content[i+1], replacement.Content[0])
|
||||
return nil
|
||||
}
|
||||
}
|
||||
node.Content = append(node.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: key}, replacement.Content[0])
|
||||
return nil
|
||||
}
|
||||
|
||||
// mergeYAMLNode updates values produced from the runtime configuration while
|
||||
// retaining keys that are not represented by the typed configuration and
|
||||
// comments that were already present in the user's config file.
|
||||
func mergeYAMLNode(dst, src *yaml.Node) {
|
||||
if dst.Kind == yaml.MappingNode && src.Kind == yaml.MappingNode {
|
||||
for i := 0; i+1 < len(src.Content); i += 2 {
|
||||
key := src.Content[i].Value
|
||||
found := false
|
||||
for j := 0; j+1 < len(dst.Content); j += 2 {
|
||||
if dst.Content[j].Value == key {
|
||||
mergeYAMLNode(dst.Content[j+1], src.Content[i+1])
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
dst.Content = append(dst.Content, src.Content[i], src.Content[i+1])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Keep comments attached to a scalar/sequence node when its value changes.
|
||||
head, line, foot := dst.HeadComment, dst.LineComment, dst.FootComment
|
||||
*dst = *src
|
||||
dst.HeadComment, dst.LineComment, dst.FootComment = head, line, foot
|
||||
}
|
||||
|
||||
func yamlMappingValue(node *yaml.Node, keys ...string) *yaml.Node {
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
if node.Kind == yaml.DocumentNode && len(node.Content) > 0 {
|
||||
node = node.Content[0]
|
||||
}
|
||||
for _, key := range keys {
|
||||
if node.Kind != yaml.MappingNode {
|
||||
return nil
|
||||
}
|
||||
var next *yaml.Node
|
||||
for i := 0; i+1 < len(node.Content); i += 2 {
|
||||
if node.Content[i].Value == key {
|
||||
next = node.Content[i+1]
|
||||
break
|
||||
}
|
||||
}
|
||||
if next == nil {
|
||||
return nil
|
||||
}
|
||||
node = next
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
func deleteYAMLMapping(node *yaml.Node, keys ...string) {
|
||||
if len(keys) == 0 || node == nil {
|
||||
return
|
||||
}
|
||||
if node.Kind == yaml.DocumentNode && len(node.Content) > 0 {
|
||||
node = node.Content[0]
|
||||
}
|
||||
if node.Kind != yaml.MappingNode {
|
||||
return
|
||||
}
|
||||
key := keys[0]
|
||||
for i := 0; i+1 < len(node.Content); i += 2 {
|
||||
if node.Content[i].Value != key {
|
||||
continue
|
||||
}
|
||||
if len(keys) == 1 {
|
||||
node.Content = append(node.Content[:i], node.Content[i+2:]...)
|
||||
return
|
||||
}
|
||||
deleteYAMLMapping(node.Content[i+1], keys[1:]...)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func setServerHTTPPort(document *yaml.Node, port int) error {
|
||||
if port <= 0 {
|
||||
return nil
|
||||
}
|
||||
host := "0.0.0.0"
|
||||
if addr := yamlMappingValue(document, "server", "http", "addr"); addr != nil {
|
||||
if currentHost, _, err := net.SplitHostPort(addr.Value); err == nil && currentHost != "" {
|
||||
host = currentHost
|
||||
}
|
||||
}
|
||||
return setYAMLMapping(document, "server", map[string]any{"http": map[string]any{"addr": net.JoinHostPort(host, strconv.Itoa(int(port)))}})
|
||||
return config.CloneAdmin(value)
|
||||
}
|
||||
|
||||
func (d *Data) persistConfig() error {
|
||||
@@ -158,40 +38,28 @@ func (d *Data) persistConfigValuesLocked(dataConfig *config.Data, adminConfig *c
|
||||
if adminConfig == nil || adminConfig.ConfigPath == "" {
|
||||
return nil
|
||||
}
|
||||
configPath := adminConfig.ConfigPath
|
||||
raw, err := os.ReadFile(configPath)
|
||||
document, err := config.OpenDocument(adminConfig.ConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var document yaml.Node
|
||||
if err = yaml.Unmarshal(raw, &document); err != nil {
|
||||
return err
|
||||
}
|
||||
dataValue, err := configMap(dataConfig)
|
||||
if err != nil {
|
||||
if err = document.Set("data", dataConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
// Storage and email live in the database; the file must not shadow them.
|
||||
fileAdmin := cloneAdminConfig(adminConfig)
|
||||
fileAdmin.Storage = nil
|
||||
fileAdmin.Email = nil
|
||||
adminValue, err := configMap(fileAdmin)
|
||||
if err != nil {
|
||||
if err = document.Set("admin", fileAdmin); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = setYAMLMapping(&document, "data", dataValue); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = setYAMLMapping(&document, "admin", adminValue); err != nil {
|
||||
return err
|
||||
}
|
||||
deleteYAMLMapping(&document, "admin", "storage")
|
||||
deleteYAMLMapping(&document, "admin", "email")
|
||||
document.Delete("admin", "storage")
|
||||
document.Delete("admin", "email")
|
||||
if adminConfig.System != nil {
|
||||
if err = setServerHTTPPort(&document, adminConfig.System.Addr); err != nil {
|
||||
if err = document.SetServerHTTPPort(adminConfig.System.Addr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return writeConfigDocument(configPath, &document)
|
||||
return document.Save()
|
||||
}
|
||||
|
||||
// persistDatabaseConfig writes only the database selected on the init page and
|
||||
@@ -207,14 +75,10 @@ func (d *Data) persistDatabaseConfig(database *config.Database, signingKey strin
|
||||
if configPath == "" {
|
||||
return nil
|
||||
}
|
||||
raw, err := os.ReadFile(configPath)
|
||||
document, err := config.OpenDocument(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var document yaml.Node
|
||||
if err = yaml.Unmarshal(raw, &document); err != nil {
|
||||
return err
|
||||
}
|
||||
source, err := databaseDSN(database, "")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -232,17 +96,17 @@ func (d *Data) persistDatabaseConfig(database *config.Database, signingKey strin
|
||||
"config": database.Config,
|
||||
"path": database.Path,
|
||||
}
|
||||
if err = setYAMLMapping(&document, "data", map[string]any{"database": databaseValue}); err != nil {
|
||||
if err = document.Set("data", map[string]any{"database": databaseValue}); err != nil {
|
||||
return err
|
||||
}
|
||||
if signingKey != "" {
|
||||
if err = setYAMLMapping(&document, "admin", map[string]any{"jwt": map[string]any{"signing_key": signingKey}}); err != nil {
|
||||
if err = document.Set("admin", map[string]any{"jwt": map[string]any{"signing_key": signingKey}}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
deleteYAMLMapping(&document, "admin", "storage")
|
||||
deleteYAMLMapping(&document, "admin", "email")
|
||||
return writeConfigDocument(configPath, &document)
|
||||
document.Delete("admin", "storage")
|
||||
document.Delete("admin", "email")
|
||||
return document.Save()
|
||||
}
|
||||
|
||||
func (d *Data) removeIntegrationConfigFromFile() error {
|
||||
@@ -252,48 +116,16 @@ func (d *Data) removeIntegrationConfigFromFile() error {
|
||||
if configPath == "" {
|
||||
return nil
|
||||
}
|
||||
raw, err := os.ReadFile(configPath)
|
||||
document, err := config.OpenDocument(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var document yaml.Node
|
||||
if err = yaml.Unmarshal(raw, &document); err != nil {
|
||||
return err
|
||||
}
|
||||
if yamlMappingValue(&document, "admin", "storage") == nil && yamlMappingValue(&document, "admin", "email") == nil {
|
||||
if !document.Has("admin", "storage") && !document.Has("admin", "email") {
|
||||
return nil
|
||||
}
|
||||
deleteYAMLMapping(&document, "admin", "storage")
|
||||
deleteYAMLMapping(&document, "admin", "email")
|
||||
return writeConfigDocument(configPath, &document)
|
||||
}
|
||||
|
||||
func writeConfigDocument(configPath string, document *yaml.Node) error {
|
||||
output, err := yaml.Marshal(document)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
temporary, err := os.CreateTemp(filepath.Dir(configPath), ".kra-config-*.yaml")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tempName := temporary.Name()
|
||||
defer os.Remove(tempName)
|
||||
if _, err = temporary.Write(output); err != nil {
|
||||
_ = temporary.Close()
|
||||
return err
|
||||
}
|
||||
if err = temporary.Chmod(0o600); err != nil {
|
||||
_ = temporary.Close()
|
||||
return err
|
||||
}
|
||||
if err = temporary.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tempName, configPath)
|
||||
document.Delete("admin", "storage")
|
||||
document.Delete("admin", "email")
|
||||
return document.Save()
|
||||
}
|
||||
|
||||
func (d *Data) reloadConfig(ctx context.Context) error {
|
||||
@@ -311,6 +143,11 @@ func (d *Data) reloadConfig(ctx context.Context) error {
|
||||
return fmt.Errorf("data.database and admin configuration are required")
|
||||
}
|
||||
next.Admin.ConfigPath = configPath
|
||||
// Every client below is opened before anything is published, so a failure at
|
||||
// any step leaves the running process untouched. undo closes whatever this
|
||||
// call opened; commit at the end hands all of it over instead.
|
||||
var undo rollback
|
||||
defer undo.run()
|
||||
databaseReady := databaseConnectionConfigured(next.Data.Database)
|
||||
var candidateDB *gorm.DB
|
||||
if databaseReady {
|
||||
@@ -324,14 +161,7 @@ func (d *Data) reloadConfig(ctx context.Context) error {
|
||||
return fmt.Errorf("reload bootstrap database: %w", err)
|
||||
}
|
||||
}
|
||||
closeCandidate := true
|
||||
defer func() {
|
||||
if closeCandidate {
|
||||
if sqlDB, closeErr := candidateDB.DB(); closeErr == nil {
|
||||
_ = sqlDB.Close()
|
||||
}
|
||||
}
|
||||
}()
|
||||
undo.add(func() { closeGormDB(candidateDB) })
|
||||
if databaseReady {
|
||||
if sqlDB, dbErr := candidateDB.DB(); dbErr != nil {
|
||||
return dbErr
|
||||
@@ -355,12 +185,12 @@ func (d *Data) reloadConfig(ctx context.Context) error {
|
||||
if legacyEmail == nil && currentAdmin != nil {
|
||||
legacyEmail = currentAdmin.Email
|
||||
}
|
||||
storageConfig, err := resolveStorageIntegrationConfig(candidateDB.WithContext(ctx), legacyStorage)
|
||||
storageConfig, err := dataintegration.ResolveStorageConfig(candidateDB.WithContext(ctx), legacyStorage)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reload storage configuration: %w", err)
|
||||
}
|
||||
next.Admin.Storage = storageConfig
|
||||
emailConfig, err := resolveEmailIntegrationConfig(candidateDB.WithContext(ctx), legacyEmail)
|
||||
emailConfig, err := dataintegration.ResolveEmailConfig(candidateDB.WithContext(ctx), legacyEmail)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reload email configuration: %w", err)
|
||||
}
|
||||
@@ -371,47 +201,37 @@ func (d *Data) reloadConfig(ctx context.Context) error {
|
||||
}
|
||||
useRedis := next.Admin.System != nil && next.Admin.System.UseRedis
|
||||
candidateRedis := openRedis(next.Data.Redis, useRedis, d.logger())
|
||||
candidateRedisAccepted := false
|
||||
defer func() {
|
||||
if !candidateRedisAccepted && candidateRedis != nil {
|
||||
_ = candidateRedis.Close()
|
||||
undo.add(func() {
|
||||
if candidateRedis != nil {
|
||||
closeRedisClient(candidateRedis)
|
||||
}
|
||||
}()
|
||||
})
|
||||
useRedisList := useRedis && next.Admin.System.UseMultipoint
|
||||
candidateRedisList := openRedisList(next.Data.RedisList, useRedisList, d.logger())
|
||||
candidateRedisListAccepted := false
|
||||
defer func() {
|
||||
if !candidateRedisListAccepted {
|
||||
closeRedisList(candidateRedisList)
|
||||
}
|
||||
}()
|
||||
undo.add(func() { closeRedisList(candidateRedisList) })
|
||||
useMongo := next.Admin.System != nil && next.Admin.System.UseMongo
|
||||
// openMongo always reports a nil client alongside its error, so an unreachable
|
||||
// Mongo only warns: it must not fail an otherwise valid reload.
|
||||
candidateMongo, mongoErr := openMongo(next.Data.Mongo, useMongo)
|
||||
if mongoErr != nil {
|
||||
d.logger().Error("mongo unavailable during configuration reload", "mod", "mongo", "error", mongoErr)
|
||||
}
|
||||
mongoAccepted := false
|
||||
defer func() {
|
||||
if !mongoAccepted && candidateMongo != nil {
|
||||
_ = candidateMongo.Disconnect(context.Background())
|
||||
undo.add(func() {
|
||||
if candidateMongo != nil {
|
||||
closeMongoClient(candidateMongo)
|
||||
}
|
||||
}()
|
||||
})
|
||||
candidateDBList, err := openDatabaseList(next.Data.DatabaseList, d.logger())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
candidateDBListAccepted := false
|
||||
defer func() {
|
||||
if !candidateDBListAccepted {
|
||||
closeDatabaseList(candidateDBList)
|
||||
}
|
||||
}()
|
||||
integrationConfigs, err := readIntegrationRuntime(candidateDB.WithContext(ctx))
|
||||
undo.add(func() { closeDatabaseList(candidateDBList) })
|
||||
integrationConfigs, err := dataintegration.ReadRuntime(candidateDB.WithContext(ctx))
|
||||
if err != nil {
|
||||
return fmt.Errorf("reload integration runtime: %w", err)
|
||||
}
|
||||
|
||||
d.gormDB.replace(candidateDB, d.enqueueDataScopeAudit)
|
||||
d.replacePrimaryDB(candidateDB)
|
||||
d.databaseReady.Store(databaseReady)
|
||||
for _, item := range candidateDBList {
|
||||
registerDataScopeCallbacks(item, d.enqueueDataScopeAudit)
|
||||
@@ -431,14 +251,12 @@ func (d *Data) reloadConfig(ctx context.Context) error {
|
||||
} else {
|
||||
d.logger().Warn("keeping the previous redis list because the reloaded configuration failed to connect", "mod", "redis")
|
||||
}
|
||||
// A nil client here means Mongo is switched off in the reloaded configuration,
|
||||
// which must still retire the running client; only an outright failure keeps it.
|
||||
if mongoErr == nil {
|
||||
d.mongo.replace(candidateMongo)
|
||||
mongoAccepted = true
|
||||
}
|
||||
closeCandidate = false
|
||||
candidateDBListAccepted = true
|
||||
candidateRedisAccepted = true
|
||||
candidateRedisListAccepted = true
|
||||
undo.commit()
|
||||
d.runtime.Replace(next)
|
||||
if d.integrations != nil {
|
||||
d.integrations.Replace(integrationConfigs)
|
||||
|
||||
Reference in New Issue
Block a user