github.com/zly-app/zapp@v1.3.3/config/apollo_sdk/sdk.go (about)

     1  /*
     2  -------------------------------------------------
     3     Author :       zlyuancn
     4     date:         2021/1/22
     5     Description :
     6  -------------------------------------------------
     7  */
     8  
     9  package apollo_sdk
    10  
    11  import (
    12  	"context"
    13  	"crypto/hmac"
    14  	"crypto/sha1"
    15  	"encoding/base64"
    16  	"encoding/json"
    17  	"errors"
    18  	"fmt"
    19  	"io/ioutil"
    20  	"net/http"
    21  	"net/url"
    22  	"time"
    23  
    24  	"go.uber.org/zap"
    25  	"gopkg.in/yaml.v3"
    26  
    27  	"github.com/zly-app/zapp/logger"
    28  )
    29  
    30  // apollo获取配置api
    31  // https://github.com/ctripcorp/apollo/wiki/%E5%85%B6%E5%AE%83%E8%AF%AD%E8%A8%80%E5%AE%A2%E6%88%B7%E7%AB%AF%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97
    32  // https://www.apolloconfig.com/#/zh/usage/other-language-client-user-guide?id=_13-%e9%80%9a%e8%bf%87%e4%b8%8d%e5%b8%a6%e7%bc%93%e5%ad%98%e7%9a%84http%e6%8e%a5%e5%8f%a3%e4%bb%8eapollo%e8%af%bb%e5%8f%96%e9%85%8d%e7%bd%ae
    33  // {config_server_url}/configs/{appId}/{clusterName}/{namespaceName}?releaseKey={releaseKey}&ip={clientIp}
    34  const ApolloGetNamespaceDataApiUrl = "/configs/%s/%s/%s?releaseKey=%s&ip=%s"
    35  
    36  const (
    37  	// apollo获取通知api
    38  	// {config_server_url}/notifications/v2?appId={appId}&cluster={clusterName}&notifications={notifications}
    39  	ApolloWatchNamespaceChangedApiUrl = "/notifications/v2?appId=%s&cluster=%s&notifications=%s"
    40  )
    41  
    42  var (
    43  	// http请求超时
    44  	HttpReqTimeout = time.Second * 3
    45  	// http请求通知超时
    46  	HttpReqNotificationTimeout = time.Second * 65
    47  )
    48  
    49  // 错误状态码描述
    50  var errStatusCodesDescribe = map[int]string{
    51  	400: "客户端传入参数的错误",
    52  	401: "客户端未授权或认证失败",
    53  	404: "命名空间数据不存在",
    54  	405: "接口访问的Method不正确",
    55  	500: "服务内部错误",
    56  }
    57  
    58  // 默认命名空间, 不会加上 NamespacePrefix
    59  const ApplicationNamespace = "application"
    60  
    61  type ApolloClient struct {
    62  	Address                 string   // apollo-api地址, 多个地址用英文逗号连接
    63  	AppId                   string   // 应用名
    64  	AccessKey               string   // 验证key, 优先级高于基础认证
    65  	AuthBasicUser           string   // 基础认证用户名, 可用于nginx的基础认证扩展
    66  	AuthBasicPassword       string   // 基础认证密码
    67  	Cluster                 string   // 集群名, 默认default
    68  	AlwaysLoadFromRemote    bool     // 总是从远程获取, 在远程加载失败时不会从备份文件加载
    69  	BackupFile              string   // 备份文件名
    70  	Namespaces              []string // 其他自定义命名空间
    71  	IgnoreNamespaceNotFound bool     // 是否忽略命名空间不存在
    72  	cache                   MultiNamespaceData
    73  }
    74  
    75  type (
    76  	// 命名空间数据
    77  	NamespaceData = struct {
    78  		AppId          string            `json:"appId"`
    79  		Cluster        string            `json:"cluster"`
    80  		Namespace      string            `json:"namespaceName"`
    81  		Configurations map[string]string `json:"configurations"`
    82  		ReleaseKey     string            `json:"releaseKey"`
    83  	}
    84  	// 多个命名空间数据
    85  	MultiNamespaceData = map[string]*NamespaceData
    86  	// 通知参数
    87  	NotificationParam struct {
    88  		NamespaceName  string `json:"namespaceName"`
    89  		NotificationId int    `json:"notificationId"`
    90  	}
    91  	// 通知结果数据
    92  	NotificationRsp struct {
    93  		NamespaceName  string `json:"namespaceName"`
    94  		NotificationId int    `json:"notificationId"`
    95  	}
    96  )
    97  
    98  func (a *ApolloClient) clientIP() string {
    99  	return ""
   100  }
   101  
   102  func (a *ApolloClient) Init() error {
   103  	a.cache = make(MultiNamespaceData)
   104  	return nil
   105  }
   106  
   107  // 获取所有命名空间的数据
   108  func (a *ApolloClient) GetNamespacesData() (MultiNamespaceData, error) {
   109  	namespaces := append([]string{ApplicationNamespace}, a.Namespaces...)
   110  	// 允许从本地备份获取
   111  	if a.isAllowLoadFromBackupFile() {
   112  		backupData, err := a.loadDataFromBackupFile()
   113  		if err != nil {
   114  			logger.Log.Error("从本地加载配置失败", zap.Error(err))
   115  		} else {
   116  			a.writeCache(backupData)
   117  		}
   118  	}
   119  
   120  	// 退出之前保存当前已存在数据
   121  	defer a.saveDataToBackupFile()
   122  
   123  	// 遍历获取
   124  	for _, namespace := range namespaces {
   125  		// 从远程获取数据
   126  		remoteData, _, err := a.loadNamespaceDataFromRemote(namespace)
   127  		if err == nil { // 成功拿到则覆盖数据
   128  			a.cache[namespace] = remoteData
   129  			continue
   130  		}
   131  
   132  		// 如果总是从远程获取则返回错误
   133  		if a.AlwaysLoadFromRemote {
   134  			return nil, fmt.Errorf("从远程获取命名空间<%s>的数据失败: %s", namespace, err)
   135  		}
   136  
   137  		logger.Log.Error("从远程获取配置失败", zap.String("namespace", namespace), zap.Error(err))
   138  		_, ok := a.cache[namespace]
   139  		if !ok {
   140  			return nil, fmt.Errorf("本地命名空间<%s>的数据不存在", namespace)
   141  		}
   142  	}
   143  
   144  	return a.cache, nil
   145  }
   146  
   147  // 从远程加载命名空间数据
   148  func (a *ApolloClient) loadNamespaceDataFromRemote(namespace string) (data *NamespaceData, changed bool, err error) {
   149  	// 检查配置
   150  	if a.Address == "" {
   151  		return nil, false, errors.New("apollo的address是空的")
   152  	}
   153  	if a.AppId == "" {
   154  		return nil, false, errors.New("apollo的appid是空的")
   155  	}
   156  	if a.Cluster == "" {
   157  		a.Cluster = "default"
   158  	}
   159  
   160  	cacheData, hasCache := a.cache[namespace]
   161  
   162  	var requestUri string
   163  	if hasCache {
   164  		requestUri = fmt.Sprintf(ApolloGetNamespaceDataApiUrl, a.AppId, a.Cluster, namespace, cacheData.ReleaseKey, a.clientIP())
   165  	} else {
   166  		requestUri = fmt.Sprintf(ApolloGetNamespaceDataApiUrl, a.AppId, a.Cluster, namespace, "", a.clientIP())
   167  	}
   168  
   169  	// 构建请求体
   170  	// 超时
   171  	ctx, cancel := context.WithTimeout(context.Background(), HttpReqTimeout)
   172  	defer cancel()
   173  
   174  	req, err := http.NewRequestWithContext(ctx, "GET", a.Address+requestUri, nil)
   175  	if err != nil {
   176  		return nil, false, err
   177  	}
   178  	a.officialSignature(req) // 认证
   179  
   180  	// 请求
   181  	resp, err := http.DefaultClient.Do(req)
   182  	if err != nil {
   183  		return nil, false, err
   184  	}
   185  	defer resp.Body.Close()
   186  
   187  	// 检查状态码
   188  	if resp.StatusCode != http.StatusOK {
   189  		if resp.StatusCode == http.StatusNotFound && a.IgnoreNamespaceNotFound && namespace != ApplicationNamespace { // 命名空间不存在
   190  			empty := &NamespaceData{
   191  				AppId:          a.AppId,
   192  				Cluster:        a.Cluster,
   193  				Namespace:      namespace,
   194  				Configurations: make(map[string]string),
   195  			}
   196  			return empty, true, nil // 视为空配置数据
   197  		}
   198  		if resp.StatusCode == http.StatusNotModified { // 未改变
   199  			return cacheData, false, nil
   200  		}
   201  
   202  		desc, ok := errStatusCodesDescribe[resp.StatusCode]
   203  		if !ok {
   204  			desc = "未知错误"
   205  		}
   206  		return nil, false, fmt.Errorf("收到错误码: %d: %s", resp.StatusCode, desc)
   207  	}
   208  
   209  	// 解码
   210  	var result NamespaceData
   211  	err = json.NewDecoder(resp.Body).Decode(&result)
   212  	if err != nil {
   213  		return nil, false, fmt.Errorf("解码失败: %v", err)
   214  	}
   215  	if result.Configurations == nil {
   216  		result.Configurations = make(map[string]string)
   217  	}
   218  	return &result, true, nil
   219  }
   220  
   221  /*
   222  获取命名空间数据
   223  
   224  	如果 oldData 为 nil 会直接获取数据
   225  	如果 oldData.ReleaseKey 不为空则检查是否会改变了
   226  */
   227  func (a *ApolloClient) GetNamespaceData(namespace string, ignoreRemoteErr bool) (oldData, newData *NamespaceData, changed bool, err error) {
   228  	oldData = a.cache[namespace]
   229  	if oldData == nil {
   230  		oldData = &NamespaceData{
   231  			AppId:          a.AppId,
   232  			Cluster:        a.Cluster,
   233  			Namespace:      namespace,
   234  			Configurations: make(map[string]string),
   235  		}
   236  	}
   237  
   238  	newData, changed, err = a.loadNamespaceDataFromRemote(namespace)
   239  	if err != nil && ignoreRemoteErr {
   240  		logger.Log.Error("获取远程命名空间数据失败",
   241  			zap.String("namespace", namespace),
   242  			zap.Error(err),
   243  		)
   244  		return oldData, oldData, false, nil
   245  	}
   246  	if changed {
   247  		a.cache[namespace] = newData
   248  		a.saveDataToBackupFile()
   249  	}
   250  	return
   251  }
   252  
   253  /*
   254  等待通知
   255  
   256  	如果数据未被改变, 此方法会导致挂起直到超时或被改变
   257  */
   258  func (a *ApolloClient) WaitNotification(ctx context.Context, param []*NotificationParam) ([]*NotificationRsp, error) {
   259  	if len(param) == 0 {
   260  		return nil, nil
   261  	}
   262  	// 检查配置
   263  	if a.Address == "" {
   264  		return nil, errors.New("apollo的address是空的")
   265  	}
   266  	if a.AppId == "" {
   267  		return nil, errors.New("apollo的appid是空的")
   268  	}
   269  	if a.Cluster == "" {
   270  		a.Cluster = "default"
   271  	}
   272  
   273  	paramData, _ := json.Marshal(param)
   274  	requestUri := fmt.Sprintf(ApolloWatchNamespaceChangedApiUrl, a.AppId, a.Cluster, url.QueryEscape(string(paramData)))
   275  
   276  	// 超时
   277  	var cancel context.CancelFunc
   278  	ctx, cancel = context.WithTimeout(ctx, HttpReqNotificationTimeout)
   279  	defer cancel()
   280  
   281  	// 构建请求体
   282  	req, err := http.NewRequestWithContext(ctx, "GET", a.Address+requestUri, nil)
   283  	if err != nil {
   284  		return nil, err
   285  	}
   286  	a.officialSignature(req) // 认证
   287  
   288  	resp, err := http.DefaultClient.Do(req)
   289  	if err != nil {
   290  		if err == context.Canceled { // 被主动取消
   291  			return nil, nil
   292  		}
   293  		return nil, err
   294  	}
   295  	defer resp.Body.Close()
   296  
   297  	// 检查状态码
   298  	if resp.StatusCode != http.StatusOK {
   299  		if resp.StatusCode == http.StatusNotModified { // 状态未改变
   300  			return nil, nil
   301  		}
   302  
   303  		desc, ok := errStatusCodesDescribe[resp.StatusCode]
   304  		if !ok {
   305  			desc = "未知错误"
   306  		}
   307  		return nil, fmt.Errorf("收到错误码: %d: %s", resp.StatusCode, desc)
   308  	}
   309  
   310  	// 解码
   311  	var result []*NotificationRsp
   312  	err = json.NewDecoder(resp.Body).Decode(&result)
   313  	if err != nil {
   314  		return nil, fmt.Errorf("解码失败: %v", err)
   315  	}
   316  	return result, nil
   317  }
   318  
   319  // 保存数据到备份文件
   320  func (a *ApolloClient) saveDataToBackupFile() {
   321  	if len(a.cache) == 0 || a.BackupFile == "" {
   322  		return
   323  	}
   324  
   325  	bs, err := yaml.Marshal(a.cache)
   326  	if err == nil {
   327  		err = ioutil.WriteFile(a.BackupFile, bs, 0644)
   328  	}
   329  	if err != nil {
   330  		logger.Log.Error("备份配置文件失败", zap.Error(err))
   331  	}
   332  }
   333  
   334  // 从备份文件加载数据
   335  func (a *ApolloClient) loadDataFromBackupFile() (MultiNamespaceData, error) {
   336  	if a.BackupFile == "" {
   337  		return nil, nil
   338  	}
   339  
   340  	bs, err := ioutil.ReadFile(a.BackupFile)
   341  	if err != nil {
   342  		return nil, err
   343  	}
   344  
   345  	var result MultiNamespaceData
   346  	err = yaml.Unmarshal(bs, &result)
   347  	if err != nil {
   348  		return nil, err
   349  	}
   350  	for k, d := range result {
   351  		if d == nil || d.Namespace != k {
   352  			return nil, fmt.Errorf("配置<%s>不正确", k)
   353  		}
   354  	}
   355  	return result, nil
   356  }
   357  
   358  // 写入缓存
   359  func (a *ApolloClient) writeCache(data MultiNamespaceData) {
   360  	for k, v := range data {
   361  		a.cache[k] = v
   362  	}
   363  }
   364  
   365  // 是否允许从本地备份获取
   366  func (a *ApolloClient) isAllowLoadFromBackupFile() bool {
   367  	return !a.AlwaysLoadFromRemote && a.BackupFile != "" // 不总是从远程获取 并且 存在备份文件
   368  }
   369  
   370  // 官方签名
   371  func (a *ApolloClient) officialSignature(req *http.Request) {
   372  	if a.AccessKey != "" {
   373  		timestamp := fmt.Sprintf("%v", time.Now().UnixNano()/int64(time.Millisecond))
   374  		stringToSign := timestamp + "\n" + req.URL.RequestURI()
   375  		key := []byte(a.AccessKey)
   376  		mac := hmac.New(sha1.New, key)
   377  		_, _ = mac.Write([]byte(stringToSign))
   378  		signature := base64.StdEncoding.EncodeToString(mac.Sum(nil))
   379  		req.Header.Add("Authorization", fmt.Sprintf("Apollo %s:%s", a.AppId, signature))
   380  		req.Header.Add("Timestamp", timestamp)
   381  		return
   382  	}
   383  
   384  	if a.AuthBasicUser != "" {
   385  		req.Header.Add("Authorization", fmt.Sprintf("basic %s", base64.StdEncoding.EncodeToString([]byte(a.AuthBasicUser+":"+a.AuthBasicPassword))))
   386  		return
   387  	}
   388  }