407 lines
11 KiB
Go
407 lines
11 KiB
Go
/*
|
|
Copyright 2021 The Dapr Authors
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package nacos
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/nacos-group/nacos-sdk-go/clients"
|
|
"github.com/nacos-group/nacos-sdk-go/clients/config_client"
|
|
"github.com/nacos-group/nacos-sdk-go/common/constant"
|
|
"github.com/nacos-group/nacos-sdk-go/vo"
|
|
|
|
"github.com/dapr/components-contrib/bindings"
|
|
"github.com/dapr/kit/logger"
|
|
)
|
|
|
|
const (
|
|
defaultGroup = "DEFAULT_GROUP"
|
|
defaultTimeout = 10 * time.Second
|
|
metadataConfigID = "config-id"
|
|
metadataConfigGroup = "config-group"
|
|
metadataConfigOnchange = "config-onchange"
|
|
)
|
|
|
|
// Config type.
|
|
type configParam struct {
|
|
dataID string
|
|
group string
|
|
}
|
|
|
|
// Nacos allows reading/writing to a Nacos server.
|
|
type Nacos struct {
|
|
settings Settings
|
|
config configParam
|
|
watches []configParam
|
|
servers []constant.ServerConfig
|
|
logger logger.Logger
|
|
configClient config_client.IConfigClient
|
|
readHandler func(response *bindings.ReadResponse) ([]byte, error)
|
|
}
|
|
|
|
// NewNacos returns a new Nacos instance.
|
|
func NewNacos(logger logger.Logger) *Nacos {
|
|
return &Nacos{logger: logger} //nolint:exhaustivestruct
|
|
}
|
|
|
|
// Init implements InputBinding/OutputBinding's Init method.
|
|
func (n *Nacos) Init(metadata bindings.Metadata) error {
|
|
n.settings = Settings{
|
|
Timeout: defaultTimeout,
|
|
}
|
|
err := n.settings.Decode(metadata.Properties)
|
|
if err != nil {
|
|
return fmt.Errorf("nacos config error: %w", err)
|
|
}
|
|
|
|
if err = n.settings.Validate(); err != nil {
|
|
return fmt.Errorf("nacos config error: %w", err)
|
|
}
|
|
|
|
if n.settings.Endpoint != "" {
|
|
n.logger.Infof("nacos server url: %s", n.settings.Endpoint)
|
|
} else if n.settings.NameServer != "" {
|
|
n.logger.Infof("nacos nameserver: %s", n.settings.NameServer)
|
|
}
|
|
|
|
if n.settings.Config != "" {
|
|
n.config, err = convertConfig(n.settings.Config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
n.watches, err = convertConfigs(n.settings.Watches)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
n.servers, err = convertServers(n.settings.Endpoint)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return n.createConfigClient()
|
|
}
|
|
|
|
func (n *Nacos) createConfigClient() error {
|
|
nacosConfig := map[string]interface{}{}
|
|
nacosConfig["clientConfig"] = constant.ClientConfig{ //nolint:exhaustivestruct
|
|
TimeoutMs: uint64(n.settings.Timeout),
|
|
NamespaceId: n.settings.NamespaceID,
|
|
Endpoint: n.settings.NameServer,
|
|
RegionId: n.settings.RegionID,
|
|
AccessKey: n.settings.AccessKey,
|
|
SecretKey: n.settings.SecretKey,
|
|
OpenKMS: n.settings.AccessKey != "" && n.settings.SecretKey != "",
|
|
CacheDir: n.settings.CacheDir,
|
|
UpdateThreadNum: n.settings.UpdateThreadNum,
|
|
NotLoadCacheAtStart: n.settings.NotLoadCacheAtStart,
|
|
UpdateCacheWhenEmpty: n.settings.UpdateCacheWhenEmpty,
|
|
Username: n.settings.Username,
|
|
Password: n.settings.Password,
|
|
LogDir: n.settings.LogDir,
|
|
RotateTime: n.settings.RotateTime,
|
|
MaxAge: int64(n.settings.MaxAge),
|
|
LogLevel: n.settings.LogLevel,
|
|
}
|
|
|
|
if len(n.servers) > 0 {
|
|
nacosConfig["serverConfigs"] = n.servers
|
|
}
|
|
|
|
var err error
|
|
n.configClient, err = clients.CreateConfigClient(nacosConfig)
|
|
if err != nil {
|
|
return fmt.Errorf("nacos config error: create config client failed. %w ", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Read implements InputBinding's Read method.
|
|
func (n *Nacos) Read(handler func(*bindings.ReadResponse) ([]byte, error)) error {
|
|
n.readHandler = handler
|
|
|
|
for _, watch := range n.watches {
|
|
go n.startListen(watch)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Close implements cancel all listeners, see https://github.com/dapr/components-contrib/issues/779
|
|
func (n *Nacos) Close() error {
|
|
n.cancelListener()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Invoke implements OutputBinding's Invoke method.
|
|
func (n *Nacos) Invoke(req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) {
|
|
switch req.Operation {
|
|
case bindings.CreateOperation:
|
|
return n.publish(req)
|
|
case bindings.GetOperation:
|
|
return n.fetch(req)
|
|
case bindings.DeleteOperation, bindings.ListOperation:
|
|
return nil, fmt.Errorf("nacos error: unsupported operation %s", req.Operation)
|
|
default:
|
|
return nil, fmt.Errorf("nacos error: unsupported operation %s", req.Operation)
|
|
}
|
|
}
|
|
|
|
// Operations implements OutputBinding's Operations method.
|
|
func (n *Nacos) Operations() []bindings.OperationKind {
|
|
return []bindings.OperationKind{bindings.CreateOperation, bindings.GetOperation}
|
|
}
|
|
|
|
func (n *Nacos) startListen(config configParam) {
|
|
n.fetchAndNotify(config)
|
|
n.addListener(config)
|
|
}
|
|
|
|
func (n *Nacos) fetchAndNotify(config configParam) {
|
|
content, err := n.configClient.GetConfig(vo.ConfigParam{
|
|
DataId: config.dataID,
|
|
Group: config.group,
|
|
Content: "",
|
|
DatumId: "",
|
|
OnChange: nil,
|
|
})
|
|
if err != nil {
|
|
n.logger.Warnf("failed to receive nacos config %s:%s, error: %v", config.dataID, config.group, err)
|
|
} else {
|
|
n.notifyApp(config.group, config.dataID, content)
|
|
}
|
|
}
|
|
|
|
func (n *Nacos) addListener(config configParam) {
|
|
err := n.configClient.ListenConfig(vo.ConfigParam{
|
|
DataId: config.dataID,
|
|
Group: config.group,
|
|
Content: "",
|
|
DatumId: "",
|
|
OnChange: n.listener,
|
|
})
|
|
if err != nil {
|
|
n.logger.Warnf("failed to add nacos listener for %s:%s, error: %v", config.dataID, config.group, err)
|
|
}
|
|
}
|
|
|
|
func (n *Nacos) addListener4InputBinding(config configParam) {
|
|
if n.addToWatches(config) {
|
|
go n.addListener(config)
|
|
}
|
|
}
|
|
|
|
func (n *Nacos) publish(req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) {
|
|
nacosConfigParam, err := n.findConfig(req.Metadata)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if _, err := n.configClient.PublishConfig(vo.ConfigParam{
|
|
DataId: nacosConfigParam.dataID,
|
|
Group: nacosConfigParam.group,
|
|
Content: string(req.Data),
|
|
DatumId: "",
|
|
OnChange: nil,
|
|
}); err != nil {
|
|
return nil, fmt.Errorf("publish failed. %w", err)
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func (n *Nacos) fetch(req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) {
|
|
nacosConfigParam, err := n.findConfig(req.Metadata)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rst, err := n.configClient.GetConfig(vo.ConfigParam{
|
|
DataId: nacosConfigParam.dataID,
|
|
Group: nacosConfigParam.group,
|
|
Content: "",
|
|
DatumId: "",
|
|
OnChange: nil,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fetch failed. err:%w", err)
|
|
}
|
|
|
|
if onchange := req.Metadata[metadataConfigOnchange]; strings.EqualFold(onchange, "true") {
|
|
n.addListener4InputBinding(*nacosConfigParam)
|
|
}
|
|
|
|
return &bindings.InvokeResponse{Data: []byte(rst), Metadata: map[string]string{}}, nil
|
|
}
|
|
|
|
func (n *Nacos) addToWatches(c configParam) bool {
|
|
if n.watches != nil {
|
|
for _, watch := range n.watches {
|
|
if c.dataID == watch.dataID && c.group == watch.group {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
n.watches = append(n.watches, c)
|
|
|
|
return true
|
|
}
|
|
|
|
func (n *Nacos) findConfig(md map[string]string) (*configParam, error) {
|
|
nacosConfigParam := n.config
|
|
if _, ok := md[metadataConfigID]; ok {
|
|
nacosConfigParam = configParam{
|
|
dataID: md[metadataConfigID],
|
|
group: md[metadataConfigGroup],
|
|
}
|
|
}
|
|
|
|
if nacosConfigParam.dataID == "" {
|
|
return nil, fmt.Errorf("nacos config error: invalid metadata, no dataID found: %v", md)
|
|
}
|
|
if nacosConfigParam.group == "" {
|
|
nacosConfigParam.group = defaultGroup
|
|
}
|
|
|
|
return &nacosConfigParam, nil
|
|
}
|
|
|
|
func (n *Nacos) listener(_, group, dataID, data string) {
|
|
n.notifyApp(group, dataID, data)
|
|
}
|
|
|
|
func (n *Nacos) cancelListener() {
|
|
for _, configParam := range n.watches {
|
|
if err := n.configClient.CancelListenConfig(vo.ConfigParam{ //nolint:exhaustivestruct
|
|
DataId: configParam.dataID,
|
|
Group: configParam.group,
|
|
}); err != nil {
|
|
n.logger.Warnf("nacos cancel listener failed err: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (n *Nacos) notifyApp(group, dataID, content string) {
|
|
metadata := map[string]string{
|
|
metadataConfigID: dataID,
|
|
metadataConfigGroup: group,
|
|
}
|
|
var err error
|
|
if n.readHandler != nil {
|
|
n.logger.Debugf("binding-nacos read content to app")
|
|
_, err = n.readHandler(&bindings.ReadResponse{Data: []byte(content), Metadata: metadata})
|
|
} else {
|
|
err = errors.New("nacos error: the InputBinding.Read handler not init")
|
|
}
|
|
|
|
if err != nil {
|
|
n.logger.Errorf("nacos config %s:%s failed to notify application, error: %v", dataID, group, err)
|
|
}
|
|
}
|
|
|
|
func convertConfig(s string) (configParam, error) {
|
|
nacosConfigParam := configParam{dataID: "", group: ""}
|
|
pair := strings.Split(s, ":")
|
|
nacosConfigParam.dataID = strings.TrimSpace(pair[0])
|
|
if len(pair) == 2 {
|
|
nacosConfigParam.group = strings.TrimSpace(pair[1])
|
|
}
|
|
if nacosConfigParam.group == "" {
|
|
nacosConfigParam.group = defaultGroup
|
|
}
|
|
if nacosConfigParam.dataID == "" {
|
|
return nacosConfigParam, fmt.Errorf("nacos config error: invalid config keys, no config-id defined: %s", s)
|
|
}
|
|
|
|
return nacosConfigParam, nil
|
|
}
|
|
|
|
func convertConfigs(ss string) ([]configParam, error) {
|
|
configs := make([]configParam, 0)
|
|
if ss == "" {
|
|
return configs, nil
|
|
}
|
|
|
|
for _, s := range strings.Split(ss, ",") {
|
|
nacosConfigParam, err := convertConfig(s)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
configs = append(configs, nacosConfigParam)
|
|
}
|
|
|
|
return configs, nil
|
|
}
|
|
|
|
func convertServers(ss string) ([]constant.ServerConfig, error) {
|
|
serverConfigs := make([]constant.ServerConfig, 0)
|
|
if ss == "" {
|
|
return serverConfigs, nil
|
|
}
|
|
|
|
array := strings.Split(ss, ",")
|
|
for _, s := range array {
|
|
cfg, err := parseServerURL(s)
|
|
if err != nil {
|
|
return serverConfigs, fmt.Errorf("parse url:%s error:%w", s, err)
|
|
}
|
|
serverConfigs = append(serverConfigs, *cfg)
|
|
}
|
|
|
|
return serverConfigs, nil
|
|
}
|
|
|
|
func parseServerURL(s string) (*constant.ServerConfig, error) {
|
|
if !strings.HasPrefix(s, "http") {
|
|
s = "http://" + s
|
|
}
|
|
u, err := url.Parse(s)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("nacos config error: server url %s error: %w", s, err)
|
|
}
|
|
|
|
port := uint64(80)
|
|
if u.Scheme == "" {
|
|
u.Scheme = "http"
|
|
} else if u.Scheme == "https" {
|
|
port = uint64(443)
|
|
}
|
|
|
|
if u.Port() != "" {
|
|
port, err = strconv.ParseUint(u.Port(), 10, 64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("nacos config error: server port %s err: %w", u.Port(), err)
|
|
}
|
|
}
|
|
|
|
if u.Path == "" || u.Path == "/" {
|
|
u.Path = "/nacos"
|
|
}
|
|
|
|
return &constant.ServerConfig{
|
|
ContextPath: u.Path,
|
|
IpAddr: u.Hostname(),
|
|
Port: port,
|
|
Scheme: u.Scheme,
|
|
}, nil
|
|
}
|