github.com/demonoid81/moby@v0.0.0-20200517203328-62dd8e17c460/pkg/discovery/kv/kv.go (about) 1 package kv // import "github.com/demonoid81/moby/pkg/discovery/kv" 2 3 import ( 4 "fmt" 5 "path" 6 "strings" 7 "time" 8 9 "github.com/demonoid81/moby/pkg/discovery" 10 "github.com/docker/go-connections/tlsconfig" 11 "github.com/demonoid81/libkv" 12 "github.com/demonoid81/libkv/store" 13 "github.com/demonoid81/libkv/store/consul" 14 "github.com/demonoid81/libkv/store/etcd" 15 "github.com/demonoid81/libkv/store/zookeeper" 16 "github.com/sirupsen/logrus" 17 ) 18 19 const ( 20 defaultDiscoveryPath = "docker/nodes" 21 ) 22 23 // Discovery is exported 24 type Discovery struct { 25 backend store.Backend 26 store store.Store 27 heartbeat time.Duration 28 ttl time.Duration 29 prefix string 30 path string 31 } 32 33 func init() { 34 Init() 35 } 36 37 // Init is exported 38 func Init() { 39 // Register to libkv 40 zookeeper.Register() 41 consul.Register() 42 etcd.Register() 43 44 // Register to internal discovery service 45 discovery.Register("zk", &Discovery{backend: store.ZK}) 46 discovery.Register("consul", &Discovery{backend: store.CONSUL}) 47 discovery.Register("etcd", &Discovery{backend: store.ETCD}) 48 } 49 50 // Initialize is exported 51 func (s *Discovery) Initialize(uris string, heartbeat time.Duration, ttl time.Duration, clusterOpts map[string]string) error { 52 var ( 53 parts = strings.SplitN(uris, "/", 2) 54 addrs = strings.Split(parts[0], ",") 55 err error 56 ) 57 58 // A custom prefix to the path can be optionally used. 59 if len(parts) == 2 { 60 s.prefix = parts[1] 61 } 62 63 s.heartbeat = heartbeat 64 s.ttl = ttl 65 66 // Use a custom path if specified in discovery options 67 dpath := defaultDiscoveryPath 68 if clusterOpts["kv.path"] != "" { 69 dpath = clusterOpts["kv.path"] 70 } 71 72 s.path = path.Join(s.prefix, dpath) 73 74 var config *store.Config 75 if clusterOpts["kv.cacertfile"] != "" && clusterOpts["kv.certfile"] != "" && clusterOpts["kv.keyfile"] != "" { 76 logrus.Info("Initializing discovery with TLS") 77 tlsConfig, err := tlsconfig.Client(tlsconfig.Options{ 78 CAFile: clusterOpts["kv.cacertfile"], 79 CertFile: clusterOpts["kv.certfile"], 80 KeyFile: clusterOpts["kv.keyfile"], 81 }) 82 if err != nil { 83 return err 84 } 85 config = &store.Config{ 86 // Set ClientTLS to trigger https (bug in libkv/etcd) 87 ClientTLS: &store.ClientTLSConfig{ 88 CACertFile: clusterOpts["kv.cacertfile"], 89 CertFile: clusterOpts["kv.certfile"], 90 KeyFile: clusterOpts["kv.keyfile"], 91 }, 92 // The actual TLS config that will be used 93 TLS: tlsConfig, 94 } 95 } else { 96 logrus.Info("Initializing discovery without TLS") 97 } 98 99 // Creates a new store, will ignore options given 100 // if not supported by the chosen store 101 s.store, err = libkv.NewStore(s.backend, addrs, config) 102 return err 103 } 104 105 // Watch the store until either there's a store error or we receive a stop request. 106 // Returns false if we shouldn't attempt watching the store anymore (stop request received). 107 func (s *Discovery) watchOnce(stopCh <-chan struct{}, watchCh <-chan []*store.KVPair, discoveryCh chan discovery.Entries, errCh chan error) bool { 108 for { 109 select { 110 case pairs := <-watchCh: 111 if pairs == nil { 112 return true 113 } 114 115 logrus.WithField("discovery", s.backend).Debugf("Watch triggered with %d nodes", len(pairs)) 116 117 // Convert `KVPair` into `discovery.Entry`. 118 addrs := make([]string, len(pairs)) 119 for _, pair := range pairs { 120 addrs = append(addrs, string(pair.Value)) 121 } 122 123 entries, err := discovery.CreateEntries(addrs) 124 if err != nil { 125 errCh <- err 126 } else { 127 discoveryCh <- entries 128 } 129 case <-stopCh: 130 // We were requested to stop watching. 131 return false 132 } 133 } 134 } 135 136 // Watch is exported 137 func (s *Discovery) Watch(stopCh <-chan struct{}) (<-chan discovery.Entries, <-chan error) { 138 ch := make(chan discovery.Entries) 139 errCh := make(chan error) 140 141 go func() { 142 defer close(ch) 143 defer close(errCh) 144 145 // Forever: Create a store watch, watch until we get an error and then try again. 146 // Will only stop if we receive a stopCh request. 147 for { 148 // Create the path to watch if it does not exist yet 149 exists, err := s.store.Exists(s.path) 150 if err != nil { 151 errCh <- err 152 } 153 if !exists { 154 if err := s.store.Put(s.path, []byte(""), &store.WriteOptions{IsDir: true}); err != nil { 155 errCh <- err 156 } 157 } 158 159 // Set up a watch. 160 watchCh, err := s.store.WatchTree(s.path, stopCh) 161 if err != nil { 162 errCh <- err 163 } else { 164 if !s.watchOnce(stopCh, watchCh, ch, errCh) { 165 return 166 } 167 } 168 169 // If we get here it means the store watch channel was closed. This 170 // is unexpected so let's retry later. 171 errCh <- fmt.Errorf("Unexpected watch error") 172 time.Sleep(s.heartbeat) 173 } 174 }() 175 return ch, errCh 176 } 177 178 // Register is exported 179 func (s *Discovery) Register(addr string) error { 180 opts := &store.WriteOptions{TTL: s.ttl} 181 return s.store.Put(path.Join(s.path, addr), []byte(addr), opts) 182 } 183 184 // Store returns the underlying store used by KV discovery. 185 func (s *Discovery) Store() store.Store { 186 return s.store 187 } 188 189 // Prefix returns the store prefix 190 func (s *Discovery) Prefix() string { 191 return s.prefix 192 }