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 "" }