├── .travis.yml ├── README.md └── skipper.go /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: go 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | consul-skipper 2 | ============== 3 | 4 | [![GoDoc](https://godoc.org/github.com/darkcrux/consul-skipper?status.png)](https://godoc.org/github.com/darkcrux/consul-skipper) 5 | [![Build Status](https://travis-ci.org/darkcrux/consul-skipper.png)](https://travis-ci.org/darkcrux/consul-skipper) 6 | 7 | 8 | consul-skipper is a library for a cluster leader election using Consul KV. This needs to be attached to the Consul Agent 9 | and then it runs leader election from there. 10 | 11 | ``` 12 | import "github.com/darkcrux/consul-skipper" 13 | 14 | candidate := &skipper.Candidate{ 15 | ConsulAddress: "10.0.0.10:8500", 16 | ConsulDatacenter: "dc1", 17 | LeadershipKey: "app/leader", 18 | ConsulAclToken: "", // Optional 19 | } 20 | candidate.RunForElection() 21 | ``` 22 | 23 | Running for election runs asynchronously and will keep running as long as the main application is running. To check if 24 | the current attached agent is the current leader, use: 25 | 26 | ``` 27 | skipper.IsLeader() 28 | ``` 29 | 30 | It is also possible to force a leader to step down forcing a re-election. 31 | 32 | ``` 33 | skipper.Resign() 34 | ``` 35 | -------------------------------------------------------------------------------- /skipper.go: -------------------------------------------------------------------------------- 1 | /* 2 | consul-skipper is a library for a cluster leader election using Consul KV. This needs to be attached to the Consul Agent 3 | and then it runs leader election from there. 4 | 5 | import "github.com/darkcrux/consul-skipper" 6 | 7 | candidate := &skipper.Candidate{ 8 | ConsulAddress: "10.0.0.10:8500", 9 | ConsulDatacenter: "dc1", 10 | LeadershipKey: "app/leader", 11 | ConsulAclToken: "", // Optional 12 | } 13 | candidate.RunForElection() 14 | 15 | Running for election runs asynchronously and will keep running as long as the main application is running. To check if 16 | the current attached agent is the current leader, use: 17 | 18 | skipper.IsLeader() 19 | 20 | It is also possible to force a leader to step down forcing a re-election. 21 | 22 | skipper.Resign() 23 | 24 | */ 25 | package skipper 26 | 27 | import ( 28 | "time" 29 | 30 | "github.com/Sirupsen/logrus" 31 | consulapi "github.com/hashicorp/consul/api" 32 | ) 33 | 34 | type Candidate struct { 35 | ConsulAddress string // The address of the consul agent. This defaults to 127.0.0.1:8500. 36 | ConsulAclToken string // The ACL Token to use. This defaults to an empty string." 37 | ConsulDatacenter string // The datacenter to connect to. This defaults to the config used by the agent. 38 | LeadershipKey string // The leadership key. This needs to be a proper Consul KV key. eg. app/leader 39 | session string 40 | node string 41 | } 42 | 43 | // RunForElection makes the candidate run for leadership against the other nodes in the cluster. This tries and acquire 44 | // the lock of the given LeadershipKey and setting it's value with the current node. If the lock acquisition passes, 45 | // then the node where the agent is running is now the new leader. A re-election will occur when there are changes in 46 | // the LeadershipKey. 47 | func (c *Candidate) RunForElection() { 48 | go c.campaign() 49 | } 50 | 51 | // IsLeader returns true if the current agent is the leader. 52 | func (c *Candidate) IsLeader() bool { 53 | consul := c.consulClient() 54 | c.retrieveNode() 55 | c.retrieveSession() 56 | kv, _, err := consul.KV().Get(c.LeadershipKey, nil) 57 | if err != nil { 58 | logrus.Errorln("Unable to check for leadership:", err) 59 | return false 60 | } 61 | if kv == nil { 62 | logrus.Warnf("Leadership key '%s' is missing in Consuk KV.", c.LeadershipKey) 63 | return false 64 | } 65 | return c.node == string(kv.Value) && c.session == kv.Session 66 | } 67 | 68 | // Leader returns the node of the current cluster leader. This returns an empty string if there is no leader. 69 | func (c *Candidate) Leader() string { 70 | consul := c.consulClient() 71 | kv, _, err := consul.KV().Get(c.LeadershipKey, nil) 72 | if kv == nil || err != nil { 73 | logrus.Warnln("There is no leader.") 74 | return "" 75 | } 76 | return string(kv.Value) 77 | } 78 | 79 | // Resign forces the current agent to step down as the leader forcing a re-election. Nothing happens if the agent is not 80 | // the current leader. 81 | func (c *Candidate) Resign() { 82 | if c.IsLeader() { 83 | consul := c.consulClient() 84 | kvpair := &consulapi.KVPair{ 85 | Key: c.LeadershipKey, 86 | Value: []byte(c.node), 87 | Session: c.session, 88 | } 89 | success, _, err := consul.KV().Release(kvpair, nil) 90 | if !success || err != nil { 91 | logrus.Warnf("%s was unable to step down as a leader", c.node) 92 | } else { 93 | logrus.Debugf("%s is no longer the leader.", c.node) 94 | } 95 | } 96 | } 97 | 98 | // Campaign handles leader election. Basically this just acquires a lock on the LeadershipKey and whoever gets the lock 99 | // is the leader. A re-election occurs when there are changes in the LeadershipKey. 100 | func (c *Candidate) campaign() { 101 | c.retrieveNode() 102 | c.retrieveSession() 103 | consul := c.consulClient() 104 | 105 | logrus.Debugf("%s is running for election with session %s.", c.node, c.session) 106 | 107 | kvpair := &consulapi.KVPair{ 108 | Key: c.LeadershipKey, 109 | Value: []byte(c.node), 110 | Session: c.session, 111 | } 112 | acquired, _, err := consul.KV().Acquire(kvpair, nil) 113 | if err != nil { 114 | logrus.Errorln("Failed to run Consul KV Acquire:", err) 115 | } 116 | 117 | if acquired { 118 | logrus.Infof("%s has become the leader.", c.node) 119 | } 120 | 121 | kv, _, _ := consul.KV().Get(c.LeadershipKey, nil) 122 | 123 | if kv != nil && kv.Session != "" { 124 | logrus.Debugf("%s is the current leader.", string(kv.Value)) 125 | logrus.Debugf("%s is waiting for changes in '%s'.", c.node, c.LeadershipKey) 126 | latestIndex := kv.ModifyIndex 127 | options := &consulapi.QueryOptions{ 128 | WaitIndex: latestIndex, 129 | } 130 | consul.KV().Get(c.LeadershipKey, options) 131 | } 132 | time.Sleep(15 * time.Second) 133 | c.campaign() 134 | } 135 | 136 | // RetrieveNode is a helper to retrieve the current node name of the agent. 137 | func (c *Candidate) retrieveNode() { 138 | consul := c.consulClient() 139 | agent, err := consul.Agent().Self() 140 | if err != nil { 141 | logrus.Warnln("Unable to retrieve node name.") 142 | return 143 | } 144 | c.node = agent["Config"]["NodeName"].(string) 145 | } 146 | 147 | // RetrieveSession retrieves the existing session needed to run leader election. If a session does not exist, a new 148 | // session is created with the LeadershipKey as the name. 149 | func (c *Candidate) retrieveSession() { 150 | consul := c.consulClient() 151 | 152 | if sessions, _, err := consul.Session().List(nil); err != nil { 153 | logrus.Warnln("Unable to retrieve list of sessions.") 154 | } else { 155 | for _, session := range sessions { 156 | if session.Name == c.LeadershipKey && session.Node == c.node { 157 | c.session = session.ID 158 | return 159 | } 160 | } 161 | } 162 | 163 | newSession := &consulapi.SessionEntry{ 164 | Name: c.LeadershipKey, 165 | } 166 | if sessionId, _, err := consul.Session().Create(newSession, nil); err != nil { 167 | logrus.Errorln("Unable to create new sessions:", err) 168 | } else { 169 | c.session = sessionId 170 | } 171 | } 172 | 173 | // ConsulClient is a helper to create the consulapi client for access to the Consul cluster. 174 | func (c *Candidate) consulClient() *consulapi.Client { 175 | config := consulapi.DefaultConfig() 176 | if c.ConsulAddress != "" { 177 | config.Address = c.ConsulAddress 178 | } 179 | if c.ConsulDatacenter != "" { 180 | config.Datacenter = c.ConsulDatacenter 181 | } 182 | if c.ConsulAclToken != "" { 183 | config.Token = c.ConsulAclToken 184 | } 185 | client, _ := consulapi.NewClient(config) 186 | return client 187 | } 188 | --------------------------------------------------------------------------------