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}¬ifications={notifications} 39 ApolloWatchNamespaceChangedApiUrl = "/notifications/v2?appId=%s&cluster=%s¬ifications=%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 }