This commit is contained in:
parent
e5b88fcf6c
commit
c4b114762b
|
|
@ -1,22 +1,30 @@
|
||||||
server:
|
server:
|
||||||
http:
|
http:
|
||||||
|
network: tcp
|
||||||
addr: 0.0.0.0:8000
|
addr: 0.0.0.0:8000
|
||||||
timeout: 600s
|
timeout: 600s
|
||||||
data:
|
data:
|
||||||
database:
|
database:
|
||||||
driver: mysql
|
driver: mysql
|
||||||
source: root:root@tcp(127.0.0.1:3306)/test?timeout=5s&parseTime=True&loc=Local&charset=utf8mb4
|
source: root:root@tcp(127.0.0.1:3306)/kra?timeout=5s&parseTime=True&loc=Local&charset=utf8mb4
|
||||||
host: 127.0.0.1
|
host: 127.0.0.1
|
||||||
port: "3306"
|
port: "3306"
|
||||||
user: root
|
user: root
|
||||||
password: "12345678"
|
password: "12345678"
|
||||||
name: test
|
name: kra
|
||||||
config: timeout=5s&parseTime=True&loc=Local&charset=utf8mb4
|
config: timeout=5s&parseTime=True&loc=Local&charset=utf8mb4
|
||||||
|
path: ""
|
||||||
|
alias_name: ""
|
||||||
|
disable: false
|
||||||
|
prefix: ""
|
||||||
|
engine: InnoDB
|
||||||
log_mode: info
|
log_mode: info
|
||||||
max_idle_conns: 10
|
max_idle_conns: 10
|
||||||
max_open_conns: 100
|
max_open_conns: 100
|
||||||
conn_max_lifetime: 3600
|
conn_max_lifetime: 3600
|
||||||
|
singular: false
|
||||||
redis:
|
redis:
|
||||||
|
network: tcp
|
||||||
name: default
|
name: default
|
||||||
addr: 127.0.0.1:6379
|
addr: 127.0.0.1:6379
|
||||||
password: ""
|
password: ""
|
||||||
|
|
@ -25,11 +33,58 @@ data:
|
||||||
cluster_addrs: []
|
cluster_addrs: []
|
||||||
read_timeout: 0.2s
|
read_timeout: 0.2s
|
||||||
write_timeout: 0.2s
|
write_timeout: 0.2s
|
||||||
|
# Additional databases use alias_name as the lookup key. Disabled entries
|
||||||
|
# remain available as configuration examples without opening connections.
|
||||||
database_list: []
|
database_list: []
|
||||||
|
# Example:
|
||||||
|
# database_list:
|
||||||
|
# - driver: mysql
|
||||||
|
# source: ""
|
||||||
|
# host: 127.0.0.1
|
||||||
|
# port: "3306"
|
||||||
|
# user: root
|
||||||
|
# password: ""
|
||||||
|
# name: business
|
||||||
|
# config: timeout=5s&parseTime=True&loc=Local&charset=utf8mb4
|
||||||
|
# path: ""
|
||||||
|
# alias_name: business
|
||||||
|
# disable: true
|
||||||
|
# prefix: ""
|
||||||
|
# engine: InnoDB
|
||||||
|
# log_mode: info
|
||||||
|
# max_idle_conns: 10
|
||||||
|
# max_open_conns: 100
|
||||||
|
# conn_max_lifetime: 3600
|
||||||
|
# singular: false
|
||||||
redis_list: []
|
redis_list: []
|
||||||
|
# Example:
|
||||||
|
# redis_list:
|
||||||
|
# - network: tcp
|
||||||
|
# name: cache
|
||||||
|
# addr: 127.0.0.1:6379
|
||||||
|
# password: ""
|
||||||
|
# db: 0
|
||||||
|
# use_cluster: false
|
||||||
|
# cluster_addrs: []
|
||||||
|
# read_timeout: 0.2s
|
||||||
|
# write_timeout: 0.2s
|
||||||
mongo:
|
mongo:
|
||||||
hosts: []
|
coll: ""
|
||||||
|
options: ""
|
||||||
|
database: ""
|
||||||
|
username: ""
|
||||||
|
password: ""
|
||||||
|
auth_source: ""
|
||||||
|
min_pool_size: 0
|
||||||
|
max_pool_size: 100
|
||||||
|
socket_timeout_ms: 0
|
||||||
|
connect_timeout_ms: 0
|
||||||
|
is_zap: false
|
||||||
|
hosts:
|
||||||
|
- host: ""
|
||||||
|
port: ""
|
||||||
admin:
|
admin:
|
||||||
|
# GVA system.router-prefix maps to this transport prefix.
|
||||||
router_prefix: ""
|
router_prefix: ""
|
||||||
jwt:
|
jwt:
|
||||||
# Production deployments must override this value with a private secret.
|
# Production deployments must override this value with a private secret.
|
||||||
|
|
@ -46,6 +101,7 @@ admin:
|
||||||
store_path: uploads/file
|
store_path: uploads/file
|
||||||
path_prefix: uploads/file
|
path_prefix: uploads/file
|
||||||
media:
|
media:
|
||||||
|
# Upload chunks are stored below .chunks in the selected storage backend.
|
||||||
session_ttl: 24
|
session_ttl: 24
|
||||||
max_file_size: 0
|
max_file_size: 0
|
||||||
system:
|
system:
|
||||||
|
|
@ -72,6 +128,13 @@ admin:
|
||||||
cors:
|
cors:
|
||||||
mode: whitelist
|
mode: whitelist
|
||||||
whitelist: []
|
whitelist: []
|
||||||
|
# Example:
|
||||||
|
# whitelist:
|
||||||
|
# - allow_origin: https://admin.example.com
|
||||||
|
# allow_headers: Content-Type,Authorization,X-Token,X-User-Id
|
||||||
|
# allow_methods: POST,GET,PUT,DELETE,OPTIONS
|
||||||
|
# expose_headers: Content-Length,Content-Type
|
||||||
|
# allow_credentials: true
|
||||||
app:
|
app:
|
||||||
node: ""
|
node: ""
|
||||||
app_id: kra
|
app_id: kra
|
||||||
|
|
@ -82,6 +145,80 @@ admin:
|
||||||
# local, qiniu, aliyun-oss, huawei-obs, tencent-cos, aws-s3,
|
# local, qiniu, aliyun-oss, huawei-obs, tencent-cos, aws-s3,
|
||||||
# cloudflare-r2 or minio
|
# cloudflare-r2 or minio
|
||||||
type: local
|
type: local
|
||||||
|
qiniu:
|
||||||
|
zone: ZoneHuadong
|
||||||
|
bucket: ""
|
||||||
|
base_url: ""
|
||||||
|
access_key: ""
|
||||||
|
secret_key: ""
|
||||||
|
use_https: false
|
||||||
|
use_cdn_domains: false
|
||||||
|
aliyun_oss:
|
||||||
|
endpoint: ""
|
||||||
|
region: ""
|
||||||
|
bucket: ""
|
||||||
|
access_key: ""
|
||||||
|
secret_key: ""
|
||||||
|
base_url: ""
|
||||||
|
path_prefix: ""
|
||||||
|
use_ssl: true
|
||||||
|
force_path_style: false
|
||||||
|
account_id: ""
|
||||||
|
huawei_obs:
|
||||||
|
endpoint: ""
|
||||||
|
region: ""
|
||||||
|
bucket: ""
|
||||||
|
access_key: ""
|
||||||
|
secret_key: ""
|
||||||
|
base_url: ""
|
||||||
|
path_prefix: ""
|
||||||
|
use_ssl: true
|
||||||
|
force_path_style: false
|
||||||
|
account_id: ""
|
||||||
|
tencent_cos:
|
||||||
|
endpoint: ""
|
||||||
|
region: ""
|
||||||
|
bucket: ""
|
||||||
|
access_key: ""
|
||||||
|
secret_key: ""
|
||||||
|
base_url: ""
|
||||||
|
path_prefix: ""
|
||||||
|
use_ssl: true
|
||||||
|
force_path_style: false
|
||||||
|
account_id: ""
|
||||||
|
aws_s3:
|
||||||
|
endpoint: ""
|
||||||
|
region: ""
|
||||||
|
bucket: ""
|
||||||
|
access_key: ""
|
||||||
|
secret_key: ""
|
||||||
|
base_url: ""
|
||||||
|
path_prefix: ""
|
||||||
|
use_ssl: true
|
||||||
|
force_path_style: false
|
||||||
|
account_id: ""
|
||||||
|
cloudflare_r2:
|
||||||
|
endpoint: ""
|
||||||
|
region: auto
|
||||||
|
bucket: ""
|
||||||
|
access_key: ""
|
||||||
|
secret_key: ""
|
||||||
|
base_url: ""
|
||||||
|
path_prefix: uploads
|
||||||
|
use_ssl: true
|
||||||
|
force_path_style: false
|
||||||
|
account_id: ""
|
||||||
|
minio:
|
||||||
|
endpoint: ""
|
||||||
|
region: ""
|
||||||
|
bucket: ""
|
||||||
|
access_key: ""
|
||||||
|
secret_key: ""
|
||||||
|
base_url: ""
|
||||||
|
path_prefix: ""
|
||||||
|
use_ssl: false
|
||||||
|
force_path_style: true
|
||||||
|
account_id: ""
|
||||||
email:
|
email:
|
||||||
# Leave host/from/secret empty to disable SMTP error notifications.
|
# Leave host/from/secret empty to disable SMTP error notifications.
|
||||||
to: ""
|
to: ""
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,9 @@ func (r *initializationRepo) SaveConfigurationJSON(ctx context.Context, raw json
|
||||||
nextAdmin.Email.Secret = value.Email.Secret
|
nextAdmin.Email.Secret = value.Email.Secret
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := refreshDatabaseSources(nextData); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
nextAdmin.ConfigPath = currentAdmin.ConfigPath
|
nextAdmin.ConfigPath = currentAdmin.ConfigPath
|
||||||
dataRaw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(nextData)
|
dataRaw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(nextData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -151,6 +154,46 @@ func cloneDataConfig(value *conf.Data) *conf.Data {
|
||||||
return proto.Clone(value).(*conf.Data)
|
return proto.Clone(value).(*conf.Data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func refreshDatabaseSources(value *conf.Data) error {
|
||||||
|
if value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := refreshDatabaseSource(value.Database); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, database := range value.DatabaseList {
|
||||||
|
if database == nil || database.Disable {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := refreshDatabaseSource(database); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func refreshDatabaseSource(database *conf.Data_Database) error {
|
||||||
|
if database == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
hasStructuredConfig := database.Host != "" || database.Port != "" || database.User != "" ||
|
||||||
|
database.Password != "" || database.Name != "" || database.Config != "" || database.Path != ""
|
||||||
|
if !hasStructuredConfig {
|
||||||
|
// A source-only configuration is an intentional escape hatch for custom
|
||||||
|
// driver DSNs; do not reinterpret it as the structured form.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
previousSource := database.Source
|
||||||
|
database.Source = ""
|
||||||
|
source, err := databaseDSN(database, "")
|
||||||
|
if err != nil {
|
||||||
|
database.Source = previousSource
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
database.Source = source
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func durationString(value *durationpb.Duration) string {
|
func durationString(value *durationpb.Duration) string {
|
||||||
if value == nil {
|
if value == nil {
|
||||||
return "0s"
|
return "0s"
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func protoMap(message proto.Message) (map[string]any, error) {
|
func protoMap(message proto.Message) (map[string]any, error) {
|
||||||
raw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(message)
|
raw, err := (protojson.MarshalOptions{
|
||||||
|
UseProtoNames: true,
|
||||||
|
EmitDefaultValues: true,
|
||||||
|
}).Marshal(message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -44,7 +47,7 @@ func setYAMLMapping(node *yaml.Node, key string, value any) error {
|
||||||
}
|
}
|
||||||
for i := 0; i < len(node.Content); i += 2 {
|
for i := 0; i < len(node.Content); i += 2 {
|
||||||
if node.Content[i].Value == key {
|
if node.Content[i].Value == key {
|
||||||
node.Content[i+1] = replacement.Content[0]
|
mergeYAMLNode(node.Content[i+1], replacement.Content[0])
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -52,6 +55,35 @@ func setYAMLMapping(node *yaml.Node, key string, value any) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mergeYAMLNode updates values produced from the runtime configuration while
|
||||||
|
// retaining keys that are not represented by the protobuf and comments that
|
||||||
|
// were already present in the user's config file. Replacing an entire mapping
|
||||||
|
// with protojson output would discard configuration hints and extension keys.
|
||||||
|
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 (d *Data) persistConfig() error {
|
func (d *Data) persistConfig() error {
|
||||||
dataConfig, adminConfig := d.runtime.Values()
|
dataConfig, adminConfig := d.runtime.Values()
|
||||||
return d.persistConfigValues(dataConfig, adminConfig)
|
return d.persistConfigValues(dataConfig, adminConfig)
|
||||||
|
|
@ -90,7 +122,57 @@ func (d *Data) persistConfigValuesLocked(dataConfig *conf.Data, adminConfig *con
|
||||||
if err = setYAMLMapping(&document, "admin", adminValue); err != nil {
|
if err = setYAMLMapping(&document, "admin", adminValue); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
output, err := yaml.Marshal(&document)
|
return writeConfigDocument(configPath, &document)
|
||||||
|
}
|
||||||
|
|
||||||
|
// persistDatabaseConfig writes only the database selected on the init page.
|
||||||
|
// Initialization must not serialize the partially populated runtime config
|
||||||
|
// back over the template: doing so removes all omitted/default settings from
|
||||||
|
// data and admin and leaves the next startup with no visible configuration.
|
||||||
|
func (d *Data) persistDatabaseConfig(database *conf.Data_Database) error {
|
||||||
|
d.configMu.Lock()
|
||||||
|
defer d.configMu.Unlock()
|
||||||
|
|
||||||
|
configPath := d.runtime.ConfigPath()
|
||||||
|
if configPath == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
raw, err := os.ReadFile(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
|
||||||
|
}
|
||||||
|
// Do not marshal the init request through protojson here. Proto3 omits
|
||||||
|
// empty and zero values, which makes a merge retain stale values from the
|
||||||
|
// template (an empty database password is the common example). Update the
|
||||||
|
// fields owned by the init page explicitly and leave advanced database,
|
||||||
|
// Redis, Mongo, admin and server settings untouched.
|
||||||
|
databaseValue := map[string]any{
|
||||||
|
"driver": database.Driver,
|
||||||
|
"source": source,
|
||||||
|
"host": database.Host,
|
||||||
|
"port": database.Port,
|
||||||
|
"user": database.User,
|
||||||
|
"password": database.Password,
|
||||||
|
"name": database.Name,
|
||||||
|
"config": database.Config,
|
||||||
|
"path": database.Path,
|
||||||
|
}
|
||||||
|
if err = setYAMLMapping(&document, "data", map[string]any{"database": databaseValue}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return writeConfigDocument(configPath, &document)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeConfigDocument(configPath string, document *yaml.Node) error {
|
||||||
|
output, err := yaml.Marshal(document)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,24 @@ func (r *initializationRepo) IsInitialized(ctx context.Context) (bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *initializationRepo) Initialize(ctx context.Context, input *biz.DatabaseConfig) error {
|
func (r *initializationRepo) Initialize(ctx context.Context, input *biz.DatabaseConfig) error {
|
||||||
config := &conf.Data_Database{Driver: input.Driver, Host: input.Host, Port: input.Port, User: input.User, Password: input.Password, Name: input.Name, Path: input.Path, Config: input.Config}
|
config := &conf.Data_Database{}
|
||||||
|
if current := r.data.runtime.Data(); current != nil && current.Database != nil {
|
||||||
|
config = proto.Clone(current.Database).(*conf.Data_Database)
|
||||||
|
}
|
||||||
|
config.Driver = input.Driver
|
||||||
|
config.Host = input.Host
|
||||||
|
config.Port = input.Port
|
||||||
|
config.User = input.User
|
||||||
|
config.Password = input.Password
|
||||||
|
config.Name = input.Name
|
||||||
|
config.Path = input.Path
|
||||||
|
config.Config = input.Config
|
||||||
|
config.Source = ""
|
||||||
|
source, err := databaseDSN(config, "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
config.Source = source
|
||||||
r.data.initMu.Lock()
|
r.data.initMu.Lock()
|
||||||
defer r.data.initMu.Unlock()
|
defer r.data.initMu.Unlock()
|
||||||
initialized, err := r.IsInitialized(ctx)
|
initialized, err := r.IsInitialized(ctx)
|
||||||
|
|
@ -197,10 +214,7 @@ func (r *initializationRepo) Initialize(ctx context.Context, input *biz.Database
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
currentData, currentAdmin := r.data.runtime.Values()
|
if err := r.data.persistDatabaseConfig(config); err != nil {
|
||||||
nextData := proto.Clone(currentData).(*conf.Data)
|
|
||||||
nextData.Database = config
|
|
||||||
if err := r.data.persistConfigValues(nextData, currentAdmin); err != nil {
|
|
||||||
return fmt.Errorf("persist database configuration: %w", err)
|
return fmt.Errorf("persist database configuration: %w", err)
|
||||||
}
|
}
|
||||||
r.data.activateDatabase(candidate, config)
|
r.data.activateDatabase(candidate, config)
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@scroll="updateNoticeReadState"
|
@scroll="updateNoticeReadState"
|
||||||
>
|
>
|
||||||
<div ref="noticeContentRef" style="min-height: 900px">
|
<div ref="noticeContentRef">
|
||||||
<div class="space-y-5">
|
<div class="space-y-5">
|
||||||
<div class="notice-group">
|
<div class="notice-group">
|
||||||
<h4 class="mb-2 flex items-center gap-1.5 text-sm font-semibold text-[#2264f2]">
|
<h4 class="mb-2 flex items-center gap-1.5 text-sm font-semibold text-[#2264f2]">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue