blob: c1969d43b27ad5d29fb2c4debba79522bb00d73f [file] [log] [blame]
package curator
import (
"fmt"
"strings"
clientv3 "go.etcd.io/etcd/client/v3"
)
// etcdPrefix is the location of some data in etcd, with each data element keyed
// by some unique string key.
//
// Prefixes are /-delimited filesystem-like paths, eg. prefix `/foo/bar/` could
// contain an item with ID `one` at etcd key `/foo/bar/one`, and an item with ID
// `two` at etcd key `/foo/bar/two`.
//
// Given etcd's lexicographic range operation, this allows for retrieval of all
// items under a given prefix.
//
// An etcdPrefix should be built using the newEtcdPrefix function.
type etcdPrefix struct {
// parts are the parts of an etcd prefix path, eg. for prefix '/foo/bar/' parts
// would be ["foo", "bar"].
parts []string
}
// newEtcdPrefix returns an etcdPrefix for the given string representation of a
// prefix. The string representation must start and end with a / character, and
// must contain at least one path component (ie. one /-delimited component) with
// no empty components.
func newEtcdPrefix(p string) (*etcdPrefix, error) {
parts := strings.Split(p, "/")
if len(parts) < 3 {
return nil, fmt.Errorf("invalid path: must contain at least one component")
}
// Expect ["", "foo", "bar", ""] for /foo/bar/. Ie., everything but the first
// and part path part must be non-empty, and the first and last path parts must
// be empty.
for i, p := range parts {
if i == 0 || i == len(parts)-1 {
if p != "" {
return nil, fmt.Errorf("invalid path: must start and end with a /")
}
} else {
if p == "" {
return nil, fmt.Errorf("invalid path: must not contain repeated / characters")
}
}
}
// Omit the leading and trailing parts, ie. keep ["foo", "bar"] for /foo/bar/.
parts = parts[1 : len(parts)-1]
return &etcdPrefix{parts}, nil
}
// mustNewEtcdPrefix returns an etcdPrefix given the string representation of a
// path, or panics if the string representation is invalid (see newEtcdPrefix
// for more information).
func mustNewEtcdPrefix(p string) etcdPrefix {
res, err := newEtcdPrefix(p)
if err != nil {
panic(err)
}
return *res
}
// Key returns the key for an item within an etcdPrefix, or an error if the
// given ID is invalid (ie. contains a / character).
func (p etcdPrefix) Key(id string) (string, error) {
if id == "" {
return "", fmt.Errorf("invalid id: cannot be empty")
}
if strings.Contains(id, "/") {
return "", fmt.Errorf("invalid id: cannot contain / character")
}
path := append(p.parts, id)
return "/" + strings.Join(path, "/"), nil
}
// keyRange returns a pair of [start, end) keys for use with etcd range queries
// to retrieve all keys under a given prefix.
func (p etcdPrefix) KeyRange() (start, end string) {
// Range from /foo/bar/ ... to /foo/bar0 ('0' is one ASCII codepoint after '/').
start = "/" + strings.Join(p.parts, "/") + "/"
end = "/" + strings.Join(p.parts, "/") + "0"
return
}
// Range returns an etcd clientv3.Op that represents a Range Get request over
// all the keys within this etcdPrefix.
func (p etcdPrefix) Range() clientv3.Op {
start, end := p.KeyRange()
return clientv3.OpGet(start, clientv3.WithRange(end))
}
// ExtractID carves out the etcdPrefix ID from an existing etcd key for this
// range, ie. is the reverse of the Key() function.
//
// If the given etcd key does not correspond to a valid ID, an empty string is
// returned.
func (p etcdPrefix) ExtractID(key string) string {
strPrefix := "/" + strings.Join(p.parts, "/") + "/"
if !strings.HasPrefix(key, strPrefix) {
return ""
}
id := key[len(strPrefix):]
if strings.Contains(id, "/") {
return ""
}
return id
}