mirror of https://github.com/status-im/consul.git
209 lines
4.7 KiB
Go
209 lines
4.7 KiB
Go
package watch
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/armon/consul-api"
|
|
)
|
|
|
|
// WatchPlan is the parsed version of a watch specification. A watch provides
|
|
// the details of a query, which generates a view into the Consul data store.
|
|
// This view is watched for changes and a handler is invoked to take any
|
|
// appropriate actions.
|
|
type WatchPlan struct {
|
|
Query string
|
|
Datacenter string
|
|
Token string
|
|
Type string
|
|
Func WatchFunc
|
|
Handler HandlerFunc
|
|
|
|
address string
|
|
client *consulapi.Client
|
|
lastIndex uint64
|
|
lastResult interface{}
|
|
|
|
stop bool
|
|
stopCh chan struct{}
|
|
stopLock sync.Mutex
|
|
}
|
|
|
|
// WatchFunc is used to watch for a diff
|
|
type WatchFunc func(*WatchPlan) (uint64, interface{}, error)
|
|
|
|
// HandlerFunc is used to handle new data
|
|
type HandlerFunc func(uint64, interface{})
|
|
|
|
// Parse takes a watch query and compiles it into a WatchPlan or an error
|
|
func Parse(query string) (*WatchPlan, error) {
|
|
tokens, err := tokenize(query)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Failed to parse: %v", err)
|
|
}
|
|
params := collapse(tokens)
|
|
plan := &WatchPlan{
|
|
Query: query,
|
|
stopCh: make(chan struct{}),
|
|
}
|
|
|
|
// Parse the generic parameters
|
|
if err := assignValue(params, "datacenter", &plan.Datacenter); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := assignValue(params, "token", &plan.Token); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := assignValue(params, "type", &plan.Type); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Ensure there is a watch type
|
|
if plan.Type == "" {
|
|
return nil, fmt.Errorf("Watch type must be specified")
|
|
}
|
|
|
|
// Look for a factory function
|
|
factory := watchFuncFactory[plan.Type]
|
|
if factory == nil {
|
|
return nil, fmt.Errorf("Unsupported watch type: %s", plan.Type)
|
|
}
|
|
|
|
// Get the watch func
|
|
fn, err := factory(params)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
plan.Func = fn
|
|
|
|
// Ensure all parameters are consumed
|
|
if len(params) != 0 {
|
|
var bad []string
|
|
for key := range params {
|
|
bad = append(bad, key)
|
|
}
|
|
return nil, fmt.Errorf("Invalid parameters: %v", bad)
|
|
}
|
|
return plan, nil
|
|
}
|
|
|
|
// assignValue is used to extract a value ensuring it is only
|
|
// defined once
|
|
func assignValue(params map[string][]string, name string, out *string) error {
|
|
if vals, ok := params[name]; ok {
|
|
if len(vals) != 1 {
|
|
return fmt.Errorf("Multiple definitions of %s", name)
|
|
}
|
|
*out = vals[0]
|
|
delete(params, name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// token is used to represent a "datacenter:foobar" pair, where
|
|
// datacenter is the param and foobar is the value
|
|
type token struct {
|
|
param string
|
|
val string
|
|
}
|
|
|
|
func (t *token) GoString() string {
|
|
return fmt.Sprintf("%#v", *t)
|
|
}
|
|
|
|
// tokenize splits a query string into a slice of tokens
|
|
func tokenize(query string) ([]*token, error) {
|
|
var tokens []*token
|
|
for i := 0; i < len(query); i++ {
|
|
char := query[i]
|
|
|
|
// Ignore whitespace
|
|
if char == ' ' || char == '\t' || char == '\n' {
|
|
continue
|
|
}
|
|
|
|
// Read the next token
|
|
next, offset, err := readToken(query[i:])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Store the token
|
|
tokens = append(tokens, next)
|
|
|
|
// Increment the offset
|
|
i += offset
|
|
}
|
|
return tokens, nil
|
|
}
|
|
|
|
// readToken is used to read a single token
|
|
func readToken(query string) (*token, int, error) {
|
|
// Get the token
|
|
param, offset, err := readParameter(query)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
// Get the value
|
|
query = query[offset:]
|
|
val, offset2, err := readValue(query)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
// Return the new token
|
|
token := &token{
|
|
param: param,
|
|
val: val,
|
|
}
|
|
return token, offset + offset2, nil
|
|
}
|
|
|
|
// readParameter scans for the next parameter
|
|
func readParameter(query string) (string, int, error) {
|
|
for i := 0; i < len(query); i++ {
|
|
char := query[i]
|
|
if char == ':' {
|
|
if i == 0 {
|
|
return "", 0, fmt.Errorf("Missing parameter name")
|
|
} else {
|
|
return query[:i], i + 1, nil
|
|
}
|
|
}
|
|
}
|
|
return "", 0, fmt.Errorf("Parameter delimiter not found")
|
|
}
|
|
|
|
// readValue is used to scan for the next value
|
|
func readValue(query string) (string, int, error) {
|
|
// Handle quoted values
|
|
if query[0] == '\'' || query[0] == '"' {
|
|
quoteChar := query[0:1]
|
|
endChar := strings.Index(query[1:], quoteChar)
|
|
if endChar == -1 {
|
|
return "", 0, fmt.Errorf("Missing end of quotation")
|
|
}
|
|
return query[1 : endChar+1], endChar + 2, nil
|
|
}
|
|
|
|
// Look for white space
|
|
endChar := strings.IndexAny(query, " \t\n")
|
|
if endChar == -1 {
|
|
return query, len(query), nil
|
|
}
|
|
return query[:endChar], endChar, nil
|
|
}
|
|
|
|
// collapse is used to collapse a token stream into a map
|
|
// of parameter name to list of values.
|
|
func collapse(tokens []*token) map[string][]string {
|
|
out := make(map[string][]string)
|
|
for _, t := range tokens {
|
|
existing := out[t.param]
|
|
out[t.param] = append(existing, t.val)
|
|
}
|
|
return out
|
|
}
|