|
|
|
package apollo
|
|
|
|
|
|
|
|
import (
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/apolloconfig/agollo/v4"
|
|
|
|
"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 failed,err:%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 failed,err:%v", err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
kvs = append(kvs, kv)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
kv, err := e.getConfig(ns)
|
|
|
|
if err != nil {
|
|
|
|
log.Errorf("apollo get config failed,err:%v", err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
kvs = append(kvs, kv)
|
|
|
|
}
|
|
|
|
return kvs
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *apollo) getConfig(ns string) (*config.KeyValue, error) {
|
|
|
|
next := map[string]interface{}{}
|
|
|
|
e.client.GetConfigCache(ns).Range(func(key, value interface{}) 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 interface{}, target map[string]interface{}) {
|
|
|
|
// 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]interface{})
|
|
|
|
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]interface{}); !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{}{}
|
|
|
|
}
|