mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-31 20:39:37 +08:00
422 lines
12 KiB
Go
422 lines
12 KiB
Go
/*
|
|
Copyright 2015-2019 Gravitational, Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
// Package backend provides storage backend abstraction layer
|
|
package backend
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gravitational/trace"
|
|
"github.com/jonboulle/clockwork"
|
|
|
|
"github.com/gravitational/teleport/api/internalutils/stream"
|
|
"github.com/gravitational/teleport/api/types"
|
|
)
|
|
|
|
// Forever means that object TTL will not expire unless deleted
|
|
const (
|
|
Forever time.Duration = 0
|
|
)
|
|
|
|
// Backend implements abstraction over local or remote storage backend.
|
|
// Item keys are assumed to be valid UTF8, which may be enforced by the
|
|
// various Backend implementations.
|
|
type Backend interface {
|
|
// Create creates item if it does not exist
|
|
Create(ctx context.Context, i Item) (*Lease, error)
|
|
|
|
// Put puts value into backend (creates if it does not
|
|
// exists, updates it otherwise)
|
|
Put(ctx context.Context, i Item) (*Lease, error)
|
|
|
|
// CompareAndSwap compares item with existing item
|
|
// and replaces is with replaceWith item
|
|
CompareAndSwap(ctx context.Context, expected Item, replaceWith Item) (*Lease, error)
|
|
|
|
// Update updates value in the backend
|
|
Update(ctx context.Context, i Item) (*Lease, error)
|
|
|
|
// Get returns a single item or not found error
|
|
Get(ctx context.Context, key []byte) (*Item, error)
|
|
|
|
// GetRange returns query range
|
|
GetRange(ctx context.Context, startKey []byte, endKey []byte, limit int) (*GetResult, error)
|
|
|
|
// Delete deletes item by key, returns NotFound error
|
|
// if item does not exist
|
|
Delete(ctx context.Context, key []byte) error
|
|
|
|
// DeleteRange deletes range of items with keys between startKey and endKey
|
|
DeleteRange(ctx context.Context, startKey, endKey []byte) error
|
|
|
|
// KeepAlive keeps object from expiring, updates lease on the existing object,
|
|
// expires contains the new expiry to set on the lease,
|
|
// some backends may ignore expires based on the implementation
|
|
// in case if the lease managed server side
|
|
KeepAlive(ctx context.Context, lease Lease, expires time.Time) error
|
|
|
|
// NewWatcher returns a new event watcher
|
|
NewWatcher(ctx context.Context, watch Watch) (Watcher, error)
|
|
|
|
// Close closes backend and all associated resources
|
|
Close() error
|
|
|
|
// Clock returns clock used by this backend
|
|
Clock() clockwork.Clock
|
|
|
|
// CloseWatchers closes all the watchers
|
|
// without closing the backend
|
|
CloseWatchers()
|
|
}
|
|
|
|
// IterateRange is a helper for stepping over a range
|
|
func IterateRange(ctx context.Context, bk Backend, startKey []byte, endKey []byte, limit int, fn func([]Item) (stop bool, err error)) error {
|
|
for {
|
|
rslt, err := bk.GetRange(ctx, startKey, endKey, limit)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
stop, err := fn(rslt.Items)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
if stop || len(rslt.Items) < limit {
|
|
return nil
|
|
}
|
|
startKey = nextKey(rslt.Items[limit-1].Key)
|
|
}
|
|
}
|
|
|
|
// StreamRange constructs a Stream for the given key range. This helper just
|
|
// uses standard pagination under the hood, lazily loading pages as needed. Streams
|
|
// are currently only used for periodic operations, but if they become more widely
|
|
// used in the future, it may become worthwhile to optimize the streaming of backend
|
|
// items further. Two potential improvements of note:
|
|
//
|
|
// 1. update this helper to concurrently load the next page in the background while
|
|
// items from the current page are being yielded.
|
|
//
|
|
// 2. allow individual backends to expose custom streaming methods s.t. the most performant
|
|
// impl for a given backend may be used.
|
|
func StreamRange(ctx context.Context, bk Backend, startKey, endKey []byte, pageSize int) stream.Stream[Item] {
|
|
return stream.PageFunc[Item](func() ([]Item, error) {
|
|
if startKey == nil {
|
|
return nil, io.EOF
|
|
}
|
|
rslt, err := bk.GetRange(ctx, startKey, endKey, pageSize)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
if len(rslt.Items) < pageSize {
|
|
startKey = nil
|
|
} else {
|
|
startKey = nextKey(rslt.Items[pageSize-1].Key)
|
|
}
|
|
return rslt.Items, nil
|
|
})
|
|
}
|
|
|
|
// Batch implements some batch methods
|
|
// that are not mandatory for all interfaces,
|
|
// only the ones used in bulk operations.
|
|
type Batch interface {
|
|
// PutRange puts range of items in one transaction
|
|
PutRange(ctx context.Context, items []Item) error
|
|
}
|
|
|
|
// Lease represents a lease on the item that can be used
|
|
// to extend item's TTL without updating its contents.
|
|
//
|
|
// Here is an example of renewing object TTL:
|
|
//
|
|
// item.Expires = time.Now().Add(10 * time.Second)
|
|
// lease, err := backend.Create(ctx, item)
|
|
// expires := time.Now().Add(20 * time.Second)
|
|
// err = backend.KeepAlive(ctx, lease, expires)
|
|
type Lease struct {
|
|
// Key is an object representing lease
|
|
Key []byte
|
|
// ID is a lease ID, could be empty
|
|
ID int64
|
|
}
|
|
|
|
// IsEmpty returns true if the lease is empty value
|
|
func (l *Lease) IsEmpty() bool {
|
|
return l.ID == 0 && len(l.Key) == 0
|
|
}
|
|
|
|
// Watch specifies watcher parameters
|
|
type Watch struct {
|
|
// Name is a watch name set for debugging
|
|
// purposes
|
|
Name string
|
|
// Prefixes specifies prefixes to watch,
|
|
// passed to the backend implementation
|
|
Prefixes [][]byte
|
|
// QueueSize is an optional queue size
|
|
QueueSize int
|
|
// MetricComponent if set will start reporting
|
|
// with a given component metric
|
|
MetricComponent string
|
|
}
|
|
|
|
// String returns a user-friendly description
|
|
// of the watcher
|
|
func (w *Watch) String() string {
|
|
return fmt.Sprintf("Watcher(name=%v, prefixes=%v)", w.Name, string(bytes.Join(w.Prefixes, []byte(", "))))
|
|
}
|
|
|
|
// Watcher returns watcher
|
|
type Watcher interface {
|
|
// Events returns channel with events
|
|
Events() <-chan Event
|
|
|
|
// Done returns the channel signaling the closure
|
|
Done() <-chan struct{}
|
|
|
|
// Close closes the watcher and releases
|
|
// all associated resources
|
|
Close() error
|
|
}
|
|
|
|
// GetResult provides the result of GetRange request
|
|
type GetResult struct {
|
|
// Items returns a list of items
|
|
Items []Item
|
|
}
|
|
|
|
// Event is a event containing operation with item
|
|
type Event struct {
|
|
// Type is operation type
|
|
Type types.OpType
|
|
// Item is event Item
|
|
Item Item
|
|
}
|
|
|
|
// Item is a key value item
|
|
type Item struct {
|
|
// Key is a key of the key value item
|
|
Key []byte
|
|
// Value is a value of the key value item
|
|
Value []byte
|
|
// Expires is an optional record expiry time
|
|
Expires time.Time
|
|
// ID is a record ID, newer records have newer ids
|
|
ID int64
|
|
// LeaseID is a lease ID, could be set on objects
|
|
// with TTL
|
|
LeaseID int64
|
|
}
|
|
|
|
func (e Event) String() string {
|
|
val := string(e.Item.Value)
|
|
if len(val) > 20 {
|
|
val = val[:20] + "..."
|
|
}
|
|
return fmt.Sprintf("%v %s=%s", e.Type, e.Item.Key, val)
|
|
}
|
|
|
|
// Config is used for 'storage' config section. It's a combination of
|
|
// values for various backends: 'boltdb', 'etcd', 'filesystem' and 'dynamodb'
|
|
type Config struct {
|
|
// Type can be "bolt" or "etcd" or "dynamodb"
|
|
Type string `yaml:"type,omitempty"`
|
|
|
|
// Params is a generic key/value property bag which allows arbitrary
|
|
// values to be passed to backend
|
|
Params Params `yaml:",inline"`
|
|
}
|
|
|
|
// Params type defines a flexible unified back-end configuration API.
|
|
// It is just a map of key/value pairs which gets populated by `storage` section
|
|
// in Teleport YAML config.
|
|
type Params map[string]interface{}
|
|
|
|
// GetString returns a string value stored in Params map, or an empty string
|
|
// if nothing is found
|
|
func (p Params) GetString(key string) string {
|
|
v, ok := p[key]
|
|
if !ok {
|
|
return ""
|
|
}
|
|
s, _ := v.(string)
|
|
return s
|
|
}
|
|
|
|
// NoLimit specifies no limits
|
|
const NoLimit = 0
|
|
|
|
// nextKey returns the next possible key.
|
|
// If used with a key prefix, this will return
|
|
// the end of the range for that key prefix.
|
|
func nextKey(key []byte) []byte {
|
|
end := make([]byte, len(key))
|
|
copy(end, key)
|
|
for i := len(end) - 1; i >= 0; i-- {
|
|
if end[i] < 0xff {
|
|
end[i] = end[i] + 1
|
|
end = end[:i+1]
|
|
return end
|
|
}
|
|
}
|
|
// next key does not exist (e.g., 0xffff);
|
|
return noEnd
|
|
}
|
|
|
|
var noEnd = []byte{0}
|
|
|
|
// RangeEnd returns end of the range for given key.
|
|
func RangeEnd(key []byte) []byte {
|
|
return nextKey(key)
|
|
}
|
|
|
|
// NextPaginationKey returns the next pagination key.
|
|
// For resources that have the HostID in their keys, the next key will also
|
|
// have the HostID part.
|
|
func NextPaginationKey(r types.Resource) string {
|
|
switch resourceWithType := r.(type) {
|
|
case types.DatabaseServer:
|
|
return string(nextKey(internalKey(resourceWithType.GetHostID(), resourceWithType.GetName())))
|
|
case types.AppServer:
|
|
return string(nextKey(internalKey(resourceWithType.GetHostID(), resourceWithType.GetName())))
|
|
case types.KubeServer:
|
|
return string(nextKey(internalKey(resourceWithType.GetHostID(), resourceWithType.GetName())))
|
|
default:
|
|
return string(nextKey([]byte(r.GetName())))
|
|
}
|
|
}
|
|
|
|
// GetPaginationKey returns the pagination key given resource.
|
|
func GetPaginationKey(r types.Resource) string {
|
|
switch resourceWithType := r.(type) {
|
|
case types.DatabaseServer:
|
|
return string(internalKey(resourceWithType.GetHostID(), resourceWithType.GetName()))
|
|
case types.AppServer:
|
|
return string(internalKey(resourceWithType.GetHostID(), resourceWithType.GetName()))
|
|
case types.KubeServer:
|
|
return string(internalKey(resourceWithType.GetHostID(), resourceWithType.GetName()))
|
|
case types.WindowsDesktop:
|
|
return string(internalKey(resourceWithType.GetHostID(), resourceWithType.GetName()))
|
|
default:
|
|
return r.GetName()
|
|
}
|
|
}
|
|
|
|
// MaskKeyName masks the given key name.
|
|
// e.g "123456789" -> "******789"
|
|
func MaskKeyName(keyName string) []byte {
|
|
maskedBytes := []byte(keyName)
|
|
hiddenBefore := int(0.75 * float64(len(keyName)))
|
|
for i := 0; i < hiddenBefore; i++ {
|
|
maskedBytes[i] = '*'
|
|
}
|
|
return maskedBytes
|
|
}
|
|
|
|
// Items is a sortable list of backend items
|
|
type Items []Item
|
|
|
|
// Len is part of sort.Interface.
|
|
func (it Items) Len() int {
|
|
return len(it)
|
|
}
|
|
|
|
// Swap is part of sort.Interface.
|
|
func (it Items) Swap(i, j int) {
|
|
it[i], it[j] = it[j], it[i]
|
|
}
|
|
|
|
// Less is part of sort.Interface.
|
|
func (it Items) Less(i, j int) bool {
|
|
return bytes.Compare(it[i].Key, it[j].Key) < 0
|
|
}
|
|
|
|
// TTL returns TTL in duration units, rounds up to one second
|
|
func TTL(clock clockwork.Clock, expires time.Time) time.Duration {
|
|
ttl := expires.Sub(clock.Now())
|
|
if ttl < time.Second {
|
|
return time.Second
|
|
}
|
|
return ttl
|
|
}
|
|
|
|
// EarliestExpiry returns first of the
|
|
// otherwise returns empty
|
|
func EarliestExpiry(times ...time.Time) time.Time {
|
|
if len(times) == 0 {
|
|
return time.Time{}
|
|
}
|
|
sort.Sort(earliest(times))
|
|
return times[0]
|
|
}
|
|
|
|
// Expiry converts ttl to expiry time, if ttl is 0
|
|
// returns empty time
|
|
func Expiry(clock clockwork.Clock, ttl time.Duration) time.Time {
|
|
if ttl == 0 {
|
|
return time.Time{}
|
|
}
|
|
return clock.Now().UTC().Add(ttl)
|
|
}
|
|
|
|
type earliest []time.Time
|
|
|
|
func (p earliest) Len() int {
|
|
return len(p)
|
|
}
|
|
|
|
func (p earliest) Less(i, j int) bool {
|
|
if p[i].IsZero() {
|
|
return false
|
|
}
|
|
if p[j].IsZero() {
|
|
return true
|
|
}
|
|
return p[i].Before(p[j])
|
|
}
|
|
|
|
func (p earliest) Swap(i, j int) {
|
|
p[i], p[j] = p[j], p[i]
|
|
}
|
|
|
|
// Separator is used as a separator between key parts
|
|
const Separator = '/'
|
|
|
|
// Key joins parts into path separated by Separator,
|
|
// makes sure path always starts with Separator ("/")
|
|
func Key(parts ...string) []byte {
|
|
return internalKey("", parts...)
|
|
}
|
|
|
|
// ExactKey is like Key, except a Separator is appended to the result
|
|
// path of Key. This is to ensure range matching of a path will only
|
|
// math child paths and not other paths that have the resulting path
|
|
// as a prefix.
|
|
func ExactKey(parts ...string) []byte {
|
|
return append(Key(parts...), Separator)
|
|
}
|
|
|
|
func internalKey(internalPrefix string, parts ...string) []byte {
|
|
return []byte(strings.Join(append([]string{internalPrefix}, parts...), string(Separator)))
|
|
}
|