453 lines
10 KiB
Go
453 lines
10 KiB
Go
package conf
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/md5"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"go-common/library/conf/env"
|
|
"go-common/library/log"
|
|
)
|
|
|
|
const (
|
|
// code
|
|
_codeOk = 0
|
|
_codeNotModified = -304
|
|
// api
|
|
_apiGet = "http://%s/v1/config/get2?%s"
|
|
_apiCheck = "http://%s/v1/config/check?%s"
|
|
// timeout
|
|
_retryInterval = 1 * time.Second
|
|
_httpTimeout = 60 * time.Second
|
|
_unknownVersion = -1
|
|
commonKey = "common.toml"
|
|
)
|
|
|
|
var (
|
|
conf config
|
|
)
|
|
|
|
type version struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
Data *struct {
|
|
Version int64 `json:"version"`
|
|
} `json:"data"`
|
|
}
|
|
|
|
type result struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
Data *data `json:"data"`
|
|
}
|
|
|
|
type data struct {
|
|
Version int64 `json:"version"`
|
|
Content string `json:"content"`
|
|
Md5 string `json:"md5"`
|
|
}
|
|
|
|
// Namespace the key-value config object.
|
|
type Namespace struct {
|
|
Name string `json:"name"`
|
|
Data map[string]string `json:"data"`
|
|
}
|
|
|
|
type config struct {
|
|
Svr string
|
|
Ver string
|
|
Path string
|
|
Filename string
|
|
Host string
|
|
Addr string
|
|
Env string
|
|
Token string
|
|
Appoint string
|
|
// NOTE: new caster
|
|
Region string
|
|
Zone string
|
|
AppID string
|
|
DeployEnv string
|
|
TreeID string
|
|
}
|
|
|
|
// Client is config client.
|
|
type Client struct {
|
|
ver int64 // NOTE: for config v1
|
|
diff *ver // NOTE: for config v2
|
|
customize string
|
|
httpCli *http.Client
|
|
data atomic.Value
|
|
event chan string
|
|
|
|
useV2 bool
|
|
watchFile map[string]struct{}
|
|
watchAll bool
|
|
}
|
|
|
|
func init() {
|
|
// env
|
|
conf.Svr = os.Getenv("CONF_APPID")
|
|
conf.Ver = os.Getenv("CONF_VERSION")
|
|
conf.Addr = os.Getenv("CONF_HOST")
|
|
conf.Host = os.Getenv("CONF_HOSTNAME")
|
|
conf.Path = os.Getenv("CONF_PATH")
|
|
conf.Env = os.Getenv("CONF_ENV")
|
|
conf.Token = os.Getenv("CONF_TOKEN")
|
|
conf.Appoint = os.Getenv("CONF_APPOINT")
|
|
conf.Region = os.Getenv("REGION")
|
|
conf.Zone = os.Getenv("ZONE")
|
|
conf.AppID = os.Getenv("APP_ID")
|
|
conf.DeployEnv = os.Getenv("DEPLOY_ENV")
|
|
conf.TreeID = os.Getenv("TREE_ID")
|
|
|
|
// flags
|
|
hostname, _ := os.Hostname()
|
|
flag.StringVar(&conf.Svr, "conf_appid", conf.Svr, `app name.`)
|
|
flag.StringVar(&conf.Ver, "conf_version", conf.Ver, `app version.`)
|
|
flag.StringVar(&conf.Addr, "conf_host", conf.Addr, `config center api host.`)
|
|
flag.StringVar(&conf.Host, "conf_hostname", hostname, `hostname.`)
|
|
flag.StringVar(&conf.Path, "conf_path", conf.Path, `config file path.`)
|
|
flag.StringVar(&conf.Env, "conf_env", conf.Env, `config Env.`)
|
|
flag.StringVar(&conf.Token, "conf_token", conf.Token, `config Token.`)
|
|
flag.StringVar(&conf.Appoint, "conf_appoint", conf.Appoint, `config Appoint.`)
|
|
|
|
/*
|
|
flag.StringVar(&conf.Region, "region", conf.Region, `region.`)
|
|
flag.StringVar(&conf.Zone, "zone", conf.Zone, `zone.`)
|
|
flag.StringVar(&conf.AppID, "app_id", conf.AppID, `app id.`)
|
|
flag.StringVar(&conf.DeployEnv, "deploy_env", conf.DeployEnv, `deploy env.`)
|
|
*/
|
|
conf.Region = env.Region
|
|
conf.Zone = env.Zone
|
|
conf.AppID = env.AppID
|
|
conf.DeployEnv = env.DeployEnv
|
|
|
|
// FIXME(linli) remove treeid
|
|
flag.StringVar(&conf.TreeID, "tree_id", conf.TreeID, `tree id.`)
|
|
|
|
}
|
|
|
|
// New new a ugc config center client.
|
|
func New() (cli *Client, err error) {
|
|
cli = &Client{
|
|
httpCli: &http.Client{Timeout: _httpTimeout},
|
|
event: make(chan string, 10),
|
|
}
|
|
if conf.Svr != "" && conf.Host != "" && conf.Path != "" && conf.Addr != "" && conf.Ver != "" && conf.Env != "" && conf.Token != "" &&
|
|
(strings.HasPrefix(conf.Ver, "shsb") || (strings.HasPrefix(conf.Ver, "shylf"))) {
|
|
if err = cli.init(); err != nil {
|
|
return nil, err
|
|
}
|
|
go cli.updateproc()
|
|
return
|
|
}
|
|
if conf.Zone != "" && conf.AppID != "" && conf.Host != "" && conf.Path != "" && conf.Addr != "" && conf.Ver != "" && conf.DeployEnv != "" && conf.Token != "" {
|
|
if err = cli.init2(); err != nil {
|
|
return nil, err
|
|
}
|
|
go cli.updateproc2()
|
|
cli.useV2 = true
|
|
return
|
|
}
|
|
err = fmt.Errorf("at least one params is empty. app=%s, version=%s, hostname=%s, addr=%s, path=%s, Env=%s, Token =%s, DeployEnv=%s, TreeID=%s, appID=%s",
|
|
conf.Svr, conf.Ver, conf.Host, conf.Addr, conf.Path, conf.Env, conf.Token, conf.DeployEnv, conf.TreeID, conf.AppID)
|
|
return
|
|
}
|
|
|
|
// Path get confFile Path.
|
|
func (c *Client) Path() string {
|
|
return conf.Path
|
|
}
|
|
|
|
// Toml return config value.
|
|
func (c *Client) Toml() (cf string, ok bool) {
|
|
if c.useV2 {
|
|
return c.Toml2()
|
|
}
|
|
var (
|
|
m map[string]*Namespace
|
|
n *Namespace
|
|
)
|
|
if m, ok = c.data.Load().(map[string]*Namespace); !ok {
|
|
return
|
|
}
|
|
if n, ok = m[""]; !ok {
|
|
return
|
|
}
|
|
cf, ok = n.Data[commonKey]
|
|
return
|
|
}
|
|
|
|
// Value return config value.
|
|
func (c *Client) Value(key string) (cf string, ok bool) {
|
|
if c.useV2 {
|
|
return c.Value2(key)
|
|
}
|
|
var (
|
|
m map[string]*Namespace
|
|
n *Namespace
|
|
)
|
|
if m, ok = c.data.Load().(map[string]*Namespace); !ok {
|
|
return
|
|
}
|
|
if n, ok = m[""]; !ok {
|
|
return
|
|
}
|
|
cf, ok = n.Data[key]
|
|
return
|
|
}
|
|
|
|
// SetCustomize set customize value.
|
|
func (c *Client) SetCustomize(value string) {
|
|
c.customize = value
|
|
}
|
|
|
|
// Event client update event.
|
|
func (c *Client) Event() <-chan string {
|
|
return c.event
|
|
}
|
|
|
|
// Watch watch filename change.
|
|
func (c *Client) Watch(filename ...string) {
|
|
if c.watchFile == nil {
|
|
c.watchFile = map[string]struct{}{}
|
|
}
|
|
for _, f := range filename {
|
|
c.watchFile[f] = struct{}{}
|
|
}
|
|
}
|
|
|
|
// WatchAll watch all filename change.
|
|
func (c *Client) WatchAll() {
|
|
c.watchAll = true
|
|
}
|
|
|
|
// checkLocal check local config is ok
|
|
func (c *Client) init() (err error) {
|
|
var ver int64
|
|
if ver, err = c.checkVersion(_unknownVersion); err != nil {
|
|
fmt.Printf("get remote version error(%v)\n", err)
|
|
return
|
|
}
|
|
for i := 0; i < 3; i++ {
|
|
if ver == _unknownVersion {
|
|
fmt.Println("get null version")
|
|
return
|
|
}
|
|
if err = c.download(ver); err == nil {
|
|
return
|
|
}
|
|
fmt.Printf("retry times: %d, c.download() error(%v)\n", i, err)
|
|
time.Sleep(_retryInterval)
|
|
}
|
|
return
|
|
}
|
|
|
|
func (c *Client) updateproc() (err error) {
|
|
var ver int64
|
|
for {
|
|
time.Sleep(_retryInterval)
|
|
if ver, err = c.checkVersion(c.ver); err != nil {
|
|
log.Error("c.checkVersion(%d) error(%v)", c.ver, err)
|
|
continue
|
|
} else if ver == c.ver {
|
|
continue
|
|
}
|
|
if err = c.download(ver); err != nil {
|
|
log.Error("c.download() error(%s)", err)
|
|
continue
|
|
}
|
|
c.event <- ""
|
|
}
|
|
}
|
|
|
|
// download download config from config service
|
|
func (c *Client) download(ver int64) (err error) {
|
|
var data *data
|
|
if data, err = c.getConfig(ver); err != nil {
|
|
return
|
|
}
|
|
return c.update(data)
|
|
}
|
|
|
|
// poll config server
|
|
func (c *Client) checkVersion(reqVer int64) (ver int64, err error) {
|
|
var (
|
|
url string
|
|
req *http.Request
|
|
resp *http.Response
|
|
rb []byte
|
|
)
|
|
if url = c.makeURL(_apiCheck, reqVer); url == "" {
|
|
err = fmt.Errorf("checkVersion() c.makeUrl() error url empty")
|
|
return
|
|
}
|
|
// http
|
|
if req, err = http.NewRequest("GET", url, nil); err != nil {
|
|
return
|
|
}
|
|
if resp, err = c.httpCli.Do(req); err != nil {
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
err = fmt.Errorf("checkVersion() http error url(%s) status: %d", url, resp.StatusCode)
|
|
return
|
|
}
|
|
// ok
|
|
if rb, err = ioutil.ReadAll(resp.Body); err != nil {
|
|
return
|
|
}
|
|
v := &version{}
|
|
if err = json.Unmarshal(rb, v); err != nil {
|
|
return
|
|
}
|
|
switch v.Code {
|
|
case _codeOk:
|
|
if v.Data == nil {
|
|
err = fmt.Errorf("checkVersion() response error result: %v", v)
|
|
return
|
|
}
|
|
ver = v.Data.Version
|
|
case _codeNotModified:
|
|
ver = reqVer
|
|
default:
|
|
err = fmt.Errorf("checkVersion() response error result: %v", v)
|
|
}
|
|
return
|
|
}
|
|
|
|
// updateVersion update config version
|
|
func (c *Client) getConfig(ver int64) (data *data, err error) {
|
|
var (
|
|
url string
|
|
req *http.Request
|
|
resp *http.Response
|
|
rb []byte
|
|
res = &result{}
|
|
)
|
|
if url = c.makeURL(_apiGet, ver); url == "" {
|
|
err = fmt.Errorf("getConfig() c.makeUrl() error url empty")
|
|
return
|
|
}
|
|
// http
|
|
if req, err = http.NewRequest("GET", url, nil); err != nil {
|
|
return
|
|
}
|
|
if resp, err = c.httpCli.Do(req); err != nil {
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
// ok
|
|
if resp.StatusCode != http.StatusOK {
|
|
err = fmt.Errorf("getConfig() http error url(%s) status: %d", url, resp.StatusCode)
|
|
return
|
|
}
|
|
if rb, err = ioutil.ReadAll(resp.Body); err != nil {
|
|
return
|
|
}
|
|
if err = json.Unmarshal(rb, res); err != nil {
|
|
return
|
|
}
|
|
switch res.Code {
|
|
case _codeOk:
|
|
// has new config
|
|
if res.Data == nil {
|
|
err = fmt.Errorf("getConfig() response error result: %v", res)
|
|
return
|
|
}
|
|
data = res.Data
|
|
default:
|
|
err = fmt.Errorf("getConfig() response error result: %v", res)
|
|
}
|
|
return
|
|
}
|
|
|
|
// update write config
|
|
func (c *Client) update(d *data) (err error) {
|
|
var (
|
|
tmp = make(map[string]*Namespace)
|
|
bs = []byte(d.Content)
|
|
buf = new(bytes.Buffer)
|
|
n *Namespace
|
|
ok bool
|
|
)
|
|
// md5 file
|
|
if mh := md5.Sum(bs); hex.EncodeToString(mh[:]) != d.Md5 {
|
|
err = fmt.Errorf("md5 mismatch, local:%s, remote:%s", hex.EncodeToString(mh[:]), d.Md5)
|
|
return
|
|
}
|
|
// write conf
|
|
if err = json.Unmarshal(bs, &tmp); err != nil {
|
|
return
|
|
}
|
|
for _, value := range tmp {
|
|
for k, v := range value.Data {
|
|
if strings.Contains(k, ".toml") {
|
|
buf.WriteString(v)
|
|
buf.WriteString("\n")
|
|
}
|
|
if err = ioutil.WriteFile(path.Join(conf.Path, k), []byte(v), 0644); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
if n, ok = tmp[""]; !ok {
|
|
n = &Namespace{Data: make(map[string]string)}
|
|
tmp[""] = n
|
|
}
|
|
n.Data[commonKey] = buf.String()
|
|
// update current version
|
|
c.ver = d.Version
|
|
c.data.Store(tmp)
|
|
return
|
|
}
|
|
|
|
// makeUrl signed url
|
|
func (c *Client) makeURL(api string, ver int64) (query string) {
|
|
params := url.Values{}
|
|
// service
|
|
params.Set("service", conf.Svr)
|
|
params.Set("hostname", conf.Host)
|
|
params.Set("build", conf.Ver)
|
|
params.Set("version", fmt.Sprint(ver))
|
|
params.Set("ip", localIP())
|
|
params.Set("environment", conf.Env)
|
|
params.Set("token", conf.Token)
|
|
params.Set("appoint", conf.Appoint)
|
|
params.Set("customize", c.customize)
|
|
// api
|
|
query = fmt.Sprintf(api, conf.Addr, params.Encode())
|
|
return
|
|
}
|
|
|
|
// localIP return local IP of the host.
|
|
func localIP() string {
|
|
addrs, err := net.InterfaceAddrs()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
for _, address := range addrs {
|
|
// check the address type and if it is not a loopback the display it
|
|
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
|
if ipnet.IP.To4() != nil {
|
|
return ipnet.IP.String()
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|