Files
kratos-bootstrap/remoteconfig/apollo/apollo.go
2025-06-02 14:38:13 +08:00

287 lines
6.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package apollo
import (
"github.com/apolloconfig/agollo/v4"
"strings"
"github.com/apolloconfig/agollo/v4/constant"
apolloconfig "github.com/apolloconfig/agollo/v4/env/config"
"github.com/apolloconfig/agollo/v4/extension"
"github.com/go-kratos/kratos/v2/config"
"github.com/go-kratos/kratos/v2/encoding"
"github.com/go-kratos/kratos/v2/log"
)
type apollo struct {
client agollo.Client
opt *options
}
const (
yaml = "yaml"
yml = "yml"
json = "json"
properties = "properties"
)
var formats map[string]struct{}
// Option is apollo option
type Option func(*options)
type options struct {
appid string
secret string
cluster string
endpoint string
namespace string
isBackupConfig bool
backupPath string
originConfig bool
}
// WithAppID with apollo config app id
func WithAppID(appID string) Option {
return func(o *options) {
o.appid = appID
}
}
// WithCluster with apollo config cluster
func WithCluster(cluster string) Option {
return func(o *options) {
o.cluster = cluster
}
}
// WithEndpoint with apollo config conf server ip
func WithEndpoint(endpoint string) Option {
return func(o *options) {
o.endpoint = endpoint
}
}
// WithEnableBackup with apollo config enable backup config
func WithEnableBackup() Option {
return func(o *options) {
o.isBackupConfig = true
}
}
// WithDisableBackup with apollo config enable backup config
func WithDisableBackup() Option {
return func(o *options) {
o.isBackupConfig = false
}
}
// WithSecret with apollo config app secret
func WithSecret(secret string) Option {
return func(o *options) {
o.secret = secret
}
}
// WithNamespace with apollo config namespace name
func WithNamespace(name string) Option {
return func(o *options) {
o.namespace = name
}
}
// WithBackupPath with apollo config backupPath
func WithBackupPath(backupPath string) Option {
return func(o *options) {
o.backupPath = backupPath
}
}
// WithOriginalConfig use the original configuration file without parse processing
func WithOriginalConfig() Option {
return func(o *options) {
extension.AddFormatParser(constant.JSON, &jsonExtParser{})
extension.AddFormatParser(constant.YAML, &yamlExtParser{})
extension.AddFormatParser(constant.YML, &yamlExtParser{})
o.originConfig = true
}
}
func NewSource(opts ...Option) config.Source {
op := options{}
for _, o := range opts {
o(&op)
}
client, err := agollo.StartWithConfig(func() (*apolloconfig.AppConfig, error) {
return &apolloconfig.AppConfig{
AppID: op.appid,
Cluster: op.cluster,
NamespaceName: op.namespace,
IP: op.endpoint,
IsBackupConfig: op.isBackupConfig,
Secret: op.secret,
BackupConfigPath: op.backupPath,
}, nil
})
if err != nil {
panic(err)
}
return &apollo{client: client, opt: &op}
}
func format(ns string) string {
arr := strings.Split(ns, ".")
suffix := arr[len(arr)-1]
if len(arr) <= 1 || suffix == properties {
return json
}
if _, ok := formats[suffix]; !ok {
// fallback
return json
}
return suffix
}
func (e *apollo) load() []*config.KeyValue {
kvs := make([]*config.KeyValue, 0)
namespaces := strings.Split(e.opt.namespace, ",")
for _, ns := range namespaces {
if !e.opt.originConfig {
kv, err := e.getConfig(ns)
if err != nil {
log.Errorf("apollo get config failederr:%v", err)
continue
}
kvs = append(kvs, kv)
continue
}
if strings.Contains(ns, ".") && !strings.HasSuffix(ns, "."+properties) &&
(format(ns) == yaml || format(ns) == yml || format(ns) == json) {
kv, err := e.getOriginConfig(ns)
if err != nil {
log.Errorf("apollo get config failederr:%v", err)
continue
}
kvs = append(kvs, kv)
continue
}
kv, err := e.getConfig(ns)
if err != nil {
log.Errorf("apollo get config failederr:%v", err)
continue
}
kvs = append(kvs, kv)
}
return kvs
}
func (e *apollo) getConfig(ns string) (*config.KeyValue, error) {
next := map[string]any{}
e.client.GetConfigCache(ns).Range(func(key, value any) bool {
// all values are out properties format
resolve(genKey(ns, key.(string)), value, next)
return true
})
f := format(ns)
codec := encoding.GetCodec(f)
val, err := codec.Marshal(next)
if err != nil {
return nil, err
}
return &config.KeyValue{
Key: ns,
Value: val,
Format: f,
}, nil
}
func (e apollo) getOriginConfig(ns string) (*config.KeyValue, error) {
value, err := e.client.GetConfigCache(ns).Get("content")
if err != nil {
return nil, err
}
// serialize the namespace content KeyValue into bytes.
return &config.KeyValue{
Key: ns,
Value: []byte(value.(string)),
Format: format(ns),
}, nil
}
func (e *apollo) Load() (kv []*config.KeyValue, err error) {
return e.load(), nil
}
func (e *apollo) Watch() (config.Watcher, error) {
w, err := newWatcher(e)
if err != nil {
return nil, err
}
return w, nil
}
// resolve convert kv pair into one map[string]interface{} by split key into different
// map level. such as: app.name = "application" => map[app][name] = "application"
func resolve(key string, value any, target map[string]any) {
// expand key "aaa.bbb" into map[aaa]map[bbb]interface{}
keys := strings.Split(key, ".")
last := len(keys) - 1
cursor := target
for i, k := range keys {
if i == last {
cursor[k] = value
break
}
// not the last key, be deeper
v, ok := cursor[k]
if !ok {
// create a new map
deeper := make(map[string]any)
cursor[k] = deeper
cursor = deeper
continue
}
// current exists, then check existing value type, if it's not map
// that means duplicate keys, and at least one is not map instance.
if cursor, ok = v.(map[string]any); !ok {
log.Warnf("duplicate key: %v\n", strings.Join(keys[:i+1], "."))
break
}
}
}
// genKey got the key of config.KeyValue pair.
// eg: namespace.ext with subKey got namespace.subKey
func genKey(ns, sub string) string {
arr := strings.Split(ns, ".")
if len(arr) == 1 {
if ns == "" {
return sub
}
return ns + "." + sub
}
suffix := arr[len(arr)-1]
_, ok := formats[suffix]
if ok {
return strings.Join(arr[:len(arr)-1], ".") + "." + sub
}
return ns + "." + sub
}
func init() {
formats = make(map[string]struct{})
formats[yaml] = struct{}{}
formats[yml] = struct{}{}
formats[json] = struct{}{}
formats[properties] = struct{}{}
}