├── Dockerfile ├── Makefile ├── restapi ├── operations │ ├── operations.go │ ├── create_in_order_node.go │ ├── get_node.go │ ├── delete_node.go │ └── set_node.go ├── decode.go └── decode_test.go ├── backend ├── query.go ├── condition.go ├── dialect.go ├── listener_test.go ├── listener.go ├── sql.go └── sql_test.go ├── models └── models.go ├── integration-tests └── basic.bash ├── README.md └── main.go /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | ADD etcdb-linux /etcdb 3 | EXPOSE 2379 4 | EXPOSE 4001 5 | ENTRYPOINT ["/etcdb"] 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO_FILES = $(shell find . -type f -name '*.go') 2 | 3 | etcdb: $(GO_FILES) 4 | go build -o etcdb 5 | 6 | etcdb-linux: $(GO_FILES) 7 | GOOS=linux go build -o etcdb-linux 8 | 9 | test: 10 | go test -v ./... -race 11 | 12 | test-integration: etcdb-linux 13 | basht integration-tests/*.bash 14 | 15 | test-deps: 16 | go get github.com/progrium/basht 17 | -------------------------------------------------------------------------------- /restapi/operations/operations.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | // The Operation interface represents a REST operation. 4 | type Operation interface { 5 | // Params supplies an interface that will be populated by restapi.Unmarshal() 6 | // prior to calling Call() 7 | Params() interface{} 8 | 9 | // Call returns the result of the REST operation. 10 | Call() (interface{}, error) 11 | } 12 | -------------------------------------------------------------------------------- /restapi/operations/create_in_order_node.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import ( 4 | "github.com/rancher/etcdb/backend" 5 | "github.com/rancher/etcdb/models" 6 | ) 7 | 8 | type CreateInOrderNode struct { 9 | params struct { 10 | Key string `path:"key"` 11 | Value string `formData:"value"` 12 | TTL *int64 `formData:"ttl"` 13 | } 14 | Store *backend.SqlBackend 15 | } 16 | 17 | func (op *CreateInOrderNode) Params() interface{} { 18 | return &op.params 19 | } 20 | 21 | func (op *CreateInOrderNode) Call() (interface{}, error) { 22 | node, err := op.Store.CreateInOrder(op.params.Key, op.params.Value, op.params.TTL) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | return &models.Action{ 28 | Action: "create", 29 | Node: *node, 30 | }, nil 31 | } 32 | -------------------------------------------------------------------------------- /restapi/operations/get_node.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import ( 4 | "github.com/rancher/etcdb/backend" 5 | "github.com/rancher/etcdb/models" 6 | ) 7 | 8 | type GetNode struct { 9 | params struct { 10 | Key string `path:"key"` 11 | Wait bool `query:"wait"` 12 | WaitIndex *int64 `query:"waitIndex"` 13 | Recursive bool `query:"recursive"` 14 | Sorted bool `query:"sorted"` 15 | } 16 | Store *backend.SqlBackend 17 | Watcher *backend.ChangeWatcher 18 | } 19 | 20 | func (op *GetNode) Params() interface{} { 21 | return &op.params 22 | } 23 | 24 | func (op *GetNode) Call() (interface{}, error) { 25 | if op.params.Wait { 26 | waitIndex := int64(0) 27 | if op.params.WaitIndex != nil { 28 | waitIndex = *op.params.WaitIndex 29 | } 30 | return op.Watcher.NextChange(op.params.Key, op.params.Recursive, waitIndex) 31 | } 32 | 33 | node, err := op.Store.Get(op.params.Key, op.params.Recursive) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | return &models.Action{ 39 | Action: "get", 40 | Node: *node, 41 | }, nil 42 | } 43 | -------------------------------------------------------------------------------- /backend/query.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "bytes" 5 | "database/sql" 6 | ) 7 | 8 | type Query struct { 9 | buf bytes.Buffer 10 | Params []interface{} 11 | dialect dbDialect 12 | } 13 | 14 | func (q *Query) Text(text string) *Query { 15 | q.buf.WriteString(text) 16 | return q 17 | } 18 | 19 | func (q *Query) Param(p interface{}) *Query { 20 | q.Params = append(q.Params, p) 21 | q.buf.WriteString(q.dialect.nameParam(q.Params)) 22 | return q 23 | } 24 | 25 | func (q *Query) Extend(parts ...interface{}) *Query { 26 | for i, p := range parts { 27 | if i%2 == 0 { 28 | q.Text(p.(string)) 29 | } else { 30 | q.Param(p) 31 | } 32 | } 33 | return q 34 | } 35 | 36 | func (q *Query) Exec(db Querier) (sql.Result, error) { 37 | sql := q.buf.String() 38 | return db.Exec(sql, q.Params...) 39 | } 40 | 41 | func (q *Query) Query(db Querier) (*sql.Rows, error) { 42 | sql := q.buf.String() 43 | return db.Query(sql, q.Params...) 44 | } 45 | 46 | func (q *Query) QueryRow(db Querier) *sql.Row { 47 | sql := q.buf.String() 48 | return db.QueryRow(sql, q.Params...) 49 | } 50 | 51 | type Querier interface { 52 | Exec(string, ...interface{}) (sql.Result, error) 53 | Query(string, ...interface{}) (*sql.Rows, error) 54 | QueryRow(string, ...interface{}) *sql.Row 55 | } 56 | -------------------------------------------------------------------------------- /restapi/operations/delete_node.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import ( 4 | "github.com/rancher/etcdb/backend" 5 | "github.com/rancher/etcdb/models" 6 | ) 7 | 8 | type DeleteNode struct { 9 | params struct { 10 | Key string `path:"key"` 11 | PrevValue *string `query:"prevValue"` 12 | PrevIndex *int64 `query:"prevIndex"` 13 | Dir bool `query:"dir"` 14 | Recursive bool `query:"recursive"` 15 | } 16 | Store *backend.SqlBackend 17 | } 18 | 19 | func (op *DeleteNode) Params() interface{} { 20 | return &op.params 21 | } 22 | 23 | func (op *DeleteNode) Call() (interface{}, error) { 24 | var condition backend.DeleteCondition 25 | params := op.params 26 | 27 | switch { 28 | case params.PrevValue != nil: 29 | condition = backend.PrevValue(*params.PrevValue) 30 | case params.PrevIndex != nil: 31 | condition = backend.PrevIndex(*params.PrevIndex) 32 | default: 33 | condition = backend.Always 34 | } 35 | 36 | var node *models.Node 37 | var index int64 38 | var err error 39 | 40 | if params.Dir || params.Recursive { 41 | node, index, err = op.Store.RmDir(params.Key, params.Recursive, condition) 42 | } else { 43 | node, index, err = op.Store.Delete(params.Key, condition) 44 | } 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | return &models.ActionUpdate{ 50 | Action: condition.DeleteActionName(), 51 | Node: models.Node{ 52 | Key: params.Key, 53 | CreatedIndex: node.CreatedIndex, 54 | ModifiedIndex: index, 55 | }, 56 | PrevNode: node, 57 | }, nil 58 | } 59 | -------------------------------------------------------------------------------- /restapi/operations/set_node.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import ( 4 | "github.com/rancher/etcdb/backend" 5 | "github.com/rancher/etcdb/models" 6 | ) 7 | 8 | type SetNode struct { 9 | params struct { 10 | Key string `path:"key"` 11 | Value string `formData:"value"` 12 | TTL *int64 `formData:"ttl"` 13 | Dir bool `formData:"dir"` 14 | PrevValue *string `formData:"prevValue"` 15 | PrevIndex *int64 `formData:"prevIndex"` 16 | PrevExist *bool `formData:"prevExist"` 17 | } 18 | Store *backend.SqlBackend 19 | } 20 | 21 | func (op *SetNode) Params() interface{} { 22 | return &op.params 23 | } 24 | 25 | func (op *SetNode) Call() (interface{}, error) { 26 | var condition backend.SetCondition 27 | params := op.params 28 | 29 | switch { 30 | case params.PrevExist != nil: 31 | condition = backend.PrevExist(*params.PrevExist) 32 | case params.PrevValue != nil: 33 | condition = backend.PrevValue(*params.PrevValue) 34 | case params.PrevIndex != nil: 35 | condition = backend.PrevIndex(*params.PrevIndex) 36 | default: 37 | condition = backend.Always 38 | } 39 | 40 | var node, prevNode *models.Node 41 | var err error 42 | 43 | if params.Dir { 44 | node, prevNode, err = op.Store.MkDir(params.Key, params.TTL, condition) 45 | } else if params.TTL != nil { 46 | node, prevNode, err = op.Store.SetTTL(params.Key, params.Value, *params.TTL, condition) 47 | } else { 48 | node, prevNode, err = op.Store.Set(params.Key, params.Value, condition) 49 | } 50 | 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | return &models.ActionUpdate{ 56 | Action: condition.SetActionName(), 57 | Node: *node, 58 | PrevNode: prevNode, 59 | }, nil 60 | } 61 | -------------------------------------------------------------------------------- /restapi/decode.go: -------------------------------------------------------------------------------- 1 | package restapi 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "reflect" 7 | "strconv" 8 | 9 | "github.com/gorilla/mux" 10 | ) 11 | 12 | // Unmarshal decodes values from the request into a tagged struct. 13 | // 14 | // Similar to json.Unmarshal, but reads the values from the request, based on 15 | // Swagger's naming convention for the parameter locations. 16 | // 17 | // Fields with the following tags will be read from the respective sources: 18 | // `path:"key"` -- gorilla/mux route parameters 19 | // `query:"key"` -- URL query parameters 20 | // `formData:"key"` -- form POST data 21 | func Unmarshal(r *http.Request, o interface{}) error { 22 | r.ParseForm() 23 | // using r.Form instead of r.PostForm, since etcd seems to allow 24 | // parameters set in either 25 | return unmarshal(mux.Vars(r), r.URL.Query(), r.Form, o) 26 | } 27 | 28 | func unmarshal(path map[string]string, query, form url.Values, o interface{}) error { 29 | v := reflect.ValueOf(o) 30 | typ := v.Type().Elem() 31 | 32 | for i := 0; i < typ.NumField(); i++ { 33 | field := typ.Field(i) 34 | 35 | var value string 36 | if key := field.Tag.Get("path"); key != "" { 37 | value = path[key] 38 | } else if key := field.Tag.Get("query"); key != "" { 39 | value = query.Get(key) 40 | } else if key := field.Tag.Get("formData"); key != "" { 41 | value = form.Get(key) 42 | } else { 43 | continue 44 | } 45 | 46 | err := assign(v.Elem().Field(i), value) 47 | if err != nil { 48 | return err 49 | } 50 | } 51 | 52 | return nil 53 | } 54 | 55 | func assign(v reflect.Value, value string) error { 56 | switch v.Kind() { 57 | case reflect.String: 58 | v.SetString(value) 59 | case reflect.Bool: 60 | // TODO error for values other than true / false 61 | v.SetBool(value != "" && value != "false") 62 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 63 | val, err := strconv.ParseInt(value, 10, v.Type().Bits()) 64 | if err != nil { 65 | return err 66 | } 67 | v.SetInt(val) 68 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 69 | val, err := strconv.ParseUint(value, 10, v.Type().Bits()) 70 | if err != nil { 71 | return err 72 | } 73 | v.SetUint(val) 74 | case reflect.Ptr: 75 | if value != "" { 76 | newV := reflect.New(v.Type().Elem()) 77 | err := assign(reflect.Indirect(newV), value) 78 | if err != nil { 79 | return err 80 | } 81 | v.Set(newV) 82 | } 83 | } 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /models/models.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | type Action struct { 9 | Action string `json:"action"` 10 | Node Node `json:"node"` 11 | } 12 | 13 | type ActionUpdate struct { 14 | Action string `json:"action"` 15 | Node Node `json:"node"` 16 | PrevNode *Node `json:"prevNode,omitempty"` 17 | } 18 | 19 | type Node struct { 20 | Key string `json:"key"` 21 | Value string `json:"value"` 22 | CreatedIndex int64 `json:"createdIndex,omitempty"` 23 | ModifiedIndex int64 `json:"modifiedIndex,omitempty"` 24 | Dir bool `json:"dir,omitempty"` 25 | TTL *int64 `json:"ttl,omitempty"` 26 | Expiration *time.Time `json:"expiration,omitempty"` 27 | Nodes []*Node `json:"nodes,omitempty"` 28 | } 29 | 30 | // TODO could reuse implementations from etcd code itself? 31 | 32 | type Error struct { 33 | ErrorCode int `json:"errorCode"` 34 | Message string `json:"message"` 35 | Cause string `json:"cause,omitempty"` 36 | // FIXME should be uint64 37 | Index int64 `json:"index"` 38 | } 39 | 40 | func (e Error) Error() string { 41 | return fmt.Sprintf("etcd error (%d) at index %d %s: %s", e.ErrorCode, e.Index, e.Message, e.Cause) 42 | } 43 | 44 | func NotFound(key string, index int64) Error { 45 | return Error{100, "Key not found", key, index} 46 | } 47 | 48 | func CompareFailed(expected, actual interface{}, index int64) Error { 49 | return Error{101, "Compare failed", fmt.Sprintf("[%v != %v]", expected, actual), index} 50 | } 51 | 52 | func NotAFile(key string, index int64) Error { 53 | return Error{102, "Not a file", key, index} 54 | } 55 | 56 | func NotADirectory(key string, index int64) Error { 57 | return Error{104, "Not a directory", key, index} 58 | } 59 | 60 | func KeyExists(key string, index int64) Error { 61 | return Error{105, "Key already exists", key, index} 62 | } 63 | 64 | func RootReadOnly(index int64) Error { 65 | return Error{107, "Root is read only", "/", index} 66 | } 67 | 68 | func DirectoryNotEmpty(key string, index int64) Error { 69 | return Error{108, "Directory not empty", key, index} 70 | } 71 | 72 | func InvalidField(cause string) Error { 73 | return Error{209, "Invalid field", cause, 0} 74 | } 75 | 76 | func RaftInternalError(cause string) Error { 77 | return Error{300, "Raft Internal Error", cause, 0} 78 | } 79 | 80 | func EventIndexCleared(oldest, requested, index int64) Error { 81 | cause := fmt.Sprintf("the requested history has been cleared [%v/%v]", oldest, requested) 82 | return Error{401, "The event in requested index is outdated and cleared", cause, index} 83 | } 84 | -------------------------------------------------------------------------------- /backend/condition.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import "github.com/rancher/etcdb/models" 4 | 5 | // A Condition represents a test for whether a node operation should be applied 6 | // based on the previous node value. 7 | // The Check method should return nil on success, or return an error with the 8 | // reason the check failed. 9 | type Condition interface { 10 | Check(key string, index int64, node *models.Node) error 11 | } 12 | 13 | // A SetCondition can be used when setting a node. 14 | // It provides the action name that should be used for the set operation. 15 | type SetCondition interface { 16 | Condition 17 | SetActionName() string 18 | } 19 | 20 | // A DeleteCondition can be used when deleting a node. 21 | // It provides the action name that should be used for the delete operation. 22 | type DeleteCondition interface { 23 | Condition 24 | DeleteActionName() string 25 | } 26 | 27 | type always struct{} 28 | 29 | // Always is a condition that always returns true. 30 | var Always always 31 | 32 | func (p always) Check(key string, index int64, node *models.Node) error { 33 | return nil 34 | } 35 | 36 | func (p always) SetActionName() string { 37 | return "set" 38 | } 39 | 40 | func (p always) DeleteActionName() string { 41 | return "set" 42 | } 43 | 44 | // PrevValue matches on the previous node's value. 45 | type PrevValue string 46 | 47 | // Check succeeds if the previous value matches the condition value. 48 | func (p PrevValue) Check(key string, index int64, node *models.Node) error { 49 | if node == nil { 50 | return models.NotFound(key, index) 51 | } 52 | if node.Value != string(p) { 53 | return models.CompareFailed(string(p), node.Value, index) 54 | } 55 | return nil 56 | } 57 | 58 | func (p PrevValue) SetActionName() string { 59 | return "compareAndSwap" 60 | } 61 | 62 | func (p PrevValue) DeleteActionName() string { 63 | return "compareAndDelete" 64 | } 65 | 66 | // PrevIndex matches on the previous node's modifiedIndex. 67 | type PrevIndex int64 68 | 69 | // Check succeeds if the previous node's modifiedIndex matches. 70 | func (p PrevIndex) Check(key string, index int64, node *models.Node) error { 71 | if node == nil { 72 | return models.NotFound(key, index) 73 | } 74 | if node.ModifiedIndex != int64(p) { 75 | return models.CompareFailed(int64(p), node.ModifiedIndex, index) 76 | } 77 | return nil 78 | } 79 | 80 | func (p PrevIndex) SetActionName() string { 81 | return "compareAndSwap" 82 | } 83 | 84 | func (p PrevIndex) DeleteActionName() string { 85 | return "compareAndDelete" 86 | } 87 | 88 | // PrevExist matches on whether there was a previous value. 89 | type PrevExist bool 90 | 91 | // Check succeeds if the existence of the previous node matches the condition's 92 | // truth value. 93 | func (p PrevExist) Check(key string, index int64, node *models.Node) error { 94 | if node == nil { 95 | if bool(p) { 96 | return models.NotFound(key, index) 97 | } 98 | } else if !bool(p) { 99 | return models.KeyExists(key, index) 100 | } 101 | return nil 102 | } 103 | 104 | func (p PrevExist) SetActionName() string { 105 | if bool(p) { 106 | return "update" 107 | } 108 | return "create" 109 | } 110 | -------------------------------------------------------------------------------- /integration-tests/basic.bash: -------------------------------------------------------------------------------- 1 | PREFIX="etcd-test-$$" 2 | POSTGRES_IMAGE="yvess/alpine-postgres" 3 | 4 | 5 | docker-ip() { 6 | local docker_addr="${DOCKER_HOST#tcp://}" 7 | echo "${docker_addr%:*}" 8 | } 9 | 10 | 11 | container-port() { 12 | local addr="$(docker port $1 $2)" 13 | echo "${addr#*:}" 14 | } 15 | 16 | 17 | setup() { 18 | docker build -t "$PREFIX-etcdb" . 19 | 20 | docker pull "$POSTGRES_IMAGE" > /dev/null 21 | docker run --name "$PREFIX-postgres" -P -d \ 22 | -e PGDATA=/var/services/data/postgres \ 23 | -e PGUSER=etcdb \ 24 | -e PGPASSWORD=etcdb \ 25 | -e PGDB=etcdb \ 26 | "$POSTGRES_IMAGE" > /dev/null 27 | sleep 2 28 | 29 | local db_conn="user=etcdb password=etcdb dbname=etcdb host=db sslmode=disable" 30 | local public_ip=$(docker-ip) 31 | 32 | docker run --rm -it \ 33 | --link "$PREFIX-postgres:db" \ 34 | "$PREFIX-etcdb" \ 35 | -init-db postgres "$db_conn" 36 | 37 | docker run --name "$PREFIX-etcdb" -d \ 38 | --link "$PREFIX-postgres:db" \ 39 | -p 23790:2379 \ 40 | "$PREFIX-etcdb" \ 41 | -advertise-client-urls http://$public_ip:23790 \ 42 | -listen-client-urls http://0.0.0.0:2379 \ 43 | postgres "$db_conn" 44 | 45 | sleep 2 46 | 47 | export ETCDCTL_PEERS="http://$public_ip:23790" 48 | } 49 | setup 50 | 51 | 52 | teardown() { 53 | local containers="$(docker ps -q -f name=$PREFIX)" 54 | echo "$containers" | xargs docker rm -f > /dev/null 55 | docker rmi "$PREFIX-etcdb" > /dev/null 56 | } 57 | trap teardown EXIT 58 | 59 | 60 | cleanKeys() { 61 | etcdctl ls / | xargs -n1 etcdctl rm --recursive 62 | } 63 | 64 | 65 | getField() { 66 | cat | grep $1 | fieldValue 67 | } 68 | 69 | 70 | fieldValue() { 71 | local line=$(head -n1) 72 | echo ${line#*: } 73 | } 74 | 75 | 76 | T_setGetDelete() { 77 | cleanKeys 78 | 79 | result="$(etcdctl ls /)" 80 | if [[ "$result" != "" ]]; then 81 | $T_fail "initial listing should be empty" 82 | return 83 | fi 84 | 85 | etcdctl set /foo bar > /dev/null 86 | 87 | result="$(etcdctl ls /)" 88 | if [[ "$result" != "/foo" ]]; then 89 | $T_fail "listing should contain /foo, but got $result" 90 | return 91 | fi 92 | 93 | result="$(etcdctl get /foo)" 94 | if [[ "$result" != "bar" ]]; then 95 | $T_fail "value of /foo should be bar, but got $result" 96 | return 97 | fi 98 | 99 | etcdctl rm /foo 100 | 101 | result="$(etcdctl ls /)" 102 | if [[ "$result" != "" ]]; then 103 | $T_fail "listing should be empty again after removing /foo" 104 | return 105 | fi 106 | } 107 | 108 | T_expire() { 109 | cleanKeys 110 | 111 | local createdIndex=$(etcdctl -o extended set --ttl 5 /foo bar | getField Created-Index) 112 | if [[ "$createdIndex" == "" ]]; then 113 | $T_fail "should have gotten the created index value" 114 | return 115 | fi 116 | 117 | result="$(etcdctl get /foo)" 118 | if [[ "$result" != "bar" ]]; then 119 | $T_fail "value should be set to bar, but got: $result" 120 | return 121 | fi 122 | 123 | local prevValue=$(etcdctl -o extended watch --after-index $createdIndex /foo | getField PrevNode.Value) 124 | 125 | if [[ "$prevValue" != "bar" ]]; then 126 | $T_fail "should get previous value of bar, but got: $prevValue" 127 | fi 128 | } 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | etcdb is a work-in-progress to provide an implementation of the etcd REST API 2 | on top of a SQL backend. 3 | 4 | # Contact 5 | For bugs, questions, comments, corrections, suggestions, etc., open an issue in 6 | [rancher/rancher](//github.com/rancher/rancher/issues) with a title starting with `[etcdb] `. 7 | 8 | Or just [click here](//github.com/rancher/rancher/issues/new?title=%5Betcdb%5D%20) to create a new issue. 9 | 10 | # Usage 11 | 12 | ## Database setup 13 | 14 | To create the required database tables, run the `etcdb` command once with the 15 | `-init-db` flag. This will create the tables and then exit (see below for 16 | the database connection parameters): 17 | 18 | ``` 19 | etcdb -init-db 20 | ``` 21 | 22 | ## Starting the server 23 | 24 | Etcdb supports either MySQL or Postgres backend databases. The `etcdb` command 25 | takes two required arguments, the type of database to connect to, and 26 | parameters for the database connection. Here are examples with the most commonly 27 | specified parameters: 28 | 29 | ``` 30 | etcdb postgres "user=username password=password host=hostname dbname=dbname sslmode=disable" 31 | 32 | etcdb mysql username:password@tcp(hostname:3306)/dbname 33 | ``` 34 | 35 | Additional parameters are documented for each of the Go database drivers: 36 | 37 | * [Postgres connection parameters](https://godoc.org/github.com/lib/pq#hdr-Connection_String_Parameters) 38 | * [MySQL connection parameters](https://github.com/go-sql-driver/mysql#dsn-data-source-name) 39 | 40 | ## Client connections 41 | 42 | For compatibility with `etcd`, the `etcdb` server by default listens on ports 43 | `2379` and `4001` on `localhost`. 44 | 45 | To listen on other ports or network interfaces, `etcdb` takes two options 46 | `-listen-client-urls` and `-advertise-client-urls` which are compatible with 47 | the same options on the `etcd` server. 48 | 49 | `-listen-client-urls` specifies the hosts and ports that the server will listen 50 | for connections on. 51 | 52 | `-advertise-client-urls` specifies the matching URLs that are accessible to 53 | the client. When cluster aware clients such as `etcdctl` connect to the server, 54 | this is the list of URLs it will "advertise" for these clients to connect to. 55 | 56 | To listen on a public network interface, these options can have the same value: 57 | 58 | ``` 59 | etcdb \ 60 | -listen-client-urls http://10.0.0.1:92379 \ 61 | -advertise-client-urls http://10.0.0.1:92379 \ 62 | postgres "sslmode=disable" 63 | ``` 64 | 65 | However, if for example, you're running the server in a Docker container, 66 | forwarding the external port `92379` to the container's port `2379`. You would 67 | would start `etcdb` listening for connections on all container IPs on port 68 | `2379`, but *advertise* the client URL with the publicly accessible IP and port 69 | number: 70 | 71 | ``` 72 | etcdb \ 73 | -listen-client-urls http://0.0.0.0:2379 \ 74 | -advertise-client-urls http://${PUBLIC_IP}:92379 \ 75 | postgres "sslmode=disable" 76 | ``` 77 | 78 | # Testing 79 | 80 | ## Unit tests 81 | 82 | The Makefile has a helper for running the Go tests: 83 | 84 | ``` 85 | make test 86 | ``` 87 | 88 | ## Integration testing 89 | 90 | The `integration-tests` directory contains tests using the `etcdctl` command to 91 | exercise the public etcd-compatible API. 92 | 93 | The integration tests are written in Bash and use 94 | [basht](https://github.com/progrium/basht) to run. This can be installed with: 95 | 96 | ``` 97 | make test-deps 98 | ``` 99 | 100 | The tests also expect `etcdctl` and `docker` to be on the path. The 101 | `DOCKER_HOST` environment variable needs to be set to a `tcp://` address since 102 | the tests need the Docker IP for `etcdctl` to connect to. 103 | 104 | Run the integration tests with: 105 | 106 | ``` 107 | make test-integration 108 | ``` 109 | -------------------------------------------------------------------------------- /restapi/decode_test.go: -------------------------------------------------------------------------------- 1 | package restapi 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "reflect" 7 | "runtime" 8 | "testing" 9 | ) 10 | 11 | func TestUnmarshal_Path(t *testing.T) { 12 | v := struct { 13 | String string `path:"aString"` 14 | Bool bool `path:"aBool"` 15 | Number int `path:"num"` 16 | }{} 17 | 18 | ok(t, unmarshal(map[string]string{ 19 | "aString": "bar", 20 | "aBool": "true", 21 | "num": "42", 22 | }, nil, nil, &v)) 23 | 24 | equals(t, v.String, "bar") 25 | equals(t, v.Bool, true) 26 | equals(t, v.Number, 42) 27 | } 28 | 29 | func TestUnmarshal_Query(t *testing.T) { 30 | v := struct { 31 | String string `query:"aString"` 32 | Bool bool `query:"aBool"` 33 | Number int `query:"num"` 34 | }{} 35 | 36 | ok(t, unmarshal(nil, map[string][]string{ 37 | "aString": {"bar"}, 38 | "aBool": {"true"}, 39 | "num": {"42"}, 40 | }, nil, &v)) 41 | 42 | equals(t, v.String, "bar") 43 | equals(t, v.Bool, true) 44 | equals(t, v.Number, 42) 45 | } 46 | 47 | func TestUnmarshal_Form(t *testing.T) { 48 | v := struct { 49 | String string `formData:"aString"` 50 | Bool bool `formData:"aBool"` 51 | Number int `formData:"num"` 52 | }{} 53 | 54 | ok(t, unmarshal(nil, nil, map[string][]string{ 55 | "aString": {"bar"}, 56 | "aBool": {"true"}, 57 | "num": {"42"}, 58 | }, &v)) 59 | 60 | equals(t, v.String, "bar") 61 | equals(t, v.Bool, true) 62 | equals(t, v.Number, 42) 63 | } 64 | 65 | func TestUnmarshal_InvalidNumber(t *testing.T) { 66 | v := struct { 67 | Number int `path:"num"` 68 | }{} 69 | 70 | err := unmarshal(map[string]string{ 71 | "num": "asdf", 72 | }, nil, nil, &v) 73 | 74 | if err == nil { 75 | t.Fatal("expected an error for invalid number, but got nil") 76 | } 77 | } 78 | 79 | func TestUnmarshal_InvalidNumberPointer(t *testing.T) { 80 | v := struct { 81 | Number *int `path:"num"` 82 | }{} 83 | 84 | err := unmarshal(map[string]string{ 85 | "num": "asdf", 86 | }, nil, nil, &v) 87 | 88 | if err == nil { 89 | t.Fatal("expected an error for invalid number, but got nil") 90 | } 91 | } 92 | 93 | func TestAssign_String(t *testing.T) { 94 | var v string 95 | ok(t, assign(reflect.ValueOf(&v).Elem(), "bar")) 96 | equals(t, v, "bar") 97 | } 98 | 99 | func TestAssign_StringPointer(t *testing.T) { 100 | var v *string 101 | ok(t, assign(reflect.ValueOf(&v).Elem(), "bar")) 102 | equals(t, *v, "bar") 103 | } 104 | 105 | func TestAssign_Bool_True(t *testing.T) { 106 | v := false 107 | ok(t, assign(reflect.ValueOf(&v).Elem(), "true")) 108 | equals(t, v, true) 109 | } 110 | 111 | func TestAssign_Bool_False(t *testing.T) { 112 | v := true 113 | ok(t, assign(reflect.ValueOf(&v).Elem(), "false")) 114 | equals(t, v, false) 115 | } 116 | 117 | func TestAssign_BoolPointer_True(t *testing.T) { 118 | v := new(bool) 119 | *v = false 120 | ok(t, assign(reflect.ValueOf(&v).Elem(), "true")) 121 | equals(t, true, *v) 122 | } 123 | 124 | func TestAssign_BoolPointer_False(t *testing.T) { 125 | v := new(bool) 126 | *v = true 127 | ok(t, assign(reflect.ValueOf(&v).Elem(), "false")) 128 | equals(t, false, *v) 129 | } 130 | 131 | func TestAssign_Int(t *testing.T) { 132 | var v int 133 | ok(t, assign(reflect.ValueOf(&v).Elem(), "42")) 134 | equals(t, 42, v) 135 | } 136 | 137 | func TestAssign_IntPointer(t *testing.T) { 138 | var v *int 139 | ok(t, assign(reflect.ValueOf(&v).Elem(), "42")) 140 | equals(t, 42, *v) 141 | } 142 | 143 | func TestAssign_Int8(t *testing.T) { 144 | var v int8 145 | ok(t, assign(reflect.ValueOf(&v).Elem(), "42")) 146 | equals(t, int8(42), v) 147 | } 148 | 149 | func TestAssign_Int16(t *testing.T) { 150 | var v int16 151 | ok(t, assign(reflect.ValueOf(&v).Elem(), "42")) 152 | equals(t, int16(42), v) 153 | } 154 | 155 | func TestAssign_Int32(t *testing.T) { 156 | var v int32 157 | ok(t, assign(reflect.ValueOf(&v).Elem(), "42")) 158 | equals(t, int32(42), v) 159 | } 160 | 161 | func TestAssign_Int64(t *testing.T) { 162 | var v int64 163 | ok(t, assign(reflect.ValueOf(&v).Elem(), "42")) 164 | equals(t, int64(42), v) 165 | } 166 | 167 | func TestAssign_Uint(t *testing.T) { 168 | var v uint 169 | ok(t, assign(reflect.ValueOf(&v).Elem(), "42")) 170 | equals(t, uint(42), v) 171 | } 172 | 173 | func TestAssign_Uint8(t *testing.T) { 174 | var v uint8 175 | ok(t, assign(reflect.ValueOf(&v).Elem(), "42")) 176 | equals(t, uint8(42), v) 177 | } 178 | 179 | func TestAssign_Uint16(t *testing.T) { 180 | var v uint16 181 | ok(t, assign(reflect.ValueOf(&v).Elem(), "42")) 182 | equals(t, uint16(42), v) 183 | } 184 | 185 | func TestAssign_Uint32(t *testing.T) { 186 | var v uint32 187 | ok(t, assign(reflect.ValueOf(&v).Elem(), "42")) 188 | equals(t, uint32(42), v) 189 | } 190 | 191 | func TestAssign_Uint64(t *testing.T) { 192 | var v uint64 193 | ok(t, assign(reflect.ValueOf(&v).Elem(), "42")) 194 | equals(t, uint64(42), v) 195 | } 196 | 197 | // ok fails the test if an err is not nil. 198 | func ok(tb testing.TB, err error) { 199 | if err != nil { 200 | _, file, line, _ := runtime.Caller(1) 201 | fmt.Printf("\033[31m%s:%d: unexpected error: %s\033[39m\n\n", filepath.Base(file), line, err.Error()) 202 | tb.FailNow() 203 | } 204 | } 205 | 206 | // equals fails the test if exp is not equal to act. 207 | func equals(tb testing.TB, exp, act interface{}) { 208 | if !reflect.DeepEqual(exp, act) { 209 | _, file, line, _ := runtime.Caller(1) 210 | fmt.Printf("\033[31m%s:%d:\n\n\texp: %#v\n\n\tgot: %#v\033[39m\n\n", filepath.Base(file), line, exp, act) 211 | tb.FailNow() 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /backend/dialect.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/go-sql-driver/mysql" 10 | "github.com/lib/pq" 11 | ) 12 | 13 | type dbDialect interface { 14 | Open(driver, dataSource string) (*sql.DB, error) 15 | tableDefinitions() []string 16 | nameParam([]interface{}) string 17 | incrementIndex(Querier) (int64, error) 18 | expiration(*Query, int64) 19 | isDuplicateKeyError(error) bool 20 | now() string 21 | ttl() string 22 | } 23 | 24 | type mysqlDialect struct{} 25 | 26 | func (d mysqlDialect) Open(driver, dataSource string) (*sql.DB, error) { 27 | sep := "?" 28 | if strings.ContainsRune(dataSource, '?') { 29 | sep = "&" 30 | } 31 | // Enable the ANSI_QUOTES mode so that MySQL allows double-quotes around 32 | // column or table names to escape reserved words instead of backticks. 33 | // This way the same escaping syntax works consistently across MySQL and 34 | // Postgres. 35 | dataSource = dataSource + sep + "sql_mode=ANSI_QUOTES" 36 | return sql.Open(driver, dataSource) 37 | } 38 | 39 | func (d mysqlDialect) tableDefinitions() []string { 40 | return []string{ 41 | `CREATE TABLE "nodes" ( 42 | "key" varchar(255), 43 | "created" bigint NOT NULL, 44 | "modified" bigint NOT NULL, 45 | "deleted" bigint NOT NULL DEFAULT 0, 46 | "value" text NOT NULL DEFAULT '', 47 | "expiration" timestamp NULL, 48 | "dir" boolean NOT NULL DEFAULT 0, 49 | "path_depth" integer, 50 | PRIMARY KEY ("deleted", "key") 51 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8`, 52 | 53 | `CREATE INDEX "nodes_key_modified_idx" ON "nodes" ("key", "modified")`, 54 | `CREATE INDEX "nodes_deleted_path_depth_idx" ON "nodes" ("deleted", "path_depth")`, 55 | `CREATE INDEX "nodes_deleted_expiration_idx" ON "nodes" ("deleted", "expiration")`, 56 | 57 | `CREATE TABLE "index" ( 58 | "index" bigint, 59 | PRIMARY KEY ("index") 60 | ) ENGINE=InnoDB`, 61 | 62 | `CREATE TABLE "changes" ( 63 | "index" bigint, 64 | "key" varchar(255) NOT NULL, 65 | "action" varchar(32) NOT NULL, 66 | "prev_node_modified" bigint, 67 | PRIMARY KEY ("index", "key") 68 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8`, 69 | } 70 | } 71 | 72 | func (d mysqlDialect) nameParam(params []interface{}) string { 73 | return "?" 74 | } 75 | 76 | func (d mysqlDialect) incrementIndex(db Querier) (index int64, err error) { 77 | _, err = db.Exec(` 78 | UPDATE "index" SET "index" = "index" + 1 79 | `) 80 | if err != nil { 81 | return 82 | } 83 | err = db.QueryRow(`SELECT "index" FROM "index"`).Scan(&index) 84 | return 85 | } 86 | 87 | func (d mysqlDialect) expiration(q *Query, ttl int64) { 88 | q.Extend(`DATE_ADD(UTC_TIMESTAMP, INTERVAL `, ttl, ` SECOND)`) 89 | } 90 | 91 | func (d mysqlDialect) now() string { 92 | return "UTC_TIMESTAMP" 93 | } 94 | 95 | func (d mysqlDialect) ttl() string { 96 | return "TIMESTAMPDIFF(SECOND, UTC_TIMESTAMP, expiration)" 97 | } 98 | 99 | func (d mysqlDialect) isDuplicateKeyError(err error) bool { 100 | if err, ok := err.(*mysql.MySQLError); ok { 101 | return err.Number == 1062 102 | } 103 | return false 104 | } 105 | 106 | // PostgreSQL 107 | 108 | type postgresDialect struct{} 109 | 110 | func (d postgresDialect) Open(driver, dataSource string) (*sql.DB, error) { 111 | return sql.Open(driver, dataSource) 112 | } 113 | 114 | func (d postgresDialect) tableDefinitions() []string { 115 | return []string{ 116 | `CREATE TABLE "nodes" ( 117 | "key" varchar(2048), 118 | "created" bigint NOT NULL, 119 | "modified" bigint NOT NULL, 120 | "deleted" bigint DEFAULT 0, 121 | "value" text NOT NULL DEFAULT '', 122 | "expiration" timestamp, 123 | "dir" boolean NOT NULL DEFAULT 'false', 124 | "path_depth" integer, 125 | PRIMARY KEY ("deleted", "key") 126 | )`, 127 | 128 | // need varchar_pattern_ops index to optimize LIKE queries 129 | // but not allowed in the primary key 130 | `CREATE INDEX ON "nodes" ("deleted", "key" varchar_pattern_ops)`, 131 | 132 | `CREATE INDEX ON "nodes" ("key", "modified")`, 133 | `CREATE INDEX ON "nodes" ("deleted", "path_depth")`, 134 | `CREATE INDEX ON "nodes" ("deleted", "expiration")`, 135 | 136 | `CREATE TABLE "index" ( 137 | "index" bigint, 138 | PRIMARY KEY ("index") 139 | )`, 140 | 141 | `CREATE TABLE "changes" ( 142 | "index" bigint, 143 | "key" varchar(2048) NOT NULL, 144 | "action" varchar(32) NOT NULL, 145 | "prev_node_modified" bigint, 146 | PRIMARY KEY ("index", "key") 147 | )`, 148 | 149 | // Postgres isn't using the primary key for the query to refresh 150 | // the changes cache: 151 | // WHERE "index" > ? ORDER BY "index" 152 | // so need another index just on "index" column 153 | `CREATE INDEX ON "changes" ("index")`, 154 | } 155 | } 156 | 157 | func (d postgresDialect) nameParam(params []interface{}) string { 158 | return fmt.Sprintf("$%d", len(params)) 159 | } 160 | 161 | func (d postgresDialect) incrementIndex(db Querier) (index int64, err error) { 162 | err = db.QueryRow(` 163 | UPDATE index SET index = index + 1 RETURNING index 164 | `).Scan(&index) 165 | return 166 | } 167 | 168 | func (d postgresDialect) expiration(q *Query, ttl int64) { 169 | q.Extend(`CURRENT_TIMESTAMP AT TIME ZONE 'UTC' + `, 170 | strconv.FormatInt(ttl, 10), 171 | `::INTERVAL`, 172 | ) 173 | } 174 | 175 | func (d postgresDialect) now() string { 176 | return `CURRENT_TIMESTAMP AT TIME ZONE 'UTC'` 177 | } 178 | 179 | func (d postgresDialect) ttl() string { 180 | return "CAST(EXTRACT(EPOCH FROM expiration) - EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) AS integer)" 181 | } 182 | 183 | func (d postgresDialect) isDuplicateKeyError(err error) bool { 184 | if err, ok := err.(*pq.Error); ok { 185 | return err.Code == "23505" 186 | } 187 | return false 188 | } 189 | -------------------------------------------------------------------------------- /backend/listener_test.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/rancher/etcdb/models" 8 | ) 9 | 10 | func Test_Watch_Change(t *testing.T) { 11 | store := testConn(t) 12 | defer store.Close() 13 | 14 | cw := Watch(store, 1*time.Second) 15 | defer cw.Stop() 16 | 17 | go func() { 18 | time.Sleep(10 * time.Millisecond) 19 | store.Set("/foo", "bar", Always) 20 | }() 21 | 22 | act, err := cw.NextChange("/foo", false, int64(0)) 23 | ok(t, err) 24 | 25 | equals(t, "/foo", act.Node.Key) 26 | equals(t, "bar", act.Node.Value) 27 | } 28 | 29 | func Test_Watch_ReturnsFirstMatchingChange(t *testing.T) { 30 | store := testConn(t) 31 | defer store.Close() 32 | 33 | cw := Watch(store, 1*time.Second) 34 | defer cw.Stop() 35 | 36 | store.Set("/foo", "first", Always) 37 | store.Set("/foo", "second", Always) 38 | time.Sleep(2 * time.Second) 39 | 40 | act, err := cw.NextChange("/foo", false, int64(1)) 41 | ok(t, err) 42 | 43 | equals(t, "/foo", act.Node.Key) 44 | equals(t, "first", act.Node.Value) 45 | } 46 | 47 | func Test_Watch_IgnoresOldChangeWhenIndexNotSet(t *testing.T) { 48 | store := testConn(t) 49 | defer store.Close() 50 | 51 | cw := Watch(store, 1*time.Second) 52 | defer cw.Stop() 53 | 54 | store.Set("/foo", "first", Always) 55 | time.Sleep(2 * time.Second) 56 | 57 | go func() { 58 | time.Sleep(10 * time.Millisecond) 59 | store.Set("/foo", "second", Always) 60 | }() 61 | 62 | act, err := cw.NextChange("/foo", false, int64(0)) 63 | ok(t, err) 64 | 65 | equals(t, "/foo", act.Node.Key) 66 | equals(t, "second", act.Node.Value) 67 | } 68 | 69 | func Test_ChangeList_Empty(t *testing.T) { 70 | cl := newChangeList(100) 71 | equals(t, 0, cl.Size) 72 | } 73 | 74 | func Test_ChangeList_AddOne(t *testing.T) { 75 | cl := newChangeList(100) 76 | change := cl.Next() 77 | 78 | equals(t, 1, cl.Size) 79 | equals(t, int64(0), change.Index) 80 | 81 | if cl.First() != change { 82 | t.Error("First should be the same change") 83 | } 84 | 85 | if cl.Last() != change { 86 | t.Error("Last should be the same change") 87 | } 88 | } 89 | 90 | func Test_ChangeList_FirstLast(t *testing.T) { 91 | cl := newChangeList(100) 92 | first := cl.Next() 93 | second := cl.Next() 94 | 95 | if cl.First() != first { 96 | t.Error("First should be the same change") 97 | } 98 | 99 | if cl.Last() != second { 100 | t.Error("Last should be the same change") 101 | } 102 | 103 | if first == second { 104 | t.Error("first and second should be different changes") 105 | } 106 | } 107 | 108 | func Test_ChangeList_WrapAround(t *testing.T) { 109 | cl := newChangeList(2) 110 | first := cl.Next() 111 | second := cl.Next() 112 | third := cl.Next() 113 | 114 | if first != third { 115 | t.Error("first and third should the same change") 116 | } 117 | 118 | if cl.First() != second { 119 | t.Error("the First() position should have been incremented to where second was") 120 | } 121 | } 122 | 123 | func Test_ChangeList_FirstWrapAround(t *testing.T) { 124 | cl := newChangeList(2) 125 | cl.Next() 126 | cl.Next() 127 | 128 | equals(t, cl.Size, cl.Capacity) 129 | 130 | first := cl.First() 131 | 132 | if first != cl.Next() { 133 | t.Error("the first item before should now be the next item") 134 | } 135 | } 136 | 137 | func Test_ChangeList_WrapAroundClearsValue(t *testing.T) { 138 | cl := newChangeList(2) 139 | first := cl.Next() 140 | first.value = &models.ActionUpdate{} 141 | 142 | _ = cl.Next() 143 | third := cl.Next() 144 | 145 | if first != third { 146 | t.Error("first and third should the same change") 147 | } 148 | if first.value != nil { 149 | t.Error("value should be reset to nil") 150 | } 151 | if third.value != nil { 152 | t.Error("value should be reset to nil") 153 | } 154 | } 155 | 156 | func Test_ChangeList_Pop(t *testing.T) { 157 | cl := newChangeList(100) 158 | first := cl.Next() 159 | _ = cl.Next() 160 | 161 | equals(t, 2, cl.Size) 162 | 163 | cl.Pop() 164 | 165 | equals(t, 1, cl.Size) 166 | 167 | if cl.Last() != first { 168 | t.Error("after pop, last element should be first again") 169 | } 170 | } 171 | 172 | func Test_ChangeList_PopEmpty(t *testing.T) { 173 | cl := newChangeList(100) 174 | cl.Pop() 175 | equals(t, 0, cl.Size) 176 | } 177 | 178 | func Test_Match_Same(t *testing.T) { 179 | w := &watch{Key: "/foo", Index: 1} 180 | c := &change{Key: "/foo", Index: 1, Action: "set"} 181 | equals(t, true, w.Match(c)) 182 | } 183 | 184 | func Test_Match_SubkeyNotRecursive(t *testing.T) { 185 | w := &watch{Key: "/foo"} 186 | c := &change{Key: "/foo/bar", Index: 1, Action: "set"} 187 | equals(t, false, w.Match(c)) 188 | } 189 | 190 | func Test_Match_SubkeyRecursive(t *testing.T) { 191 | w := &watch{Key: "/foo", Index: 1, Recursive: true} 192 | c := &change{Key: "/foo/bar", Index: 1, Action: "set"} 193 | equals(t, true, w.Match(c)) 194 | } 195 | 196 | func Test_Match_PrefixRecursive(t *testing.T) { 197 | w := &watch{Key: "/foo", Recursive: true} 198 | c := &change{Key: "/foobar", Index: 1, Action: "set"} 199 | equals(t, false, w.Match(c)) 200 | } 201 | 202 | func Test_Match_SameKeyRecursive(t *testing.T) { 203 | w := &watch{Key: "/foo", Recursive: true} 204 | c := &change{Key: "/foo", Index: 1, Action: "set"} 205 | equals(t, true, w.Match(c)) 206 | } 207 | 208 | func Test_Match_LowerIndex(t *testing.T) { 209 | w := &watch{Key: "/foo", Index: 1} 210 | c := &change{Key: "/foo", Index: 2, Action: "set"} 211 | equals(t, true, w.Match(c)) 212 | } 213 | 214 | func Test_Match_HigherIndex(t *testing.T) { 215 | w := &watch{Key: "/foo", Index: 2} 216 | c := &change{Key: "/foo", Index: 1, Action: "set"} 217 | equals(t, false, w.Match(c)) 218 | } 219 | 220 | func Test_Match_SetParent(t *testing.T) { 221 | w := &watch{Key: "/foo/bar"} 222 | c := &change{Key: "/foo", Index: 1, Action: "set"} 223 | equals(t, false, w.Match(c)) 224 | } 225 | 226 | func Test_Match_DeleteParent(t *testing.T) { 227 | w := &watch{Key: "/foo/bar"} 228 | c := &change{Key: "/foo", Index: 1, Action: "delete"} 229 | equals(t, true, w.Match(c)) 230 | } 231 | 232 | func Test_Match_ExpireParent(t *testing.T) { 233 | w := &watch{Key: "/foo/bar"} 234 | c := &change{Key: "/foo", Index: 1, Action: "expire"} 235 | equals(t, true, w.Match(c)) 236 | } 237 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "net" 9 | "net/http" 10 | "net/url" 11 | "os" 12 | "path/filepath" 13 | "strings" 14 | "time" 15 | 16 | "github.com/gorilla/mux" 17 | 18 | "github.com/rancher/etcdb/backend" 19 | "github.com/rancher/etcdb/models" 20 | "github.com/rancher/etcdb/restapi" 21 | "github.com/rancher/etcdb/restapi/operations" 22 | ) 23 | 24 | type UrlsValue []url.URL 25 | 26 | func (uv *UrlsValue) Set(s string) error { 27 | vals := strings.Split(s, ",") 28 | urls := make([]url.URL, len(vals)) 29 | 30 | for i, val := range vals { 31 | val = strings.TrimSpace(val) 32 | u, err := url.Parse(val) 33 | if err != nil { 34 | return err 35 | } 36 | if u.Scheme != "http" { 37 | return fmt.Errorf("URLs must use the http scheme: %s", val) 38 | } 39 | if u.Path != "" { 40 | return fmt.Errorf("URLs cannot include a path: %s", val) 41 | } 42 | if _, _, err := net.SplitHostPort(u.Host); err != nil { 43 | return fmt.Errorf("URLs must include a port: %s", val) 44 | } 45 | 46 | urls[i] = *u 47 | } 48 | 49 | *uv = urls 50 | return nil 51 | } 52 | 53 | func (uv *UrlsValue) String() string { 54 | // for flags, join with just comma since spaces are less shell-friendly 55 | return uv.Join(",") 56 | } 57 | 58 | func (uv *UrlsValue) Join(sep string) string { 59 | vals := make([]string, len(*uv)) 60 | for i, u := range *uv { 61 | vals[i] = u.String() 62 | } 63 | return strings.Join(vals, sep) 64 | } 65 | 66 | func UrlsFlag(name, value, usage string) *UrlsValue { 67 | urls := &UrlsValue{} 68 | urls.Set(value) 69 | flag.Var(urls, name, usage) 70 | return urls 71 | } 72 | 73 | var defaultClientUrls = "http://localhost:2379,http://localhost:4001" 74 | 75 | var initDb = flag.Bool("init-db", false, "Initialize the DB schema and exit.") 76 | var watchPoll = flag.Duration("watch-poll", 1*time.Second, "Poll rate for watches.") 77 | var listenClientUrls = UrlsFlag("listen-client-urls", defaultClientUrls, "List of URLs to listen on for client traffic.") 78 | var advertiseClientUrls = UrlsFlag("advertise-client-urls", defaultClientUrls, "List of public URLs available to access the client.") 79 | 80 | func main() { 81 | flag.Usage = func() { 82 | executable := os.Args[0] 83 | cmd := filepath.Base(executable) 84 | 85 | fmt.Fprintf(os.Stderr, "Usage of %s:\n\n", executable) 86 | fmt.Fprintf(os.Stderr, " %s [options] \n\n", cmd) 87 | flag.PrintDefaults() 88 | 89 | fmt.Fprintln(os.Stderr, "\n Examples:") 90 | fmt.Fprintf(os.Stderr, " %s postgres \"user=username password=password host=hostname dbname=dbname sslmode=disable\"\n", cmd) 91 | fmt.Fprintf(os.Stderr, " %s mysql username:password@tcp(hostname:3306)/dbname\n", cmd) 92 | 93 | fmt.Fprintln(os.Stderr, "\n Datasource formats:") 94 | fmt.Fprintln(os.Stderr, " postgres: https://godoc.org/github.com/lib/pq#hdr-Connection_String_Parameters") 95 | fmt.Fprintln(os.Stderr, " mysql: https://github.com/go-sql-driver/mysql#dsn-data-source-name") 96 | } 97 | 98 | flag.Parse() 99 | if flag.NArg() != 2 { 100 | flag.Usage() 101 | os.Exit(2) 102 | } 103 | 104 | dbDriver := flag.Arg(0) 105 | dbDataSource := flag.Arg(1) 106 | 107 | fmt.Println("connecting to database:", dbDriver, dbDataSource) 108 | store, err := backend.New(dbDriver, dbDataSource) 109 | if err != nil { 110 | log.Fatalln(err) 111 | } 112 | 113 | if *initDb { 114 | fmt.Println("initializing db schema...") 115 | err = store.CreateSchema() 116 | if err != nil { 117 | log.Fatalln(err) 118 | } 119 | return 120 | } 121 | 122 | cw := backend.Watch(store, *watchPoll) 123 | 124 | r := mux.NewRouter() 125 | 126 | r.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) { 127 | fmt.Fprint(w, "2") 128 | }) 129 | 130 | r.HandleFunc("/v2/machines", func(w http.ResponseWriter, r *http.Request) { 131 | // for etcdctl it expects a comma and space separator instead of comma-only 132 | fmt.Fprint(w, advertiseClientUrls.Join(", ")) 133 | }) 134 | 135 | r.HandleFunc("/v2/keys{key:/.*}", func(rw http.ResponseWriter, r *http.Request) { 136 | var op operations.Operation 137 | switch r.Method { 138 | case "GET": 139 | op = &operations.GetNode{Store: store, Watcher: cw} 140 | case "PUT": 141 | op = &operations.SetNode{Store: store} 142 | case "POST": 143 | op = &operations.CreateInOrderNode{Store: store} 144 | case "DELETE": 145 | op = &operations.DeleteNode{Store: store} 146 | default: 147 | rw.Header().Set("Allow", "GET, PUT, POST, DELETE") 148 | rw.WriteHeader(http.StatusMethodNotAllowed) 149 | return 150 | } 151 | 152 | res := func() interface{} { 153 | if err := restapi.Unmarshal(r, op.Params()); err != nil { 154 | return models.InvalidField(err.Error()) 155 | } 156 | 157 | res, err := op.Call() 158 | if _, ok := err.(models.Error); ok { 159 | return err 160 | } else if err != nil { 161 | log.Println(err) 162 | return models.RaftInternalError(err.Error()) 163 | } 164 | 165 | return res 166 | }() 167 | 168 | js, _ := json.Marshal(res) 169 | 170 | rw.Header().Set("Content-Type", "application/json") 171 | 172 | if err, ok := res.(models.Error); ok { 173 | rw.Header().Add("X-Etcd-Index", fmt.Sprint(err.Index)) 174 | 175 | switch err.ErrorCode { 176 | default: 177 | rw.WriteHeader(http.StatusBadRequest) 178 | case 100: 179 | rw.WriteHeader(http.StatusNotFound) 180 | case 101: 181 | rw.WriteHeader(http.StatusPreconditionFailed) 182 | case 102: 183 | rw.WriteHeader(http.StatusForbidden) 184 | case 105: 185 | rw.WriteHeader(http.StatusPreconditionFailed) 186 | case 108: 187 | rw.WriteHeader(http.StatusForbidden) 188 | case 300: 189 | rw.WriteHeader(http.StatusInternalServerError) 190 | } 191 | } 192 | 193 | fmt.Fprintln(rw, string(js)) 194 | }) 195 | 196 | log.Println("etcdb: advertise client URLs", advertiseClientUrls.String()) 197 | 198 | listenErr := make(chan error) 199 | 200 | for _, u := range *listenClientUrls { 201 | go func(u url.URL) { 202 | log.Println("etcdb: listening for client requests on", u.String()) 203 | listenErr <- http.ListenAndServe(u.Host, r) 204 | }(u) 205 | } 206 | 207 | if err := <-listenErr; err != nil { 208 | log.Fatalln(err) 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /backend/listener.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "time" 8 | 9 | "github.com/rancher/etcdb/models" 10 | ) 11 | 12 | // A ChangeWatcher monitors the store's changes table to serve watch results 13 | type ChangeWatcher struct { 14 | store *SqlBackend 15 | changes *changeList 16 | watch chan *watch 17 | watches map[*watch]struct{} 18 | refreshPeriod time.Duration 19 | lastIndex int64 20 | stop chan struct{} 21 | } 22 | 23 | // Watch creates and starts a new ChangeWatcher for the SqlBackend 24 | func Watch(store *SqlBackend, refreshPeriod time.Duration) *ChangeWatcher { 25 | cw := &ChangeWatcher{ 26 | store: store, 27 | watch: make(chan *watch), 28 | refreshPeriod: refreshPeriod, 29 | stop: make(chan struct{}), 30 | watches: make(map[*watch]struct{}), 31 | changes: newChangeList(MaxChanges), 32 | } 33 | go cw.Run() 34 | return cw 35 | } 36 | 37 | // Stop stops the ChangeWatcher's Run loop 38 | func (cw *ChangeWatcher) Stop() { 39 | close(cw.stop) 40 | } 41 | 42 | // NextChange waits for a matching change event, and returns an ActionUpdate 43 | // with the change data 44 | func (cw *ChangeWatcher) NextChange(key string, recursive bool, index int64) (*models.ActionUpdate, error) { 45 | w := NewWatch(index, key, recursive) 46 | cw.watch <- w 47 | return w.Result() 48 | } 49 | 50 | // Run starts the event loop to poll for changes, and receive new watch requests 51 | func (cw *ChangeWatcher) Run() { 52 | cw.refresh() 53 | 54 | refresh := time.NewTicker(cw.refreshPeriod) 55 | 56 | for { 57 | select { 58 | case <-cw.stop: 59 | refresh.Stop() 60 | return 61 | case w := <-cw.watch: 62 | cw.addWatch(w) 63 | case <-refresh.C: 64 | cw.refresh() 65 | } 66 | } 67 | } 68 | 69 | func (cw *ChangeWatcher) addWatch(w *watch) { 70 | cw.watches[w] = struct{}{} 71 | 72 | if w.Index <= 0 || cw.changes.Size == 0 { 73 | return 74 | } 75 | 76 | if oldestIndex := cw.changes.First().Index; w.Index < oldestIndex { 77 | w.SetResult(nil, models.EventIndexCleared(oldestIndex, w.Index, cw.lastIndex)) 78 | delete(cw.watches, w) 79 | return 80 | } 81 | 82 | for i := 0; i < cw.changes.Size; i++ { 83 | c := cw.changes.Item(i) 84 | if cw.checkChange(c, w) { 85 | break 86 | } 87 | } 88 | } 89 | 90 | func (cw *ChangeWatcher) checkChange(c *change, w *watch) bool { 91 | if !w.Match(c) { 92 | return false 93 | } 94 | 95 | action, err := c.Value(cw.store) 96 | if err == ErrChangeIndexCleared { 97 | // if this change was already cleared, but watch didn't specify an index, 98 | // just return to wait for the next matching change 99 | if w.Index == 0 { 100 | return false 101 | } 102 | err = models.EventIndexCleared(c.Index+1, w.Index, cw.lastIndex) 103 | } 104 | w.SetResult(action, err) 105 | delete(cw.watches, w) 106 | 107 | return true 108 | } 109 | 110 | func (cw *ChangeWatcher) refresh() { 111 | newCount, err := cw.fetchSince(cw.lastIndex) 112 | if err != nil { 113 | log.Println("error refreshing:", err) 114 | // don't return since we still want to process any changes we did get 115 | } 116 | if newCount == 0 { 117 | return 118 | } 119 | 120 | cw.lastIndex = cw.changes.Last().Index 121 | 122 | i := 0 123 | if newCount < cw.changes.Size { 124 | i = cw.changes.Size - newCount 125 | } 126 | 127 | for ; i < cw.changes.Size; i++ { 128 | c := cw.changes.Item(i) 129 | for w := range cw.watches { 130 | cw.checkChange(c, w) 131 | } 132 | } 133 | } 134 | 135 | func (cw *ChangeWatcher) fetchSince(lastIndex int64) (count int, err error) { 136 | // store.Begin() makes sure expired nodes are updated, even though we don't 137 | // really need a new transaction for this one read query 138 | tx, err := cw.store.Begin() 139 | if err != nil { 140 | return 0, err 141 | } 142 | defer tx.Rollback() 143 | 144 | rows, err := cw.store.Query().Extend(` 145 | SELECT "index", "key", "action", "prev_node_modified" FROM "changes" 146 | WHERE "index" > `, lastIndex, ` 147 | ORDER BY "index"`).Query(tx) 148 | 149 | if err != nil { 150 | return 0, err 151 | } 152 | defer rows.Close() 153 | 154 | for rows.Next() { 155 | c := cw.changes.Next() 156 | err = rows.Scan(&c.Index, &c.Key, &c.Action, &c.PrevNodeModified) 157 | if err != nil { 158 | // remove the change w/ the error, but return count of successfully 159 | // added changes 160 | cw.changes.Pop() 161 | return count, err 162 | } 163 | count++ 164 | } 165 | 166 | return count, nil 167 | } 168 | 169 | // changeList is a simple circular buffer for storing the changes. 170 | // Old changes will automatically be overwritten when the buffer is full. 171 | type changeList struct { 172 | changes []change 173 | Capacity int 174 | Begin int 175 | Size int 176 | } 177 | 178 | func newChangeList(capacity int) *changeList { 179 | return &changeList{Capacity: capacity, changes: make([]change, capacity)} 180 | } 181 | 182 | func (cl *changeList) Item(i int) *change { 183 | return &cl.changes[(cl.Begin+i)%cl.Capacity] 184 | } 185 | 186 | func (cl *changeList) First() *change { 187 | return cl.Item(0) 188 | } 189 | 190 | func (cl *changeList) Last() *change { 191 | return cl.Item(cl.Size - 1) 192 | } 193 | 194 | func (cl *changeList) Pop() { 195 | if cl.Size == 0 { 196 | return 197 | } 198 | cl.Size-- 199 | } 200 | 201 | // Next moves the last position forward by one and returns the new last item. 202 | // If the buffer is at capacity, the first item is dropped and cleared to be 203 | // reused. 204 | func (cl *changeList) Next() *change { 205 | if cl.Size == cl.Capacity { 206 | cl.First().Clear() 207 | cl.Begin = (cl.Begin + 1) % cl.Capacity 208 | } else { 209 | cl.Size++ 210 | } 211 | return cl.Last() 212 | } 213 | 214 | // ErrChangeIndexCleared is returned by change.Value() when one of the nodes 215 | // referenced by the change has been cleared from the nodes table. 216 | var ErrChangeIndexCleared = errors.New("one of the nodes for this change has been cleared") 217 | 218 | type change struct { 219 | Index int64 220 | Key string 221 | Action string 222 | PrevNodeModified *int64 223 | value *models.ActionUpdate 224 | } 225 | 226 | // Clear resets the value pointer so that the change struct can be reused 227 | func (c *change) Clear() { 228 | c.value = nil 229 | } 230 | 231 | // Value fetches the node values for the changes, and returns an ActionUpdate 232 | // The result is memoized after the first call. 233 | func (c *change) Value(store *SqlBackend) (*models.ActionUpdate, error) { 234 | if c.value == nil { 235 | isDeleteAction := false 236 | switch c.Action { 237 | case "delete", "compareAndDelete", "expire": 238 | isDeleteAction = true 239 | } 240 | 241 | if isDeleteAction && c.PrevNodeModified == nil { 242 | return nil, fmt.Errorf("action type %s should have prev_node_modified set", c.Action) 243 | } 244 | 245 | q := store.queryNodeWithDeleted().Extend(` WHERE "key" = `, c.Key, ` AND "modified" IN (`) 246 | if isDeleteAction { 247 | q.Param(c.PrevNodeModified) 248 | } else { 249 | q.Param(c.Index) 250 | if c.PrevNodeModified != nil { 251 | q.Extend(`, `, c.PrevNodeModified) 252 | } 253 | } 254 | q.Text(`)`) 255 | 256 | rows, err := q.Query(store.db) 257 | if err != nil { 258 | return nil, err 259 | } 260 | defer rows.Close() 261 | 262 | nodes := make(map[int64]*models.Node) 263 | 264 | for rows.Next() { 265 | node, err := scanNode(rows) 266 | if err != nil { 267 | return nil, err 268 | } 269 | nodes[node.ModifiedIndex] = node 270 | } 271 | 272 | action := models.ActionUpdate{Action: c.Action} 273 | 274 | if c.PrevNodeModified != nil { 275 | prevNode, ok := nodes[*c.PrevNodeModified] 276 | if !ok { 277 | return nil, ErrChangeIndexCleared 278 | } 279 | action.PrevNode = prevNode 280 | } 281 | 282 | if isDeleteAction { 283 | action.Node.Key = c.Key 284 | action.Node.CreatedIndex = action.PrevNode.CreatedIndex 285 | action.Node.ModifiedIndex = c.Index 286 | } else { 287 | node, ok := nodes[c.Index] 288 | if !ok { 289 | return nil, ErrChangeIndexCleared 290 | } 291 | action.Node = *node 292 | } 293 | 294 | c.value = &action 295 | } 296 | 297 | return c.value, nil 298 | } 299 | 300 | type watchResult struct { 301 | Action *models.ActionUpdate 302 | Err error 303 | } 304 | 305 | type watch struct { 306 | Index int64 307 | Key string 308 | Recursive bool 309 | result chan watchResult 310 | } 311 | 312 | func NewWatch(index int64, key string, recursive bool) *watch { 313 | return &watch{index, key, recursive, make(chan watchResult, 1)} 314 | } 315 | 316 | func (w *watch) SetResult(action *models.ActionUpdate, err error) { 317 | select { 318 | case w.result <- watchResult{action, err}: 319 | default: 320 | // drop duplicate results 321 | } 322 | } 323 | 324 | func (w *watch) Result() (*models.ActionUpdate, error) { 325 | res := <-w.result 326 | return res.Action, res.Err 327 | } 328 | 329 | func (w *watch) Match(c *change) bool { 330 | if c.Index < w.Index { 331 | return false 332 | } 333 | if c.Key == w.Key { 334 | return true 335 | } 336 | if w.Recursive && isParent(w.Key, c.Key) { 337 | return true 338 | } 339 | switch c.Action { 340 | case "delete", "expire": 341 | return isParent(c.Key, w.Key) 342 | } 343 | return false 344 | } 345 | 346 | func isParent(a, b string) bool { 347 | return b[:len(a)+1] == a+"/" 348 | } 349 | -------------------------------------------------------------------------------- /backend/sql.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | "strings" 8 | 9 | "github.com/go-sql-driver/mysql" 10 | "github.com/rancher/etcdb/models" 11 | ) 12 | 13 | // MaxChanges is the maximum rows to keep in the changes table, and the 14 | // corresponding previous versions of modified or deleted nodes. 15 | const MaxChanges = 1000 16 | 17 | // SqlBackend SQL implementation 18 | type SqlBackend struct { 19 | db *sql.DB 20 | dialect dbDialect 21 | } 22 | 23 | // New creates a SqlBackend for the DB 24 | func New(driver, dataSource string) (*SqlBackend, error) { 25 | var dialect dbDialect 26 | switch driver { 27 | case "mysql": 28 | dialect = mysqlDialect{} 29 | case "postgres": 30 | dialect = postgresDialect{} 31 | default: 32 | return nil, fmt.Errorf("Unrecognized database driver %s, should be 'mysql' or 'postgres'", driver) 33 | } 34 | 35 | db, err := dialect.Open(driver, dataSource) 36 | if err != nil { 37 | return nil, err 38 | } 39 | backend := &SqlBackend{db, dialect} 40 | return backend, nil 41 | } 42 | 43 | func (b *SqlBackend) Close() error { 44 | return b.db.Close() 45 | } 46 | 47 | func (b *SqlBackend) runQueries(queries ...string) error { 48 | for _, q := range queries { 49 | _, err := b.db.Exec(q) 50 | if err != nil { 51 | log.Printf("err: %s -- %T %s", err, err, q) 52 | return err 53 | } 54 | } 55 | 56 | return nil 57 | } 58 | 59 | func (b *SqlBackend) dropSchema() error { 60 | return b.runQueries( 61 | `DROP TABLE IF EXISTS "nodes"`, 62 | `DROP TABLE IF EXISTS "index"`, 63 | `DROP TABLE IF EXISTS "changes"`, 64 | ) 65 | } 66 | 67 | // CreateSchema creates the DB schema 68 | func (b *SqlBackend) CreateSchema() error { 69 | queries := b.dialect.tableDefinitions() 70 | queries = append(queries, `INSERT INTO "index" ("index") VALUES (0)`) 71 | return b.runQueries(queries...) 72 | } 73 | 74 | func (b *SqlBackend) Query() *Query { 75 | return &Query{dialect: b.dialect} 76 | } 77 | 78 | func (b *SqlBackend) Begin() (tx *sql.Tx, err error) { 79 | err = b.purgeExpired() 80 | if err != nil { 81 | log.Println("error expiring:", err) 82 | return 83 | } 84 | 85 | return b.db.Begin() 86 | } 87 | 88 | func (b *SqlBackend) purgeExpired() (err error) { 89 | tx, err := b.db.Begin() 90 | if err != nil { 91 | return err 92 | } 93 | defer func() { 94 | if err == nil { 95 | err = tx.Commit() 96 | } else { 97 | tx.Rollback() 98 | } 99 | if err == sql.ErrNoRows { 100 | err = nil 101 | } 102 | }() 103 | 104 | index, err := b.incrementIndex(tx) 105 | if err != nil { 106 | return 107 | } 108 | 109 | rows, err := tx.Query(`SELECT "key", "modified" FROM "nodes" 110 | WHERE "deleted" = 0 AND "expiration" < ` + b.dialect.now() + ` 111 | ORDER BY "expiration"`) 112 | if err != nil { 113 | return 114 | } 115 | defer rows.Close() 116 | 117 | var nodes []*models.Node 118 | 119 | for rows.Next() { 120 | var node models.Node 121 | err = rows.Scan(&node.Key, &node.ModifiedIndex) 122 | if err != nil { 123 | return err 124 | } 125 | nodes = append(nodes, &node) 126 | } 127 | 128 | if len(nodes) == 0 { 129 | return sql.ErrNoRows 130 | } 131 | 132 | expirationIndex := index 133 | 134 | for _, node := range nodes { 135 | err = b.recordChange(tx, expirationIndex, "expire", node.Key, node) 136 | if err != nil { 137 | return err 138 | } 139 | 140 | query := b.Query().Extend(`UPDATE nodes SET deleted = `, expirationIndex, 141 | ` WHERE deleted = 0 AND ("key" = `, node.Key, ` OR "key" LIKE `, node.Key+"/%", `)`) 142 | _, err = query.Exec(tx) 143 | if err != nil { 144 | return err 145 | } 146 | 147 | expirationIndex++ 148 | } 149 | 150 | // undo last increment to match the final index value used 151 | expirationIndex-- 152 | 153 | _, err = b.Query().Extend(`UPDATE "index" SET "index" = `, expirationIndex).Exec(tx) 154 | 155 | return err 156 | } 157 | 158 | // Get returns a node for the key 159 | func (b *SqlBackend) Get(key string, recursive bool) (node *models.Node, err error) { 160 | tx, err := b.Begin() 161 | if err != nil { 162 | return nil, err 163 | } 164 | defer func() { 165 | if err == nil { 166 | err = tx.Commit() 167 | } else { 168 | tx.Rollback() 169 | } 170 | }() 171 | 172 | query := b.queryNode() 173 | if key == "/" { 174 | if !recursive { 175 | query.Text(` AND path_depth = 1`) 176 | } 177 | } else { 178 | query.Extend(` AND ("key" = `, key, ` OR ("key" LIKE `, key+"/%") 179 | if !recursive { 180 | query.Extend(" AND path_depth = ", pathDepth(key)+1) 181 | } 182 | query.Text("))") 183 | } 184 | rows, err := query.Query(tx) 185 | if err != nil { 186 | return nil, err 187 | } 188 | defer rows.Close() 189 | 190 | nodes := make(map[string]*models.Node) 191 | 192 | for rows.Next() { 193 | node, err := scanNode(rows) 194 | if err != nil { 195 | return nil, err 196 | } 197 | nodes[node.Key] = node 198 | } 199 | 200 | if key == "/" { 201 | nodes["/"] = &models.Node{Dir: true} 202 | } 203 | 204 | if _, ok := nodes[key]; !ok { 205 | currIndex, err := b.currIndex(tx) 206 | if err != nil { 207 | return nil, err 208 | } 209 | return nil, models.NotFound(key, currIndex) 210 | } 211 | 212 | for _, node := range nodes { 213 | if node.Key == key || node.Key == "" { 214 | // don't need to compute parent of the requested key, or root key 215 | continue 216 | } 217 | parent := nodes[splitKey(node.Key)] 218 | parent.Nodes = append(parent.Nodes, node) 219 | } 220 | 221 | return nodes[key], nil 222 | } 223 | 224 | type scannable interface { 225 | Scan(...interface{}) error 226 | } 227 | 228 | func scanNode(scanner scannable) (*models.Node, error) { 229 | var node models.Node 230 | // mysql.NullTime is more portable and works with the Postgres driver 231 | var expiration mysql.NullTime 232 | err := scanner.Scan(&node.Key, &node.CreatedIndex, &node.ModifiedIndex, 233 | &node.Value, &node.Dir, &expiration, &node.TTL) 234 | if err != nil { 235 | return nil, err 236 | } 237 | if expiration.Valid { 238 | node.Expiration = &expiration.Time 239 | } 240 | return &node, nil 241 | } 242 | 243 | func (b *SqlBackend) queryNodeWithDeleted() *Query { 244 | return b.Query().Text(` 245 | SELECT "key", "created", "modified", "value", "dir", "expiration", 246 | `).Text(b.dialect.ttl()).Text(` 247 | FROM "nodes"`) 248 | } 249 | 250 | func (b *SqlBackend) queryNode() *Query { 251 | return b.queryNodeWithDeleted().Text(` WHERE "deleted" = 0`) 252 | } 253 | 254 | func (b *SqlBackend) getOne(tx *sql.Tx, key string) (*models.Node, error) { 255 | node, err := scanNode(b.queryNode().Extend(` AND "key" = `, key).QueryRow(tx)) 256 | if err == sql.ErrNoRows { 257 | return nil, nil 258 | } 259 | return node, err 260 | } 261 | 262 | // Set sets the value for a key 263 | func (b *SqlBackend) Set(key, value string, condition SetCondition) (*models.Node, *models.Node, error) { 264 | return b.set(key, value, false, nil, condition) 265 | } 266 | 267 | func (b *SqlBackend) SetTTL(key, value string, ttl int64, condition SetCondition) (*models.Node, *models.Node, error) { 268 | return b.set(key, value, false, &ttl, condition) 269 | } 270 | 271 | func (b *SqlBackend) MkDir(key string, ttl *int64, condition SetCondition) (*models.Node, *models.Node, error) { 272 | return b.set(key, "", true, ttl, condition) 273 | } 274 | 275 | func (b *SqlBackend) readOnlyError() error { 276 | index, err := b.currIndex(b.db) 277 | if err != nil { 278 | return err 279 | } 280 | return models.RootReadOnly(index) 281 | } 282 | 283 | func (b *SqlBackend) set(key, value string, dir bool, ttl *int64, condition SetCondition) (node *models.Node, prevNode *models.Node, err error) { 284 | if key == "/" { 285 | return nil, nil, b.readOnlyError() 286 | } 287 | 288 | tx, err := b.Begin() 289 | if err != nil { 290 | return nil, nil, err 291 | } 292 | defer func() { 293 | if err == nil { 294 | err = tx.Commit() 295 | } else { 296 | tx.Rollback() 297 | } 298 | }() 299 | 300 | index, err := b.incrementIndex(tx) 301 | if err != nil { 302 | return nil, nil, err 303 | } 304 | 305 | prevNode, err = b.getOne(tx, key) 306 | if err != nil { 307 | return nil, nil, err 308 | } 309 | 310 | prevIndex := index - 1 311 | 312 | if err := condition.Check(key, prevIndex, prevNode); err != nil { 313 | return nil, nil, err 314 | } 315 | 316 | if prevNode != nil && prevNode.Dir { 317 | return nil, nil, models.NotAFile(key, prevIndex) 318 | } 319 | 320 | err = b.mkdirs(tx, splitKey(key), index) 321 | if err != nil { 322 | return nil, nil, err 323 | } 324 | 325 | if prevNode != nil { 326 | _, err = b.Query().Extend( 327 | `UPDATE nodes SET "deleted" = `, index, 328 | ` WHERE "deleted" = 0 AND "key" = `, key, 329 | ).Exec(tx) 330 | if err != nil { 331 | return nil, nil, err 332 | } 333 | } 334 | 335 | _, err = b.insertQuery(key, value, dir, index, ttl).Exec(tx) 336 | if err != nil { 337 | return nil, nil, err 338 | } 339 | 340 | node, err = b.getOne(tx, key) 341 | if err != nil { 342 | return nil, nil, err 343 | } 344 | 345 | err = b.recordChange(tx, index, condition.SetActionName(), key, prevNode) 346 | if err != nil { 347 | return nil, nil, err 348 | } 349 | 350 | return node, prevNode, nil 351 | } 352 | 353 | func (b *SqlBackend) recordChange(db Querier, index int64, action, key string, prevNode *models.Node) (err error) { 354 | query := b.Query().Extend(`INSERT INTO changes 355 | ("index", "key", "action", "prev_node_modified") VALUES (`, 356 | index, `, `, key, `,`, action) 357 | if prevNode == nil { 358 | query.Text(`, null)`) 359 | } else { 360 | query.Extend(`, `, prevNode.ModifiedIndex, `)`) 361 | } 362 | _, err = query.Exec(db) 363 | if err != nil { 364 | return 365 | } 366 | 367 | _, err = b.Query().Extend(`DELETE FROM changes WHERE "index" < `, index-MaxChanges).Exec(db) 368 | if err != nil { 369 | return 370 | } 371 | 372 | _, err = b.Query().Extend(`DELETE FROM "nodes" WHERE "deleted" > 0 AND "deleted" < `, index-MaxChanges).Exec(db) 373 | return 374 | } 375 | 376 | func (b *SqlBackend) insertQuery(key, value string, dir bool, index int64, ttl *int64) *Query { 377 | pathDepth := pathDepth(key) 378 | query := b.Query() 379 | query.Text(`INSERT INTO nodes ("key", "value", "dir", "created", "modified", "path_depth"`) 380 | if ttl != nil { 381 | query.Text(`, expiration`) 382 | } 383 | query.Extend(`) VALUES (`, 384 | key, `, `, value, `, `, dir, `, `, index, `, `, index, `, `, pathDepth, 385 | ) 386 | if ttl != nil { 387 | query.Text(`, `) 388 | b.dialect.expiration(query, *ttl) 389 | } 390 | query.Text(")") 391 | return query 392 | } 393 | 394 | func (b *SqlBackend) mkdirs(tx *sql.Tx, path string, index int64) error { 395 | pathDepth := pathDepth(path) 396 | for ; path != "/" && path != ""; path = splitKey(path) { 397 | _, err := tx.Exec("SAVEPOINT mkdirs") 398 | if err != nil { 399 | return err 400 | } 401 | _, err = b.Query().Extend(` 402 | INSERT INTO nodes ("key", "dir", "created", "modified", "path_depth") 403 | VALUES (`, path, `, true, `, index, `, `, index, `, `, pathDepth, `) 404 | `).Exec(tx) 405 | if err != nil { 406 | tx.Exec("ROLLBACK TO SAVEPOINT mkdirs") 407 | } 408 | if b.dialect.isDuplicateKeyError(err) { 409 | var existingIsDir bool 410 | err := b.Query().Extend(`SELECT dir FROM nodes WHERE "deleted" = 0 AND "key" = `, path).QueryRow(tx).Scan(&existingIsDir) 411 | if err != nil { 412 | return err 413 | } 414 | if !existingIsDir { 415 | // FIXME should this be previous index before the update? 416 | return models.NotADirectory(path, index) 417 | } 418 | return nil 419 | } 420 | if err != nil { 421 | return err 422 | } 423 | pathDepth-- 424 | } 425 | return nil 426 | } 427 | 428 | func (b *SqlBackend) CreateInOrder(key, value string, ttl *int64) (node *models.Node, err error) { 429 | tx, err := b.Begin() 430 | if err != nil { 431 | return nil, err 432 | } 433 | defer func() { 434 | if err == nil { 435 | err = tx.Commit() 436 | } else { 437 | tx.Rollback() 438 | } 439 | }() 440 | 441 | index, err := b.incrementIndex(tx) 442 | if err != nil { 443 | return nil, err 444 | } 445 | 446 | key = fmt.Sprintf("%s/%d", key, index) 447 | 448 | _, err = b.insertQuery(key, value, false, index, ttl).Exec(tx) 449 | if err != nil { 450 | return nil, err 451 | } 452 | 453 | node, err = b.getOne(tx, key) 454 | if err != nil { 455 | return nil, err 456 | } 457 | 458 | err = b.recordChange(tx, index, "create", key, nil) 459 | if err != nil { 460 | return nil, err 461 | } 462 | 463 | return node, nil 464 | } 465 | 466 | // Delete removes the key 467 | func (b *SqlBackend) Delete(key string, condition DeleteCondition) (node *models.Node, index int64, err error) { 468 | if key == "/" { 469 | return nil, 0, b.readOnlyError() 470 | } 471 | 472 | tx, err := b.Begin() 473 | if err != nil { 474 | return nil, 0, err 475 | } 476 | defer func() { 477 | if err == nil { 478 | err = tx.Commit() 479 | } else { 480 | tx.Rollback() 481 | } 482 | }() 483 | 484 | index, err = b.incrementIndex(tx) 485 | if err != nil { 486 | return nil, 0, err 487 | } 488 | 489 | node, err = b.getOne(tx, key) 490 | if err != nil { 491 | return nil, 0, err 492 | } 493 | 494 | prevIndex := index - 1 495 | 496 | if node == nil { 497 | return nil, 0, models.NotFound(key, prevIndex) 498 | } 499 | if node.Dir { 500 | return nil, 0, models.NotAFile(key, prevIndex) 501 | } 502 | 503 | if err := condition.Check(key, prevIndex, node); err != nil { 504 | return nil, 0, err 505 | } 506 | 507 | _, err = b.Query().Extend(` 508 | UPDATE "nodes" SET "deleted" = `, index, 509 | ` WHERE "key" = `, key, ` AND "deleted" = 0`).Exec(tx) 510 | if err != nil { 511 | return nil, 0, err 512 | } 513 | 514 | err = b.recordChange(tx, index, condition.DeleteActionName(), key, node) 515 | if err != nil { 516 | return nil, 0, err 517 | } 518 | 519 | return node, index, nil 520 | } 521 | 522 | // RmDir removes the key for directories 523 | func (b *SqlBackend) RmDir(key string, recursive bool, condition DeleteCondition) (node *models.Node, index int64, err error) { 524 | if key == "/" { 525 | return nil, 0, b.readOnlyError() 526 | } 527 | 528 | tx, err := b.Begin() 529 | if err != nil { 530 | return nil, 0, err 531 | } 532 | defer func() { 533 | if err == nil { 534 | err = tx.Commit() 535 | } else { 536 | tx.Rollback() 537 | } 538 | }() 539 | 540 | index, err = b.incrementIndex(tx) 541 | if err != nil { 542 | return nil, 0, err 543 | } 544 | 545 | // use the previous index in any errors 546 | prevIndex := index - 1 547 | 548 | node, err = b.getOne(tx, key) 549 | if err != nil { 550 | return nil, 0, err 551 | } 552 | 553 | if node == nil { 554 | return nil, 0, models.NotFound(key, prevIndex) 555 | } 556 | 557 | if err := condition.Check(key, prevIndex, node); err != nil { 558 | return nil, 0, err 559 | } 560 | 561 | query := b.Query().Extend(` 562 | UPDATE nodes SET deleted = `, index, 563 | ` WHERE deleted = 0 AND ("key" = `, key, ` OR "key" LIKE `, key+"/%", `)`) 564 | res, err := query.Exec(tx) 565 | if err != nil { 566 | return nil, 0, err 567 | } 568 | 569 | if !recursive { 570 | rowsDeleted, err := res.RowsAffected() 571 | if err != nil { 572 | return nil, 0, err 573 | } 574 | if rowsDeleted > 1 { 575 | return nil, 0, models.DirectoryNotEmpty(key, prevIndex) 576 | } 577 | } 578 | 579 | err = b.recordChange(tx, index, condition.DeleteActionName(), key, node) 580 | if err != nil { 581 | return nil, 0, err 582 | } 583 | 584 | return node, index, nil 585 | } 586 | 587 | func splitKey(key string) string { 588 | i := len(key) - 1 589 | for i >= 0 && key[i] != '/' { 590 | i-- 591 | } 592 | if i < 0 { 593 | return "" 594 | } 595 | if i == 0 { 596 | return "/" 597 | } 598 | return key[:i] 599 | } 600 | 601 | func (b *SqlBackend) currIndex(db Querier) (index int64, err error) { 602 | err = db.QueryRow(`SELECT "index" FROM "index"`).Scan(&index) 603 | return 604 | } 605 | 606 | func (b *SqlBackend) incrementIndex(db Querier) (index int64, err error) { 607 | return b.dialect.incrementIndex(db) 608 | } 609 | 610 | func pathDepth(key string) int { 611 | if key == "/" { 612 | return 0 613 | } 614 | return strings.Count(key, "/") 615 | } 616 | -------------------------------------------------------------------------------- /backend/sql_test.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | "reflect" 9 | "runtime" 10 | "testing" 11 | "time" 12 | 13 | "github.com/rancher/etcdb/models" 14 | ) 15 | 16 | func TestMain(m *testing.M) { 17 | dbDriver = "postgres" 18 | dbDataSource = "sslmode=disable database=etcd_test" 19 | log.Println("Running PostgreSQL tests") 20 | postgresResult := m.Run() 21 | 22 | dbDriver = "mysql" 23 | dbDataSource = "root@/etcd_test" 24 | log.Println("Running MySQL tests") 25 | mysqlResult := m.Run() 26 | 27 | os.Exit(mysqlResult | postgresResult) 28 | } 29 | 30 | var dbDriver, dbDataSource string 31 | 32 | func testConn(t *testing.T) *SqlBackend { 33 | store, err := New(dbDriver, dbDataSource) 34 | ok(t, err) 35 | err = store.dropSchema() 36 | ok(t, err) 37 | err = store.CreateSchema() 38 | ok(t, err) 39 | 40 | return store 41 | } 42 | 43 | func currIndex(store *SqlBackend) int64 { 44 | index, _ := store.currIndex(store.db) 45 | return index 46 | } 47 | 48 | func TestGetMissingReturnsNotFound(t *testing.T) { 49 | store := testConn(t) 50 | defer store.Close() 51 | 52 | _, err := store.Get("/foo", false) 53 | expectError(t, "Key not found", "/foo", err) 54 | } 55 | 56 | func Test_Get_NotFoundErrorIncludesIndex(t *testing.T) { 57 | store := testConn(t) 58 | defer store.Close() 59 | 60 | _, err := store.Get("/bar", false) 61 | expectError(t, "Key not found", "/bar", err) 62 | equals(t, currIndex(store), err.(models.Error).Index) 63 | } 64 | 65 | func Test_Get_RootEmpty(t *testing.T) { 66 | store := testConn(t) 67 | defer store.Close() 68 | 69 | node, err := store.Get("/", false) 70 | ok(t, err) 71 | 72 | // etcd omits the key for the root node 73 | equals(t, "", node.Key) 74 | equals(t, true, node.Dir) 75 | equals(t, 0, len(node.Nodes)) 76 | } 77 | 78 | func Test_Get_RootChildren(t *testing.T) { 79 | store := testConn(t) 80 | defer store.Close() 81 | 82 | _, _, err := store.Set("/foo", "bar", Always) 83 | ok(t, err) 84 | 85 | node, err := store.Get("/", false) 86 | ok(t, err) 87 | 88 | // etcd omits the key for the root node 89 | equals(t, "", node.Key) 90 | equals(t, true, node.Dir) 91 | equals(t, 1, len(node.Nodes)) 92 | 93 | child := node.Nodes[0] 94 | equals(t, "/foo", child.Key) 95 | equals(t, "bar", child.Value) 96 | } 97 | 98 | func Test_Set_RootReadOnly(t *testing.T) { 99 | store := testConn(t) 100 | defer store.Close() 101 | 102 | _, _, err := store.Set("/", "bar", Always) 103 | expectError(t, "Root is read only", "/", err) 104 | } 105 | 106 | func Test_SetTTL_RootReadOnly(t *testing.T) { 107 | store := testConn(t) 108 | defer store.Close() 109 | 110 | _, _, err := store.SetTTL("/", "bar", 100, Always) 111 | expectError(t, "Root is read only", "/", err) 112 | } 113 | 114 | func Test_MkDir_RootReadOnly(t *testing.T) { 115 | store := testConn(t) 116 | defer store.Close() 117 | 118 | _, _, err := store.MkDir("/", nil, Always) 119 | expectError(t, "Root is read only", "/", err) 120 | } 121 | 122 | func Test_Delete_RootReadOnly(t *testing.T) { 123 | store := testConn(t) 124 | defer store.Close() 125 | 126 | _, _, err := store.Delete("/", Always) 127 | expectError(t, "Root is read only", "/", err) 128 | } 129 | 130 | func Test_RmDir_RootReadOnly_NonRecursive(t *testing.T) { 131 | store := testConn(t) 132 | defer store.Close() 133 | 134 | _, _, err := store.RmDir("/", false, Always) 135 | expectError(t, "Root is read only", "/", err) 136 | } 137 | 138 | func Test_RmDir_RootReadOnly_Recursive(t *testing.T) { 139 | store := testConn(t) 140 | defer store.Close() 141 | 142 | _, _, err := store.RmDir("/", true, Always) 143 | expectError(t, "Root is read only", "/", err) 144 | } 145 | 146 | func TestSet(t *testing.T) { 147 | store := testConn(t) 148 | defer store.Close() 149 | 150 | node, prevNode, err := store.Set("/foo", "bar", Always) 151 | ok(t, err) 152 | 153 | if node == nil { 154 | fatalf(t, "node should not be nil") 155 | } 156 | 157 | if prevNode != nil { 158 | fatalf(t, "setting new node should return nil for prevNode, but got: %#v", prevNode) 159 | } 160 | 161 | equals(t, "/foo", node.Key) 162 | equals(t, "bar", node.Value) 163 | } 164 | 165 | func TestSetThenGet(t *testing.T) { 166 | store := testConn(t) 167 | defer store.Close() 168 | 169 | _, prevNode, err := store.Set("/foo", "bar", Always) 170 | ok(t, err) 171 | 172 | if prevNode != nil { 173 | fatalf(t, "setting new node should return nil for prevNode, but got: %#v", prevNode) 174 | } 175 | 176 | node, err := store.Get("/foo", false) 177 | ok(t, err) 178 | 179 | equals(t, "/foo", node.Key) 180 | equals(t, "bar", node.Value) 181 | } 182 | 183 | func TestFullCycle(t *testing.T) { 184 | store := testConn(t) 185 | defer store.Close() 186 | 187 | node, err := store.Get("/foo", false) 188 | 189 | if node != nil { 190 | fatalf(t, "node should be nil before set, but got: %#v", node) 191 | } 192 | 193 | _, prevNode, err := store.Set("/foo", "bar", Always) 194 | ok(t, err) 195 | 196 | if prevNode != nil { 197 | fatalf(t, "setting new node should return nil for prevNode, but got: %#v", prevNode) 198 | } 199 | 200 | node, err = store.Get("/foo", false) 201 | 202 | equals(t, "/foo", node.Key) 203 | equals(t, "bar", node.Value) 204 | 205 | prevNode, _, err = store.Delete("/foo", Always) 206 | ok(t, err) 207 | 208 | equals(t, "/foo", prevNode.Key) 209 | equals(t, "bar", prevNode.Value) 210 | 211 | node, err = store.Get("/foo", false) 212 | 213 | if node != nil { 214 | fatalf(t, "node should be nil after deleting") 215 | } 216 | } 217 | 218 | func TestSet_ErrorIndex(t *testing.T) { 219 | store := testConn(t) 220 | defer store.Close() 221 | 222 | origIndex, err := store.incrementIndex(store.db) 223 | ok(t, err) 224 | 225 | // ensure the index update is persisted 226 | equals(t, origIndex, currIndex(store)) 227 | 228 | _, _, err = store.Set("/foo", "updated", PrevExist(true)) 229 | expectError(t, "Key not found", "/foo", err) 230 | 231 | // the index should not be updated by the failed set 232 | equals(t, origIndex, currIndex(store)) 233 | 234 | // the error should contain the current index 235 | equals(t, origIndex, err.(models.Error).Index) 236 | } 237 | 238 | func TestSet_PrevExist_True_Success(t *testing.T) { 239 | store := testConn(t) 240 | defer store.Close() 241 | 242 | _, _, err := store.Set("/foo", "original", Always) 243 | ok(t, err) 244 | 245 | node, prevNode, err := store.Set("/foo", "updated", PrevExist(true)) 246 | ok(t, err) 247 | 248 | equals(t, "/foo", node.Key) 249 | equals(t, "updated", node.Value) 250 | 251 | equals(t, "/foo", prevNode.Key) 252 | equals(t, "original", prevNode.Value) 253 | } 254 | 255 | func TestSet_PrevExist_True_Fail(t *testing.T) { 256 | store := testConn(t) 257 | defer store.Close() 258 | 259 | _, _, err := store.Set("/foo", "updated", PrevExist(true)) 260 | expectError(t, "Key not found", "/foo", err) 261 | } 262 | 263 | func TestSet_PrevExist_False_Success(t *testing.T) { 264 | store := testConn(t) 265 | defer store.Close() 266 | 267 | node, prevNode, err := store.Set("/foo", "bar", PrevExist(false)) 268 | ok(t, err) 269 | 270 | equals(t, "/foo", node.Key) 271 | equals(t, "bar", node.Value) 272 | 273 | if prevNode != nil { 274 | fatalf(t, "expected prevNode to be nil, but got: %#v", prevNode) 275 | } 276 | } 277 | 278 | func TestSet_PrevExist_False_Fail(t *testing.T) { 279 | store := testConn(t) 280 | defer store.Close() 281 | 282 | _, _, err := store.Set("/foo", "original", Always) 283 | ok(t, err) 284 | 285 | _, _, err = store.Set("/foo", "updated", PrevExist(false)) 286 | expectError(t, "Key already exists", "/foo", err) 287 | } 288 | 289 | func TestSet_PrevValue_Success(t *testing.T) { 290 | store := testConn(t) 291 | defer store.Close() 292 | 293 | _, _, err := store.Set("/foo", "original", Always) 294 | ok(t, err) 295 | 296 | node, prevNode, err := store.Set("/foo", "updated", PrevValue("original")) 297 | ok(t, err) 298 | 299 | equals(t, "/foo", node.Key) 300 | equals(t, "updated", node.Value) 301 | 302 | equals(t, "/foo", prevNode.Key) 303 | equals(t, "original", prevNode.Value) 304 | } 305 | 306 | func TestSet_PrevValue_Fail_Missing(t *testing.T) { 307 | store := testConn(t) 308 | defer store.Close() 309 | 310 | _, _, err := store.Set("/foo", "updated", PrevValue("does not exist")) 311 | expectError(t, "Key not found", "/foo", err) 312 | } 313 | 314 | func TestSet_PrevValue_Fail_ValueMismatch(t *testing.T) { 315 | store := testConn(t) 316 | defer store.Close() 317 | 318 | _, _, err := store.Set("/foo", "original", Always) 319 | ok(t, err) 320 | 321 | _, _, err = store.Set("/foo", "updated", PrevValue("different value")) 322 | expectError(t, "Compare failed", "[different value != original]", err) 323 | } 324 | 325 | func TestSet_PrevIndex_Success(t *testing.T) { 326 | store := testConn(t) 327 | defer store.Close() 328 | 329 | node, _, err := store.Set("/foo", "original", Always) 330 | ok(t, err) 331 | 332 | node, prevNode, err := store.Set("/foo", "updated", PrevIndex(node.ModifiedIndex)) 333 | ok(t, err) 334 | 335 | equals(t, "/foo", node.Key) 336 | equals(t, "updated", node.Value) 337 | 338 | equals(t, "/foo", prevNode.Key) 339 | equals(t, "original", prevNode.Value) 340 | } 341 | 342 | func TestSet_PrevIndex_Fail_Missing(t *testing.T) { 343 | store := testConn(t) 344 | defer store.Close() 345 | 346 | _, _, err := store.Set("/foo", "updated", PrevIndex(0)) 347 | expectError(t, "Key not found", "/foo", err) 348 | } 349 | 350 | func TestSet_PrevIndex_Fail_IndexMismatch(t *testing.T) { 351 | store := testConn(t) 352 | defer store.Close() 353 | 354 | _, _, err := store.Set("/foo", "original", Always) 355 | ok(t, err) 356 | 357 | _, _, err = store.Set("/foo", "updated", PrevIndex(100)) 358 | expectError(t, "Compare failed", "[100 != 1]", err) 359 | } 360 | 361 | func TestDelete_ErrorIndex(t *testing.T) { 362 | store := testConn(t) 363 | defer store.Close() 364 | 365 | origIndex, err := store.incrementIndex(store.db) 366 | ok(t, err) 367 | 368 | // ensure the index update is persisted 369 | equals(t, origIndex, currIndex(store)) 370 | 371 | _, _, err = store.Delete("/foo", Always) 372 | expectError(t, "Key not found", "/foo", err) 373 | 374 | // the index should not be updated by the failed delete 375 | equals(t, origIndex, currIndex(store)) 376 | 377 | // the error should contain the current index 378 | equals(t, origIndex, err.(models.Error).Index) 379 | } 380 | 381 | func TestDelete_PrevValue_Success(t *testing.T) { 382 | store := testConn(t) 383 | defer store.Close() 384 | 385 | _, _, err := store.Set("/foo", "original", Always) 386 | ok(t, err) 387 | 388 | prevNode, _, err := store.Delete("/foo", PrevValue("original")) 389 | ok(t, err) 390 | 391 | equals(t, "/foo", prevNode.Key) 392 | equals(t, "original", prevNode.Value) 393 | } 394 | 395 | func TestDelete_PrevValue_Fail_Missing(t *testing.T) { 396 | store := testConn(t) 397 | defer store.Close() 398 | 399 | _, _, err := store.Delete("/foo", PrevValue("does not exist")) 400 | expectError(t, "Key not found", "/foo", err) 401 | } 402 | 403 | func TestDelete_PrevValue_Fail_ValueMismatch(t *testing.T) { 404 | store := testConn(t) 405 | defer store.Close() 406 | 407 | _, _, err := store.Set("/foo", "original", Always) 408 | ok(t, err) 409 | 410 | _, _, err = store.Delete("/foo", PrevValue("different value")) 411 | expectError(t, "Compare failed", "[different value != original]", err) 412 | } 413 | 414 | func TestDelete_PrevIndex_Success(t *testing.T) { 415 | store := testConn(t) 416 | defer store.Close() 417 | 418 | node, _, err := store.Set("/foo", "original", Always) 419 | ok(t, err) 420 | 421 | prevNode, _, err := store.Delete("/foo", PrevIndex(node.ModifiedIndex)) 422 | ok(t, err) 423 | 424 | equals(t, "/foo", prevNode.Key) 425 | equals(t, "original", prevNode.Value) 426 | } 427 | 428 | func TestDelete_PrevIndex_Fail_Missing(t *testing.T) { 429 | store := testConn(t) 430 | defer store.Close() 431 | 432 | _, _, err := store.Delete("/foo", PrevIndex(0)) 433 | expectError(t, "Key not found", "/foo", err) 434 | } 435 | 436 | func TestDelete_PrevIndex_Fail_IndexMismatch(t *testing.T) { 437 | store := testConn(t) 438 | defer store.Close() 439 | 440 | _, _, err := store.Set("/foo", "original", Always) 441 | ok(t, err) 442 | 443 | _, _, err = store.Delete("/foo", PrevIndex(100)) 444 | expectError(t, "Compare failed", "[100 != 1]", err) 445 | } 446 | 447 | func Test_CreateDirectory_Simple(t *testing.T) { 448 | store := testConn(t) 449 | defer store.Close() 450 | 451 | _, _, err := store.MkDir("/foo", nil, Always) 452 | ok(t, err) 453 | 454 | node, err := store.Get("/foo", false) 455 | ok(t, err) 456 | 457 | equals(t, true, node.Dir) 458 | equals(t, 0, len(node.Nodes)) 459 | } 460 | 461 | func Test_CreateDirectory_ReplacesFile(t *testing.T) { 462 | store := testConn(t) 463 | defer store.Close() 464 | 465 | _, _, err := store.Set("/foo", "original", Always) 466 | ok(t, err) 467 | 468 | node, prevNode, err := store.MkDir("/foo", nil, Always) 469 | ok(t, err) 470 | 471 | equals(t, true, node.Dir) 472 | equals(t, false, prevNode.Dir) 473 | equals(t, "original", prevNode.Value) 474 | } 475 | 476 | func Test_CreateDirectory_DoesNotReplaceDir(t *testing.T) { 477 | store := testConn(t) 478 | defer store.Close() 479 | 480 | _, _, err := store.MkDir("/foo", nil, Always) 481 | ok(t, err) 482 | 483 | _, _, err = store.MkDir("/foo", nil, Always) 484 | expectError(t, "Not a file", "/foo", err) 485 | } 486 | 487 | func Test_CreateDirectory_IfNotExist(t *testing.T) { 488 | store := testConn(t) 489 | defer store.Close() 490 | 491 | _, _, err := store.MkDir("/foo", nil, Always) 492 | ok(t, err) 493 | 494 | _, _, err = store.MkDir("/foo", nil, PrevExist(false)) 495 | expectError(t, "Key already exists", "/foo", err) 496 | } 497 | 498 | func Test_Get_ListDirectory(t *testing.T) { 499 | store := testConn(t) 500 | defer store.Close() 501 | 502 | _, _, err := store.MkDir("/foo", nil, Always) 503 | ok(t, err) 504 | 505 | _, _, err = store.Set("/foo/bar", "value", Always) 506 | ok(t, err) 507 | 508 | node, err := store.Get("/foo", false) 509 | ok(t, err) 510 | 511 | equals(t, true, node.Dir) 512 | equals(t, 1, len(node.Nodes)) 513 | 514 | equals(t, "/foo/bar", node.Nodes[0].Key) 515 | equals(t, "value", node.Nodes[0].Value) 516 | } 517 | 518 | func Test_Get_ListDirectory_NotRecursive(t *testing.T) { 519 | store := testConn(t) 520 | defer store.Close() 521 | 522 | _, _, err := store.MkDir("/foo", nil, Always) 523 | ok(t, err) 524 | 525 | _, _, err = store.MkDir("/foo/bar", nil, Always) 526 | ok(t, err) 527 | 528 | _, _, err = store.Set("/foo/bar/baz", "value", Always) 529 | ok(t, err) 530 | 531 | node, err := store.Get("/foo", false) 532 | ok(t, err) 533 | 534 | equals(t, true, node.Dir) 535 | equals(t, 1, len(node.Nodes)) 536 | 537 | child := node.Nodes[0] 538 | 539 | equals(t, "/foo/bar", child.Key) 540 | equals(t, true, child.Dir) 541 | equals(t, 0, len(child.Nodes)) 542 | } 543 | 544 | func Test_Get_ListDirectory_Recursive(t *testing.T) { 545 | store := testConn(t) 546 | defer store.Close() 547 | 548 | _, _, err := store.MkDir("/foo", nil, Always) 549 | ok(t, err) 550 | 551 | _, _, err = store.MkDir("/foo/bar", nil, Always) 552 | ok(t, err) 553 | 554 | _, _, err = store.Set("/foo/bar/baz", "value", Always) 555 | ok(t, err) 556 | 557 | node, err := store.Get("/foo", true) 558 | ok(t, err) 559 | 560 | equals(t, true, node.Dir) 561 | equals(t, 1, len(node.Nodes)) 562 | 563 | child := node.Nodes[0] 564 | 565 | equals(t, "/foo/bar", child.Key) 566 | equals(t, true, child.Dir) 567 | equals(t, 1, len(child.Nodes)) 568 | 569 | grandchild := child.Nodes[0] 570 | 571 | equals(t, "/foo/bar/baz", grandchild.Key) 572 | equals(t, false, grandchild.Dir) 573 | equals(t, "value", grandchild.Value) 574 | equals(t, 0, len(grandchild.Nodes)) 575 | } 576 | 577 | func Test_Set_CreatesParentDirectories(t *testing.T) { 578 | store := testConn(t) 579 | defer store.Close() 580 | 581 | _, _, err := store.Set("/foo/bar/baz", "value", Always) 582 | ok(t, err) 583 | 584 | node, err := store.Get("/foo", true) 585 | ok(t, err) 586 | 587 | equals(t, true, node.Dir) 588 | equals(t, 1, len(node.Nodes)) 589 | 590 | child := node.Nodes[0] 591 | 592 | equals(t, "/foo/bar", child.Key) 593 | equals(t, true, child.Dir) 594 | equals(t, 1, len(child.Nodes)) 595 | 596 | grandchild := child.Nodes[0] 597 | 598 | equals(t, "/foo/bar/baz", grandchild.Key) 599 | equals(t, false, grandchild.Dir) 600 | equals(t, "value", grandchild.Value) 601 | equals(t, 0, len(grandchild.Nodes)) 602 | 603 | equals(t, grandchild.CreatedIndex, node.CreatedIndex) 604 | equals(t, grandchild.ModifiedIndex, node.ModifiedIndex) 605 | } 606 | 607 | func Test_Set_CreatesParentDirectories_GetNonRecursive(t *testing.T) { 608 | store := testConn(t) 609 | defer store.Close() 610 | 611 | _, _, err := store.Set("/foo/bar/baz", "value", Always) 612 | ok(t, err) 613 | 614 | node, err := store.Get("/foo", false) 615 | ok(t, err) 616 | 617 | if node == nil { 618 | fatalf(t, "expected a directory, but got nil") 619 | } 620 | 621 | equals(t, true, node.Dir) 622 | equals(t, 1, len(node.Nodes)) 623 | 624 | child := node.Nodes[0] 625 | 626 | equals(t, "/foo/bar", child.Key) 627 | equals(t, true, child.Dir) 628 | equals(t, 0, len(child.Nodes)) 629 | } 630 | 631 | func Test_Set_DoesNotOverwriteParentFile(t *testing.T) { 632 | store := testConn(t) 633 | defer store.Close() 634 | 635 | _, _, err := store.Set("/foo", "value", Always) 636 | ok(t, err) 637 | 638 | _, _, err = store.Set("/foo/bar", "value", Always) 639 | expectError(t, "Not a directory", "/foo", err) 640 | } 641 | 642 | func Test_MkDir_DoesNotOverwriteParentFile(t *testing.T) { 643 | store := testConn(t) 644 | defer store.Close() 645 | 646 | _, _, err := store.Set("/foo", "value", Always) 647 | ok(t, err) 648 | 649 | _, _, err = store.MkDir("/foo/bar", nil, Always) 650 | expectError(t, "Not a directory", "/foo", err) 651 | } 652 | 653 | func Test_Delete_DoesNotRemoveDirectory(t *testing.T) { 654 | store := testConn(t) 655 | defer store.Close() 656 | 657 | _, _, err := store.MkDir("/foo", nil, Always) 658 | ok(t, err) 659 | 660 | _, _, err = store.Delete("/foo", Always) 661 | expectError(t, "Not a file", "/foo", err) 662 | } 663 | 664 | // XXX this is kind of weird, but dir=true can also delete files 665 | func Test_RmDir_CanRemoveFile(t *testing.T) { 666 | store := testConn(t) 667 | defer store.Close() 668 | 669 | _, _, err := store.Set("/foo", "value", Always) 670 | ok(t, err) 671 | 672 | _, _, err = store.RmDir("/foo", false, Always) 673 | ok(t, err) 674 | 675 | _, err = store.Get("/foo", false) 676 | expectError(t, "Key not found", "/foo", err) 677 | } 678 | 679 | func Test_RmDir_MissingKey(t *testing.T) { 680 | store := testConn(t) 681 | defer store.Close() 682 | 683 | _, _, err := store.RmDir("/foo", false, Always) 684 | expectError(t, "Key not found", "/foo", err) 685 | } 686 | 687 | func Test_RmDir_CanRemoveEmptyDirectory(t *testing.T) { 688 | store := testConn(t) 689 | defer store.Close() 690 | 691 | _, _, err := store.MkDir("/foo", nil, Always) 692 | ok(t, err) 693 | 694 | _, _, err = store.RmDir("/foo", false, Always) 695 | ok(t, err) 696 | } 697 | 698 | func Test_RmDir_DoesNotRemoveNonEmptyDirectory(t *testing.T) { 699 | store := testConn(t) 700 | defer store.Close() 701 | 702 | _, _, err := store.Set("/foo/bar", "value", Always) 703 | ok(t, err) 704 | 705 | _, _, err = store.RmDir("/foo", false, Always) 706 | expectError(t, "Directory not empty", "/foo", err) 707 | 708 | node, err := store.Get("/foo", false) 709 | ok(t, err) 710 | equals(t, true, node.Dir) 711 | 712 | node, err = store.Get("/foo/bar", false) 713 | ok(t, err) 714 | equals(t, "value", node.Value) 715 | } 716 | 717 | func Test_RmDir_Recursive(t *testing.T) { 718 | store := testConn(t) 719 | defer store.Close() 720 | 721 | _, _, err := store.Set("/foo/bar", "value", Always) 722 | ok(t, err) 723 | 724 | _, _, err = store.RmDir("/foo", true, Always) 725 | ok(t, err) 726 | 727 | _, err = store.Get("/foo", false) 728 | expectError(t, "Key not found", "/foo", err) 729 | 730 | _, err = store.Get("/foo/bar", false) 731 | expectError(t, "Key not found", "/foo/bar", err) 732 | } 733 | 734 | func Test_RmDir_ErrorIndex(t *testing.T) { 735 | store := testConn(t) 736 | defer store.Close() 737 | 738 | origIndex, err := store.incrementIndex(store.db) 739 | ok(t, err) 740 | 741 | // ensure the index update is persisted 742 | equals(t, origIndex, currIndex(store)) 743 | 744 | _, _, err = store.RmDir("/foo", false, Always) 745 | expectError(t, "Key not found", "/foo", err) 746 | 747 | // the index should not be updated by the failed rmdir 748 | equals(t, origIndex, currIndex(store)) 749 | 750 | // the error should contain the current index 751 | equals(t, origIndex, err.(models.Error).Index) 752 | } 753 | 754 | func Test_TTL_SetsExpiration(t *testing.T) { 755 | store := testConn(t) 756 | defer store.Close() 757 | 758 | _, _, err := store.SetTTL("/foo", "value", 100, Always) 759 | ok(t, err) 760 | 761 | node, err := store.Get("/foo", false) 762 | ok(t, err) 763 | 764 | equals(t, int64(100), *node.TTL) 765 | if node.Expiration.IsZero() { 766 | fatalf(t, "expected Expiration to have a non-zero value") 767 | } 768 | 769 | diff := node.Expiration.Sub(time.Now().UTC()) 770 | if diff.Seconds() > 110 || diff.Seconds() < 90 { 771 | fatalf(t, "expected Expiration to occur in ~100s, but got: %d", diff) 772 | } 773 | } 774 | 775 | func Test_TTL_MkDir(t *testing.T) { 776 | store := testConn(t) 777 | defer store.Close() 778 | 779 | ttl := int64(100) 780 | 781 | _, _, err := store.MkDir("/foo", &ttl, Always) 782 | ok(t, err) 783 | 784 | node, err := store.Get("/foo", false) 785 | ok(t, err) 786 | 787 | equals(t, true, node.Dir) 788 | 789 | equals(t, ttl, *node.TTL) 790 | if node.Expiration.IsZero() { 791 | fatalf(t, "expected Expiration to have a non-zero value") 792 | } 793 | 794 | diff := node.Expiration.Sub(time.Now().UTC()) 795 | if diff.Seconds() > 110 || diff.Seconds() < 90 { 796 | fatalf(t, "expected Expiration to occur in ~100s, but got: %d", diff) 797 | } 798 | } 799 | 800 | func Test_TTL_SetThenClear(t *testing.T) { 801 | store := testConn(t) 802 | defer store.Close() 803 | 804 | _, _, err := store.SetTTL("/foo", "value", 100, Always) 805 | ok(t, err) 806 | 807 | node, err := store.Get("/foo", false) 808 | ok(t, err) 809 | 810 | equals(t, int64(100), *node.TTL) 811 | if node.Expiration.IsZero() { 812 | fatalf(t, "expected Expiration to have a non-zero value") 813 | } 814 | 815 | // Should clear the TTL 816 | _, _, err = store.Set("/foo", "value", Always) 817 | 818 | node, err = store.Get("/foo", false) 819 | ok(t, err) 820 | 821 | if node.TTL != nil { 822 | fatalf(t, "expected TTL to be nil, but got: %d", *node.TTL) 823 | } 824 | if node.Expiration != nil { 825 | fatalf(t, "expected Expiration to be nil, but got: %s", node.Expiration) 826 | } 827 | } 828 | 829 | func Test_TTL_CountsDown(t *testing.T) { 830 | store := testConn(t) 831 | defer store.Close() 832 | 833 | _, _, err := store.SetTTL("/foo", "value", 100, Always) 834 | ok(t, err) 835 | 836 | node, err := store.Get("/foo", false) 837 | ok(t, err) 838 | equals(t, int64(100), *node.TTL) 839 | 840 | // MySQL only stores to 1-second precision, so sleep long enough 841 | // to make sure there's no chance of truncation error 842 | time.Sleep(2 * time.Second) 843 | 844 | node, err = store.Get("/foo", false) 845 | ok(t, err) 846 | 847 | if !(*node.TTL < 100) { 848 | fatalf(t, "expected TTL to have decreased, but got: %d", *node.TTL) 849 | } 850 | } 851 | 852 | func Test_TTL_NodeExpires(t *testing.T) { 853 | store := testConn(t) 854 | defer store.Close() 855 | 856 | _, _, err := store.SetTTL("/foo", "value", 1, Always) 857 | ok(t, err) 858 | 859 | node, err := store.Get("/foo", false) 860 | ok(t, err) 861 | equals(t, int64(1), *node.TTL) 862 | 863 | // MySQL only stores to 1-second precision, so sleep long enough 864 | // to make sure there's no chance of truncation error 865 | time.Sleep(2 * time.Second) 866 | 867 | _, err = store.Get("/foo", false) 868 | expectError(t, "Key not found", "/foo", err) 869 | } 870 | 871 | func Test_TTL_DirExpiresEmpty(t *testing.T) { 872 | store := testConn(t) 873 | defer store.Close() 874 | 875 | ttl := int64(1) 876 | 877 | _, _, err := store.MkDir("/foo", &ttl, Always) 878 | ok(t, err) 879 | 880 | node, err := store.Get("/foo", false) 881 | ok(t, err) 882 | equals(t, int64(1), *node.TTL) 883 | equals(t, true, node.Dir) 884 | 885 | // MySQL only stores to 1-second precision, so sleep long enough 886 | // to make sure there's no chance of truncation error 887 | time.Sleep(2 * time.Second) 888 | 889 | _, err = store.Get("/foo", false) 890 | expectError(t, "Key not found", "/foo", err) 891 | } 892 | 893 | func Test_TTL_DirExpiresChildren(t *testing.T) { 894 | store := testConn(t) 895 | defer store.Close() 896 | 897 | ttl := int64(1) 898 | 899 | _, _, err := store.MkDir("/foo", &ttl, Always) 900 | ok(t, err) 901 | 902 | _, _, err = store.Set("/foo/bar", "bar", Always) 903 | ok(t, err) 904 | 905 | node, err := store.Get("/foo/bar", false) 906 | ok(t, err) 907 | equals(t, "bar", node.Value) 908 | 909 | // MySQL only stores to 1-second precision, so sleep long enough 910 | // to make sure there's no chance of truncation error 911 | time.Sleep(2 * time.Second) 912 | 913 | _, err = store.Get("/foo/bar", false) 914 | expectError(t, "Key not found", "/foo/bar", err) 915 | } 916 | 917 | func Test_CreateInOrder(t *testing.T) { 918 | store := testConn(t) 919 | defer store.Close() 920 | 921 | node1, err := store.CreateInOrder("/foo", "value", nil) 922 | ok(t, err) 923 | 924 | equals(t, int64(1), node1.CreatedIndex) 925 | equals(t, "/foo/1", node1.Key) 926 | equals(t, "value", node1.Value) 927 | 928 | node2, err := store.CreateInOrder("/foo", "value", nil) 929 | ok(t, err) 930 | 931 | equals(t, int64(2), node2.CreatedIndex) 932 | equals(t, "/foo/2", node2.Key) 933 | equals(t, "value", node2.Value) 934 | } 935 | 936 | func Test_CreateInOrder_TTL(t *testing.T) { 937 | store := testConn(t) 938 | defer store.Close() 939 | 940 | ttl := int64(100) 941 | node, err := store.CreateInOrder("/foo", "value", &ttl) 942 | ok(t, err) 943 | 944 | equals(t, "/foo/1", node.Key) 945 | equals(t, ttl, *node.TTL) 946 | if node.Expiration.IsZero() { 947 | fatalf(t, "expected Expiration to have a non-zero value") 948 | } 949 | } 950 | 951 | func fatalf(tb testing.TB, format string, args ...interface{}) { 952 | fatalfLvl(1, tb, format, args...) 953 | } 954 | 955 | func fatalfLvl(lvl int, tb testing.TB, format string, args ...interface{}) { 956 | _, file, line, _ := runtime.Caller(lvl + 1) 957 | msg := fmt.Sprintf(format, args...) 958 | fmt.Printf("\033[31m%s:%d:%s\033[39m\n\n", filepath.Base(file), line, msg) 959 | tb.FailNow() 960 | } 961 | 962 | func expectError(tb testing.TB, message, cause string, err error) { 963 | if modelError, ok := err.(models.Error); ok { 964 | if modelError.Message != message { 965 | fatalfLvl(1, tb, "\n\n\texpected Message: %#v\n\n\tgot: %#v", message, modelError.Message) 966 | } 967 | if modelError.Cause != cause { 968 | fatalfLvl(1, tb, "\n\n\texpected Cause: %#v\n\n\tgot: %#v", cause, modelError.Cause) 969 | } 970 | } else { 971 | fatalfLvl(1, tb, "expected models.Error, but got %T %#v", err, err) 972 | } 973 | } 974 | 975 | // ok fails the test if an err is not nil. 976 | func ok(tb testing.TB, err error) { 977 | if err != nil { 978 | fatalfLvl(1, tb, " unexpected error: %s", err.Error()) 979 | } 980 | } 981 | 982 | // equals fails the test if exp is not equal to act. 983 | func equals(tb testing.TB, exp, act interface{}) { 984 | if !reflect.DeepEqual(exp, act) { 985 | fatalfLvl(1, tb, "\n\n\texp: %#v\n\n\tgot: %#v", exp, act) 986 | } 987 | } 988 | --------------------------------------------------------------------------------