services/config: move mdns-based config to experimental.
mdns-based config distribution mechanism is not being used for now.
move services/config to experimental/users/mdnsconfig
https://github.com/veyron/release-issues/issues/156
Change-Id: Iac87ab387deed6b2f8c117f50439636f7d43d5d7
diff --git a/services/config/lib/config.go b/services/config/lib/config.go
deleted file mode 100644
index 4a1fbbd..0000000
--- a/services/config/lib/config.go
+++ /dev/null
@@ -1,417 +0,0 @@
-// A mdns based config service. We make an mdns group for each config.
-// Mdns is probably an over kill for this but why use two when one will do?
-
-// The service provides an eventually consistent map to all nodes on the
-// network. The winning map is the one with the highest version number.
-// If a server stores the map in a file, it can also be a source of the
-// map
-
-package config
-
-import (
- "fmt"
- "io/ioutil"
- "path/filepath"
- "sort"
- "strconv"
- "strings"
- "sync"
-
- "v.io/v23/verror"
- "v.io/x/lib/vlog"
-
- "github.com/presotto/go-mdns-sd"
- "github.com/presotto/go-mdns-sd/go_dns"
-)
-
-const maxDNSStringLength = 254
-
-type config struct {
- version uint64
- pairs map[string]string
-}
-
-type configService struct {
- rwlock sync.RWMutex // protects elements of this structure.
- mdns *mdns.MDNS
- service string
- file string // file containing config
- current *config // the highest numbered config
- done chan bool // closed to tell children to go away
- change *sync.Cond // condition variable broadcast to on every config change
- gen int // incremented every config change
-}
-
-const pkgPath = "v.io/x/ref/services/config/lib"
-
-// Errors
-var (
- errCantParse = verror.Register(pkgPath+".errCantParse", verror.NoRetry, "{1:}{2:} can't parse{:_}")
- errEntryTooLong = verror.Register(pkgPath+".errEntryTooLong", verror.NoRetry, "{1:}{2:} entry {3}:{4} is too long{:_}")
- errNoFileToRead = verror.Register(pkgPath+".errNoFileToRead", verror.NoRetry, "{1:}{2:} no file to read{:_}")
- errFileError = verror.Register(pkgPath+".errFileError", verror.NoRetry, "{1:}{2:} file {3}{:_}")
- errMissingLegalVersion = verror.Register(pkgPath+".errMissingLegalVersion", verror.NoRetry, "{1:}{2:} missing legal version for file{:_}")
- errMissingConfigVersion = verror.Register(pkgPath+".errMissingConfigVersion", verror.NoRetry, "{1:}{2:} missing config version{:_}")
- errNoConfig = verror.Register(pkgPath+".errNoConfig", verror.NoRetry, "{1:}{2:} no config{:_}")
- errConfigHasNoKey = verror.Register(pkgPath+".errConfigHasNoKey", verror.NoRetry, "{1:}{2:} config has no key {3}{:_}")
- errOfferingConfigError = verror.Register(pkgPath+".errOfferingConfigError", verror.NoRetry, "{1:}{2:} offering config {3}{:_}")
-)
-
-// MDNSConfigService creates a new instance of the config service with the given name.
-// If file is non blank, the initial config is read from file and any learned configs are
-// stored in it. Only instances with a file to backup will offer configs to the net.
-// All other instances are passive.
-func MDNSConfigService(name, file string, loopback bool, port uint16) (ConfigService, error) {
- x := filepath.Base(file)
- if x == "." {
- x = ""
- }
- var ipv4hp, ipv6hp string
- if port != 0 {
- ipv4hp = fmt.Sprintf("224.0.0.251:%d", port)
- ipv6hp = fmt.Sprintf("[FF02::FB]:%d", port)
- }
- mdns, err := mdns.NewMDNS(x, ipv4hp, ipv6hp, loopback, false)
- if err != nil {
- vlog.Errorf("mdns startup failed: %s", err)
- return nil, err
- }
- cs := &configService{mdns: mdns, service: name + "-config", file: file, done: make(chan bool)}
- cs.change = sync.NewCond(&cs.rwlock)
-
- // Read the config file if we have one and offer it to everyone else.
- if cs.current, err = readFile(file); err != nil {
- vlog.Errorf("reading initial config: %s", err)
- }
- cs.Offer()
-
- // Watch config changes and remember them.
- go cs.watcher()
- return cs, nil
-}
-
-// Stop the service.
-func (cs *configService) Stop() {
- cs.rwlock.Lock()
- mdns := cs.mdns
- cs.mdns = nil
- cs.rwlock.Unlock()
- if mdns != nil {
- mdns.Stop()
- close(cs.done)
- }
-}
-
-func newConfig() *config {
- return &config{pairs: make(map[string]string)}
-}
-
-// parseVersion parses a config version. The version is a pair of uint32s separated by a '.'.
-// If the second is missing, we assume 0. The first number is for humans. The
-// second is to break ties if machines generate configs.
-func parseVersion(s string) (uint64, error) {
- f := strings.SplitN(s, ".", 2)
- v, err := strconv.ParseUint(f[0], 10, 32)
- if err != nil {
- return 0, err
- }
- var r uint64
- if len(f) == 2 {
- r, err = strconv.ParseUint(f[1], 10, 32)
- if err != nil {
- return 0, err
- }
- }
- return (v << 32) | r, nil
-}
-
-func serializeVersion(version uint64) string {
- return fmt.Sprintf("%d.%d", version>>32, version&0xffffffff)
-}
-
-// parseEntry parse an entry of the form "key : value" and
-// add it to the map. White space before and after "key" and
-// value is discarded.
-func (c *config) parseEntry(l string) error {
- // Ignore lines with nothing but white space or starting with #
- l = strings.TrimSpace(l)
- if len(l) == 0 || strings.HasPrefix(l, "#") {
- return nil
- }
- // The reset have to be key<white>*:<white>*value
- f := strings.SplitN(l, ":", 2)
- if len(f) != 2 {
- return verror.New(errCantParse, nil, l)
- }
- k := strings.TrimSpace(f[0])
- v := strings.TrimSpace(f[1])
- if len(k)+len(v) > maxDNSStringLength {
- return verror.New(errEntryTooLong, nil, k, v)
- }
- c.pairs[k] = v
- if k != "version" {
- return nil
- }
- var err error
- c.version, err = parseVersion(v)
- return err
-}
-
-func serializeEntry(k, v string) (string, error) {
- if len(k)+len(v) > maxDNSStringLength {
- return "", verror.New(errEntryTooLong, nil, k, v)
- }
- return k + ":" + v, nil
-}
-
-func readFile(file string) (*config, error) {
- if len(file) == 0 {
- return nil, verror.New(errNoFileToRead, nil)
- }
-
- // The config has to be small so just read it all in one go.
- b, err := ioutil.ReadFile(file)
- if err != nil {
- return nil, err
- }
- c := newConfig()
- for _, l := range strings.Split(string(b), "\n") {
- if err := c.parseEntry(l); err != nil {
- return nil, verror.New(errFileError, nil, file, err)
- }
- }
- if _, ok := c.pairs["version"]; !ok {
- return nil, verror.New(errMissingLegalVersion, nil, file)
- }
- return c, nil
-}
-
-// writeFile is called with the write lock held.
-func writeFile(file string, c *config) {
- if len(file) == 0 || c == nil || len(c.pairs) == 0 {
- return
- }
- var s string
- for k, v := range c.pairs {
- e, err := serializeEntry(k, v)
- if err != nil {
- vlog.Errorf("writing %s: %s", file, err)
- return
- }
- s += e + "\n"
- }
- if err := ioutil.WriteFile(file, []byte(s), 0644); err != nil {
- vlog.Errorf("writing %s: %q", file, err)
- }
-}
-
-// rrToConfig converts a set of TXT rrs to a config.
-func rrToConfig(rr *dns.RR_TXT) (*config, error) {
- c := newConfig()
- for _, s := range rr.Txt {
- if err := c.parseEntry(s); err != nil {
- return nil, err
- }
- }
- // Ignore any config with no version.
- if _, ok := c.pairs["version"]; !ok {
- return nil, verror.New(errMissingConfigVersion, nil)
- }
- return c, nil
-}
-
-func (cs *configService) watchSingle(key string, c chan Pair) {
- ov, err := cs.Get(key)
- oexists := err == nil
- c <- Pair{Key: key, Value: ov, Nonexistant: !oexists}
- gen := 0
- for {
- v, err := cs.Get(key)
- exists := err == nil
- if exists != oexists || v != ov {
- c <- Pair{Key: key, Value: v, Nonexistant: !exists}
- ov, oexists = v, exists
- }
- // Block waiting for a change.
- cs.rwlock.Lock()
- for gen == cs.gen {
- cs.change.Wait()
- }
- gen = cs.gen
- cs.rwlock.Unlock()
- }
-}
-
-func (cs *configService) watchAll(c chan Pair) {
- omap := make(map[string]string, 0)
- gen := 0
- for {
- nmap, err := cs.GetAll()
- if err != nil {
- nmap = make(map[string]string, 0)
- }
- // See if any key disappeared.
- for k := range omap {
- if _, ok := nmap[k]; !ok {
- c <- Pair{Key: k, Value: "", Nonexistant: true}
- }
- }
- // See if any value changed or new key appeared.
- for k, v := range nmap {
- ov, ok := omap[k]
- if !ok || v != ov {
- c <- Pair{Key: k, Value: v}
- }
- }
- omap = nmap
- // Block waiting for a change.
- cs.rwlock.Lock()
- for gen == cs.gen {
- cs.change.Wait()
- }
- gen = cs.gen
- cs.rwlock.Unlock()
- }
-}
-
-// watcher waits for config changes and remembers them.
-// TODO(p): Should we also watch the file for changes?
-func (cs *configService) watcher() {
- cs.rwlock.RLock()
- if cs.mdns == nil {
- cs.rwlock.RUnlock()
- return
- }
- c := cs.mdns.ServiceMemberWatch(cs.service)
- cs.mdns.SubscribeToService(cs.service)
- cs.rwlock.RUnlock()
- defer close(c)
- for {
- select {
- case si := <-c:
- var config *config
- for _, rr := range si.TxtRRs {
- c, err := rrToConfig(rr)
- if err != nil {
- continue
- }
- if config == nil || c.version > config.version {
- config = c
- }
- }
- if config == nil {
- continue
- }
- // This lock is to synchronize writing and reading of the data structure.
- cs.rwlock.Lock()
- if cs.current != nil && config.version <= cs.current.version {
- cs.rwlock.Unlock()
- continue
- }
- cs.current = config
- cs.gen++
- cs.change.Broadcast()
- writeFile(cs.file, cs.current)
- cs.rwlock.Unlock()
- cs.Offer()
- case <-cs.done:
- return
- }
- }
-}
-
-// Get returns the value associated with key.
-func (cs *configService) Get(key string) (string, error) {
- cs.rwlock.RLock()
- defer cs.rwlock.RUnlock()
- if cs.current == nil {
- return "", verror.New(errNoConfig, nil)
- }
- if v, ok := cs.current.pairs[key]; !ok {
- return "", verror.New(errConfigHasNoKey, nil, key)
- } else {
- return v, nil
- }
-}
-
-// Watch for changes to a particular key.
-func (cs *configService) Watch(key string) chan Pair {
- c := make(chan Pair)
- go cs.watchSingle(key, c)
- return c
-}
-
-// Get returns the complete configuration.
-func (cs *configService) GetAll() (map[string]string, error) {
- cs.rwlock.RLock()
- defer cs.rwlock.RUnlock()
- if cs.current == nil {
- return nil, verror.New(errNoConfig, nil)
- }
- // Copy so caller can't change the map under our feet.
- reply := make(map[string]string)
- for k, v := range cs.current.pairs {
- reply[k] = v
- }
- return reply, nil
-}
-
-// WatchAll watches for changes to any key.
-func (cs *configService) WatchAll() chan Pair {
- c := make(chan Pair)
- go cs.watchAll(c)
- return c
-}
-
-// Offer offers the pairs for the config to other servers.
-func (cs *configService) Offer() {
- cs.rwlock.RLock()
- defer cs.rwlock.RUnlock()
- if cs.mdns == nil || cs.current == nil || len(cs.file) == 0 {
- return
- }
- // First sort the keys to get a canonical list for the txt entries.
- keys := make([]string, 0)
- for k := range cs.current.pairs {
- keys = append(keys, k)
- }
- sort.Strings(keys)
- // Convert config to a slice of strings.
- var txt []string
- for _, k := range keys {
- e, err := serializeEntry(k, cs.current.pairs[k])
- if err != nil {
- verror.New(errOfferingConfigError, nil, cs.file, err)
- return
- }
- txt = append(txt, e)
- }
-
- // Send (and keep sending).
- cs.mdns.AddService(cs.service, "", 0, txt...)
-}
-
-// Reread the config file and remember it if the version is newer than current.
-func (cs *configService) Reread() error {
- cs.rwlock.Lock()
- file := cs.file
- if len(file) == 0 {
- cs.rwlock.Unlock()
- return nil
- }
- c, err := readFile(file)
- if err != nil {
- cs.rwlock.Unlock()
- return err
- }
- if cs.current != nil && c.version <= cs.current.version {
- cs.rwlock.Unlock()
- return nil
- }
- cs.current = c
- cs.gen++
- cs.change.Broadcast()
- cs.rwlock.Unlock()
- cs.Offer()
- return nil
-}
diff --git a/services/config/lib/config_test.go b/services/config/lib/config_test.go
deleted file mode 100644
index b3bc4dd..0000000
--- a/services/config/lib/config_test.go
+++ /dev/null
@@ -1,319 +0,0 @@
-package config
-
-import (
- "errors"
- "fmt"
- "io/ioutil"
- "math/rand"
- "os"
- "testing"
- "time"
-)
-
-// A config file to be written to disk.
-var configFileA = `
-the:rain
- in:spain
-falls: mainly
- on : the
-# something ugly this way comes
-plain: today
-version : 1
-`
-
-// A map referring to the config in configFileA.
-var referenceA = map[string]string{
- "the": "rain",
- "in": "spain",
- "falls": "mainly",
- "on": "the",
- "plain": "today",
- "version": "1",
-}
-
-// Another config file to be written to disk.
-var configFileB = `
-the:rain
- in:spain
-falls: mainly
- on : my foot
-# something ugly this way comes
-version : 1.1
-`
-
-// A map referring to the config in configFileB.
-var referenceB = map[string]string{
- "the": "rain",
- "in": "spain",
- "falls": "mainly",
- "on": "my foot",
- "version": "1.1",
-}
-
-func compare(actual map[string]string, reference map[string]string) error {
- for k, rv := range reference {
- av, ok := actual[k]
- if !ok {
- return fmt.Errorf("missing entry for key %q", k)
- }
- if av != rv {
- return fmt.Errorf("bad value for key %q: expected %q, got %q", k, rv, av)
- }
- }
- for k, av := range actual {
- if _, ok := reference[k]; !ok {
- return fmt.Errorf("unexpected pair %q : %q", k, av)
- }
- }
- return nil
-}
-
-func compareConfigToReference(cs ConfigService, ref map[string]string) error {
- actual, err := cs.GetAll()
- if err != nil {
- return err
- }
- return compare(actual, ref)
-}
-
-func compareFileToReference(file string, ref map[string]string) error {
- c, err := readFile(file)
- if err != nil {
- return err
- }
- return compare(c.pairs, ref)
-}
-
-func waitForConsistency(cs ConfigService, ref map[string]string) error {
- finalerr := errors.New("inconsistent")
- for loops := 0; loops < 30; loops++ {
- actual, err := cs.GetAll()
- if err == nil {
- err := compare(actual, ref)
- if err == nil {
- return nil
- }
- finalerr = err
- }
- time.Sleep(500 * time.Millisecond)
- }
- return errors.New("timeout waiting for consistency: " + finalerr.Error())
-}
-
-func pickRandomPort() uint16 {
- rand.Seed(time.Now().UnixNano())
- return uint16(rand.Intn(1000)) + 5354
-}
-
-func testConfig(fileA, fileB, fileD string) error {
- port := pickRandomPort()
-
- // Write a config to fileA.
- if err := ioutil.WriteFile(fileA, []byte(configFileA), 0644); err != nil {
- return fmt.Errorf("writing initial %s: %s", fileA, err)
- }
-
- // Start a config service with a file and compare the parsed file to the reference.
- csa, err := MDNSConfigService("foo", fileA, true, port)
- if err != nil {
- return fmt.Errorf("creating service %s", err)
- }
- defer csa.Stop()
- if err := compareConfigToReference(csa, referenceA); err != nil {
- return fmt.Errorf("confA ~ refA: %s", err)
- }
-
- // Create a second instance with a non existant file.
- csb, err := MDNSConfigService("foo", fileB, true, port)
- if err != nil {
- return fmt.Errorf("creating service %s", err)
- }
- defer csb.Stop()
-
- // Create a third passive instance with no file.
- csc, err := MDNSConfigService("foo", "", true, port)
- if err != nil {
- return fmt.Errorf("creating service %s", err)
- }
- defer csc.Stop()
-
- // Loop till the second instance gets a config or we time out.
- if err := waitForConsistency(csb, referenceA); err != nil {
- return fmt.Errorf("confB ~ refA: %s", err)
- }
-
- // Make sure that the new instance updated its file.
- if err := compareFileToReference(fileB, referenceA); err != nil {
- return fmt.Errorf("fileB ~ refA: %s", err)
- }
-
- // Rewrite/Reread the second instance's file, make sure the it rereads it correctly.
- if err := ioutil.WriteFile(fileB, []byte(configFileB), 0644); err != nil {
- return fmt.Errorf("writing fileB: %s", err)
- }
- if err := csb.Reread(); err != nil {
- return fmt.Errorf("Rereading fileB: %s", err)
- }
- if err := compareConfigToReference(csb, referenceB); err != nil {
- return fmt.Errorf("confB ~ refB: %s", err)
- }
- // Loop till the first instance changes its config or we time out.
- if err := waitForConsistency(csa, referenceB); err != nil {
- return fmt.Errorf("confA ~ refB: %s", err)
- }
-
- // Make sure that the first instance updated its file.
- if err := compareFileToReference(fileA, referenceB); err != nil {
- return fmt.Errorf("fileA ~ refB: %s", err)
- }
-
- // Loop till the passive instance changes its config or we time out.
- if err := waitForConsistency(csc, referenceB); err != nil {
- return fmt.Errorf("confC ~ refB: %s", err)
- }
-
- // Create an instance with an lower numbered version. Make sure it doesn't
- // overcome the higher numbered one.
- if err := ioutil.WriteFile(fileD, []byte(configFileA), 0644); err != nil {
- return fmt.Errorf("writing initial %s: %s", fileD, err)
- }
- csd, err := MDNSConfigService("foo", fileD, true, port)
- if err != nil {
- return fmt.Errorf("creating service %s", err)
- }
- defer csd.Stop()
- if err := waitForConsistency(csd, referenceB); err != nil {
- return fmt.Errorf("confD ~ refB: %s", err)
- }
- if err := waitForConsistency(csc, referenceA); err == nil {
- return errors.New("eventual consistency picked wrong version for C")
- }
-
- return nil
-}
-
-func createTempFile(t *testing.T, base string) string {
- f, err := ioutil.TempFile("", base)
- if err != nil {
- t.Fatal("creating temp file: %s", err)
- }
- f.Close()
- return f.Name()
-}
-
-func TestConfig(t *testing.T) {
- // Create temporary files.
- fileA := createTempFile(t, "testconfigA")
- defer os.Remove(fileA)
- fileB := createTempFile(t, "testconfigB")
- defer os.Remove(fileB)
- fileD := createTempFile(t, "testconfigD")
- defer os.Remove(fileD)
-
- if err := testConfig(fileA, fileB, fileD); err != nil {
- t.Fatal(err)
- }
-}
-
-func TestConfigStream(t *testing.T) {
- // Create temporary file.
- fileA := createTempFile(t, "testconfigA")
- defer os.Remove(fileA)
-
- // Start a cs service.
- cs, err := MDNSConfigService("foo", fileA, true, pickRandomPort())
- if err != nil {
- t.Fatal("creating service %s", err)
- }
-
- // Watch a single key and all keys.
- ws := cs.Watch("version")
- wa := cs.WatchAll()
-
- // Since there's no config yet, we should just get a
- // deleted entry for the single key.
- select {
- case p := <-ws:
- if !p.Nonexistant {
- t.Fatal("expected Nonexistant: got %v", p)
- }
- case <-time.After(2 * time.Second):
- t.Fatal("timeout waiting for single config var")
- }
-
- // Write a config to the file and read it.
- if err := ioutil.WriteFile(fileA, []byte(configFileA), 0644); err != nil {
- t.Fatal("writing initial %s: %s", fileA, err)
- }
- if err := cs.Reread(); err != nil {
- t.Fatal("rereading config: %s", err)
- }
-
- // We should now have a version number.
- select {
- case p := <-ws:
- if p.Nonexistant || p.Key != "version" || p.Value != "1" {
- t.Fatal("expected [version, 1, false]: got %v", p)
- }
-
- case <-time.After(2 * time.Second):
- t.Fatal("timeout waiting for single config var")
- }
-
- // And we should be streamed the whole version.
- cmap := make(map[string]string, 0)
-L:
- for {
- select {
- case p := <-wa:
- if p.Nonexistant {
- t.Fatal("unexpected %v", p)
- }
- cmap[p.Key] = p.Value
- err := compare(referenceA, cmap)
- if err == nil {
- break L
- }
- case <-time.After(2 * time.Second):
- t.Fatal("timeout waiting for all config vars")
- }
- }
-
- // Change and reread the config file.
- if err := ioutil.WriteFile(fileA, []byte(configFileB), 0644); err != nil {
- t.Fatal("writing initial %s: %s", fileA, err)
- }
- if err := cs.Reread(); err != nil {
- t.Fatal("rereading config: %s", err)
- }
-
- // The version should have changed.
- select {
- case p := <-ws:
- if p.Nonexistant || p.Key != "version" || p.Value != "1.1" {
- t.Fatal("expected [version, 1.1, false]: got %v", p)
- }
-
- case <-time.After(2 * time.Second):
- t.Fatal("timeout waiting for single config var")
- }
-
- // And we should now be consistent with referenceB
-L2:
- for {
- select {
- case p := <-wa:
- if p.Nonexistant {
- delete(cmap, p.Key)
- break
- }
- cmap[p.Key] = p.Value
- err := compare(referenceB, cmap)
- if err == nil {
- break L2
- }
- case <-time.After(2 * time.Second):
- t.Fatal("timeout waiting for all config vars")
- }
- }
-}
diff --git a/services/config/lib/model.go b/services/config/lib/model.go
deleted file mode 100644
index 92aa459..0000000
--- a/services/config/lib/model.go
+++ /dev/null
@@ -1,37 +0,0 @@
-package config
-
-// ConfigService exists to get things going when you plug a device into a network.
-// Anything that we currently pass to apps as environment variables (proxy server,
-// global name server, ...) should come from here.
-
-// Both keys and values can have embedded white space but they can't start
-// or end with whitespace. keys cannot include ':'s.
-
-type Pair struct {
- Key string
- Value string
- Nonexistant bool
-}
-
-type ConfigService interface {
- // Stop stops a config service.
- Stop()
-
- // Get returns the value associated with name or an error if no value exists
- // or can be determined.
- Get(name string) (string, error)
-
- // GetAll returns all attribute/value pairs as a map or an error if no config
- // can be found.
- GetAll() (map[string]string, error)
-
- // Watch returns a stream of values for a particuar key.
- Watch(key string) chan Pair
-
- // Watch returns a stream key, value pairs.
- WatchAll() chan Pair
-
- // Reread the config info (for example, from a file). In particular
- // this says nothing about where the config resides or how it is read.
- Reread() error
-}