/* 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 }