diff --git a/lib/backend/backend.go b/lib/backend/backend.go
index 1490d6eb98236..8fafadb123a2e 100644
--- a/lib/backend/backend.go
+++ b/lib/backend/backend.go
@@ -20,12 +20,10 @@
package backend
import (
- "bytes"
"context"
"fmt"
"io"
"sort"
- "strings"
"time"
"github.com/google/uuid"
@@ -222,7 +220,7 @@ type Watch struct {
// 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(", "))))
+ return fmt.Sprintf("Watcher(name=%v, prefixes=%v)", w.Name, w.Prefixes)
}
// Watcher returns watcher
@@ -386,7 +384,7 @@ func (it Items) Swap(i, j int) {
// Less is part of sort.Interface.
func (it Items) Less(i, j int) bool {
- return bytes.Compare(it[i].Key, it[j].Key) < 0
+ return it[i].Key.Compare(it[j].Key) < 0
}
// TTL returns TTL in duration units, rounds up to one second
@@ -437,27 +435,6 @@ 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 = '/'
-
-// NewKey joins parts into path separated by Separator,
-// makes sure path always starts with Separator ("/")
-func NewKey(parts ...string) Key {
- 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) Key {
- return append(NewKey(parts...), Separator)
-}
-
-func internalKey(internalPrefix string, parts ...string) Key {
- return Key(strings.Join(append([]string{internalPrefix}, parts...), string(Separator)))
-}
-
// CreateRevision generates a new identifier to be used
// as a resource revision. Backend implementations that provide
// their own mechanism for versioning resources should be
diff --git a/lib/backend/buffer.go b/lib/backend/buffer.go
index 24211e0b2b1ea..9509a4b038138 100644
--- a/lib/backend/buffer.go
+++ b/lib/backend/buffer.go
@@ -322,7 +322,7 @@ type BufferWatcher struct {
// String returns user-friendly representation
// of the buffer watcher
func (w *BufferWatcher) String() string {
- return fmt.Sprintf("Watcher(name=%v, prefixes=%v, capacity=%v, size=%v)", w.Name, string(bytes.Join(w.Prefixes, []byte(", "))), w.capacity, len(w.eventsC))
+ return fmt.Sprintf("Watcher(name=%v, prefixes=%v, capacity=%v, size=%v)", w.Name, w.Prefixes, w.capacity, len(w.eventsC))
}
// Events returns events channel. This method performs internal work and should be re-called after each event
diff --git a/lib/backend/dynamo/dynamodbbk.go b/lib/backend/dynamo/dynamodbbk.go
index a11e735f0fa1e..cd174ccea4b58 100644
--- a/lib/backend/dynamo/dynamodbbk.go
+++ b/lib/backend/dynamo/dynamodbbk.go
@@ -19,7 +19,6 @@
package dynamo
import (
- "bytes"
"context"
"net/http"
"sort"
@@ -553,7 +552,7 @@ func (b *Backend) CompareAndSwap(ctx context.Context, expected backend.Item, rep
if len(replaceWith.Key) == 0 {
return nil, trace.BadParameter("missing parameter Key")
}
- if !bytes.Equal(expected.Key, replaceWith.Key) {
+ if expected.Key.Compare(replaceWith.Key) != 0 {
return nil, trace.BadParameter("expected and replaceWith keys should match")
}
diff --git a/lib/backend/etcdbk/etcd.go b/lib/backend/etcdbk/etcd.go
index cec359637cc25..a268be31c78bc 100644
--- a/lib/backend/etcdbk/etcd.go
+++ b/lib/backend/etcdbk/etcd.go
@@ -20,7 +20,6 @@
package etcdbk
import (
- "bytes"
"context"
"crypto/tls"
"crypto/x509"
@@ -798,7 +797,7 @@ func (b *EtcdBackend) CompareAndSwap(ctx context.Context, expected backend.Item,
if len(replaceWith.Key) == 0 {
return nil, trace.BadParameter("missing parameter Key")
}
- if !bytes.Equal(expected.Key, replaceWith.Key) {
+ if expected.Key.Compare(replaceWith.Key) != 0 {
return nil, trace.BadParameter("expected and replaceWith keys should match")
}
var opts []clientv3.OpOption
@@ -1112,7 +1111,7 @@ func fromType(eventType mvccpb.Event_EventType) types.OpType {
}
func (b *EtcdBackend) trimPrefix(in backend.Key) backend.Key {
- return bytes.TrimPrefix(in, backend.Key(b.cfg.Key))
+ return in.TrimPrefix(backend.Key(b.cfg.Key))
}
func (b *EtcdBackend) prependPrefix(in backend.Key) string {
diff --git a/lib/backend/firestore/firestorebk.go b/lib/backend/firestore/firestorebk.go
index ba6ea8ec3b4bb..bac9b056ab251 100644
--- a/lib/backend/firestore/firestorebk.go
+++ b/lib/backend/firestore/firestorebk.go
@@ -572,7 +572,7 @@ func (b *Backend) CompareAndSwap(ctx context.Context, expected backend.Item, rep
if len(replaceWith.Key) == 0 {
return nil, trace.BadParameter("missing parameter Key")
}
- if !bytes.Equal(expected.Key, replaceWith.Key) {
+ if expected.Key.Compare(replaceWith.Key) != 0 {
return nil, trace.BadParameter("expected and replaceWith keys should match")
}
diff --git a/lib/backend/key.go b/lib/backend/key.go
index 2ff85090ef9fd..4c7f25c604edc 100644
--- a/lib/backend/key.go
+++ b/lib/backend/key.go
@@ -16,5 +16,93 @@
package backend
+import (
+ "bytes"
+ "fmt"
+ "strings"
+)
+
// Key is the unique identifier for an [Item].
-type Key = []byte
+type Key []byte
+
+// Separator is used as a separator between key parts
+const Separator = '/'
+
+// NewKey joins parts into path separated by Separator,
+// makes sure path always starts with Separator ("/")
+func NewKey(parts ...string) Key {
+ 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) Key {
+ return append(NewKey(parts...), Separator)
+}
+
+func internalKey(internalPrefix string, parts ...string) Key {
+ return Key(strings.Join(append([]string{internalPrefix}, parts...), string(Separator)))
+}
+
+// String returns the textual representation of the key with
+// each component concatenated together via the [Separator].
+func (k Key) String() string {
+ return string(k)
+}
+
+// HasPrefix reports whether the key begins with prefix.
+func (k Key) HasPrefix(prefix Key) bool {
+ return bytes.HasPrefix(k, prefix)
+}
+
+// TrimPrefix returns the key without the provided leading prefix string.
+// If the key doesn't start with prefix, it is returned unchanged.
+func (k Key) TrimPrefix(prefix Key) Key {
+ return bytes.TrimPrefix(k, prefix)
+}
+
+func (k Key) PrependPrefix(p Key) Key {
+ return append(p, k...)
+}
+
+// HasSuffix reports whether the key ends with suffix.
+func (k Key) HasSuffix(suffix Key) bool {
+ return bytes.HasSuffix(k, suffix)
+}
+
+// TrimSuffix returns the key without the provided trailing suffix string.
+// If the key doesn't end with suffix, it is returned unchanged.
+func (k Key) TrimSuffix(suffix Key) Key {
+ return bytes.TrimSuffix(k, suffix)
+}
+
+func (k Key) Components() [][]byte {
+ if len(k) == 0 {
+ return nil
+ }
+
+ sep := []byte{Separator}
+ return bytes.Split(bytes.TrimPrefix(k, sep), sep)
+}
+
+func (k Key) Compare(o Key) int {
+ return bytes.Compare(k, o)
+}
+
+// Scan implement sql.Scanner, allowing a [Key] to
+// be directly retrieved from sql backends without
+// an intermediary object.
+func (k *Key) Scan(scan any) error {
+ switch key := scan.(type) {
+ case []byte:
+ *k = bytes.Clone(key)
+ case string:
+ *k = []byte(strings.Clone(key))
+ default:
+ return fmt.Errorf("invalid Key type %T", scan)
+ }
+
+ return nil
+}
diff --git a/lib/backend/key_test.go b/lib/backend/key_test.go
new file mode 100644
index 0000000000000..d554fb6922357
--- /dev/null
+++ b/lib/backend/key_test.go
@@ -0,0 +1,475 @@
+// Teleport
+// Copyright (C) 2024 Gravitational, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package backend_test
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/gravitational/teleport/lib/backend"
+)
+
+func TestKey(t *testing.T) {
+
+ k1 := backend.Key("test")
+ k2 := backend.NewKey("test")
+ k3 := backend.ExactKey("test")
+
+ assert.NotEqual(t, k1, k2)
+ assert.NotEqual(t, k2, k3)
+ assert.NotEqual(t, k1, k3)
+
+ assert.Equal(t, "test", k1.String())
+ assert.Equal(t, "/test", k2.String())
+ assert.Equal(t, "/test/", k3.String())
+}
+
+func TestKeyString(t *testing.T) {
+ tests := []struct {
+ name string
+ expected string
+ key backend.Key
+ }{
+ {
+ name: "empty key produces empty string",
+ },
+ {
+ name: "empty new key produces empty string",
+ key: backend.NewKey(),
+ expected: "",
+ },
+ {
+ name: "key with only empty string produces separator",
+ key: backend.NewKey(""),
+ expected: "/",
+ },
+ {
+ name: "key with contents are separated",
+ key: backend.NewKey("foo", "bar", "baz", "quux"),
+ expected: "/foo/bar/baz/quux",
+ },
+ {
+ name: "empty exact key produces separator",
+ key: backend.ExactKey(),
+ expected: "/",
+ },
+ {
+ name: "empty string exact key produces double separator",
+ key: backend.ExactKey(""),
+ expected: "//",
+ },
+ {
+ name: "exact key adds trailing separator",
+ key: backend.ExactKey("foo", "bar", "baz", "quux"),
+ expected: "/foo/bar/baz/quux/",
+ },
+ {
+ name: "noend key",
+ key: backend.Key{0},
+ expected: "\x00",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ assert.Equal(t, test.expected, test.key.String())
+
+ })
+ }
+}
+
+func TestKeyComponents(t *testing.T) {
+ tests := []struct {
+ name string
+ key backend.Key
+ expected [][]byte
+ }{
+ {
+ name: "default value has zero components",
+ },
+ {
+ name: "empty key has zero components",
+ key: backend.NewKey(),
+ },
+ {
+ name: "empty exact key has empty component",
+ key: backend.ExactKey(),
+ expected: [][]byte{{}},
+ },
+ {
+ name: "single value key has a component",
+ key: backend.NewKey("alpha"),
+ expected: [][]byte{[]byte("alpha")},
+ },
+ {
+ name: "multiple components",
+ key: backend.NewKey("foo", "bar", "baz"),
+ expected: [][]byte{[]byte("foo"), []byte("bar"), []byte("baz")},
+ },
+ {
+ name: "key without separator",
+ key: backend.Key("testing"),
+ expected: [][]byte{[]byte("testing")},
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ assert.Equal(t, test.expected, test.key.Components())
+ })
+ }
+}
+
+func TestKeyScan(t *testing.T) {
+ tests := []struct {
+ name string
+ scan any
+ expectedError string
+ expectedKey backend.Key
+ }{
+ {
+ name: "invalid type int",
+ scan: 123,
+ expectedError: "invalid Key type int",
+ },
+ {
+ name: "invalid type bool",
+ scan: false,
+ expectedError: "invalid Key type bool",
+ },
+ {
+ name: "empty string key",
+ scan: "",
+ expectedKey: backend.Key{},
+ },
+ {
+ name: "empty byte slice key",
+ scan: []byte{},
+ expectedKey: backend.Key{},
+ },
+ {
+ name: "populated string key",
+ scan: backend.NewKey("foo", "bar", "baz").String(),
+ expectedKey: backend.NewKey("foo", "bar", "baz"),
+ },
+ {
+ name: "populated byte slice key",
+ scan: []byte(backend.NewKey("foo", "bar", "baz").String()),
+ expectedKey: backend.NewKey("foo", "bar", "baz"),
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ k := new(backend.Key)
+ err := k.Scan(test.scan)
+ if test.expectedError == "" {
+ assert.NoError(t, err)
+ } else {
+ assert.EqualError(t, err, test.expectedError)
+ }
+ assert.Equal(t, test.expectedKey, *k)
+ })
+ }
+}
+
+func TestKeyHasSuffix(t *testing.T) {
+ tests := []struct {
+ name string
+ key backend.Key
+ suffix backend.Key
+ assertion assert.BoolAssertionFunc
+ }{
+ {
+ name: "default key has no suffixes",
+ suffix: backend.NewKey("test"),
+ assertion: assert.False,
+ },
+ {
+ name: "default key is suffix",
+ assertion: assert.True,
+ },
+ {
+ name: "prefix is not a suffix",
+ key: backend.NewKey("a", "b", "c"),
+ suffix: backend.NewKey("a", "b"),
+ assertion: assert.False,
+ },
+ {
+ name: "empty suffix",
+ key: backend.NewKey("a", "b", "c"),
+ assertion: assert.True,
+ },
+ {
+ name: "valid multi component suffix",
+ key: backend.NewKey("a", "b", "c"),
+ suffix: backend.NewKey("b", "c"),
+ assertion: assert.True,
+ },
+ {
+ name: "valid single component suffix",
+ key: backend.NewKey("a", "b", "c"),
+ suffix: backend.NewKey("c"),
+ assertion: assert.True,
+ },
+ {
+ name: "equivalent keys are suffix",
+ key: backend.NewKey("a", "b", "c"),
+ suffix: backend.NewKey("a", "b", "c"),
+ assertion: assert.True,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ test.assertion(t, test.key.HasSuffix(test.suffix))
+ })
+ }
+}
+func TestKeyHasPrefix(t *testing.T) {
+ tests := []struct {
+ name string
+ key backend.Key
+ prefix backend.Key
+ assertion assert.BoolAssertionFunc
+ }{
+ {
+ name: "default key has no prexies",
+ prefix: backend.NewKey("test"),
+ assertion: assert.False,
+ },
+ {
+ name: "default key is prefix",
+ assertion: assert.True,
+ },
+ {
+ name: "suffix is not a prefix",
+ key: backend.NewKey("a", "b", "c"),
+ prefix: backend.NewKey("b", "c"),
+ assertion: assert.False,
+ },
+ {
+ name: "empty prefix",
+ key: backend.NewKey("a", "b", "c"),
+ assertion: assert.True,
+ },
+ {
+ name: "valid multi component prefix",
+ key: backend.NewKey("a", "b", "c"),
+ prefix: backend.NewKey("a", "b"),
+ assertion: assert.True,
+ },
+ {
+ name: "valid single component prefix",
+ key: backend.NewKey("a", "b", "c"),
+ prefix: backend.NewKey("a"),
+ assertion: assert.True,
+ },
+ {
+ name: "equivalent keys are prefix",
+ key: backend.NewKey("a", "b", "c"),
+ prefix: backend.NewKey("a", "b", "c"),
+ assertion: assert.True,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ test.assertion(t, test.key.HasPrefix(test.prefix))
+ })
+ }
+}
+
+func TestKeyTrimSuffix(t *testing.T) {
+ tests := []struct {
+ name string
+ key backend.Key
+ trim backend.Key
+ expected backend.Key
+ }{
+ {
+ name: "empty key trims nothing",
+ trim: backend.NewKey("a", "b"),
+ },
+ {
+ name: "empty trim trims nothing",
+ key: backend.NewKey("a", "b"),
+ expected: backend.NewKey("a", "b"),
+ },
+ {
+ name: "non-matching trim trims nothing",
+ key: backend.NewKey("a", "b"),
+ trim: backend.NewKey("c", "d"),
+ expected: backend.NewKey("a", "b"),
+ },
+ {
+ name: "prefix trim trims nothing",
+ key: backend.NewKey("a", "b", "c"),
+ trim: backend.NewKey("a", "b"),
+ expected: backend.NewKey("a", "b", "c"),
+ },
+ {
+ name: "all trimmed on exact match",
+ key: backend.NewKey("a", "b", "c"),
+ trim: backend.NewKey("a", "b", "c"),
+ expected: backend.NewKey(),
+ },
+ {
+ name: "partial trim",
+ key: backend.NewKey("a", "b", "c"),
+ trim: backend.NewKey("b", "c"),
+ expected: backend.NewKey("a"),
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ trimmed := test.key.TrimSuffix(test.trim)
+ assert.Equal(t, test.expected, trimmed)
+ })
+ }
+}
+
+func TestKeyTrimPrefix(t *testing.T) {
+ tests := []struct {
+ name string
+ key backend.Key
+ trim backend.Key
+ expected backend.Key
+ }{
+ {
+ name: "empty key trims nothing",
+ trim: backend.NewKey("a", "b"),
+ },
+ {
+ name: "empty trim trims nothing",
+ key: backend.NewKey("a", "b"),
+ expected: backend.NewKey("a", "b"),
+ },
+ {
+ name: "non-matching trim trims nothing",
+ key: backend.NewKey("a", "b"),
+ trim: backend.NewKey("c", "d"),
+ expected: backend.NewKey("a", "b"),
+ },
+ {
+ name: "suffix trim trims nothing",
+ key: backend.NewKey("a", "b", "c"),
+ trim: backend.NewKey("b", "c"),
+ expected: backend.NewKey("a", "b", "c"),
+ },
+ {
+ name: "all trimmed on exact match",
+ key: backend.NewKey("a", "b", "c"),
+ trim: backend.NewKey("a", "b", "c"),
+ expected: backend.NewKey(),
+ },
+ {
+ name: "partial trim",
+ key: backend.NewKey("a", "b", "c"),
+ trim: backend.NewKey("a", "b"),
+ expected: backend.NewKey("c"),
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ trimmed := test.key.TrimPrefix(test.trim)
+ assert.Equal(t, test.expected, trimmed)
+ })
+ }
+}
+
+func TestKeyPrependPrefix(t *testing.T) {
+ tests := []struct {
+ name string
+ key backend.Key
+ prefix backend.Key
+ expected backend.Key
+ }{
+ {
+ name: "empty prefix is noop",
+ key: backend.NewKey("a", "b"),
+ expected: backend.NewKey("a", "b"),
+ },
+ {
+ name: "empty key is prefixed",
+ prefix: backend.NewKey("a", "b"),
+ expected: backend.NewKey("a", "b"),
+ },
+ {
+ name: "prefix applied",
+ key: backend.NewKey("a", "b"),
+ prefix: backend.NewKey("1", "2"),
+ expected: backend.NewKey("1", "2", "a", "b"),
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ prefixed := test.key.PrependPrefix(test.prefix)
+ assert.Equal(t, test.expected, prefixed)
+ })
+ }
+}
+
+func TestKeyCompare(t *testing.T) {
+ tests := []struct {
+ name string
+ key backend.Key
+ other backend.Key
+ expected int
+ }{
+ {
+ name: "equal keys",
+ key: backend.NewKey("a", "b", "c"),
+ other: backend.NewKey("a", "b", "c"),
+ expected: 0,
+ },
+ {
+ name: "less",
+ key: backend.NewKey("a", "b", "c"),
+ other: backend.NewKey("a", "b", "d"),
+ expected: -1,
+ },
+ {
+ name: "greater",
+ key: backend.NewKey("d", "b", "c"),
+ other: backend.NewKey("a", "b"),
+ expected: 1,
+ },
+ {
+ name: "empty key is always less",
+ other: backend.NewKey("a", "b"),
+ expected: -1,
+ },
+ {
+ name: "key is always greater than empty",
+ key: backend.NewKey("a", "b"),
+ expected: 1,
+ },
+ {
+ name: "empty keys are equal",
+ expected: 0,
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ assert.Equal(t, test.expected, test.key.Compare(test.other))
+ })
+ }
+}
diff --git a/lib/backend/lite/lite.go b/lib/backend/lite/lite.go
index aabb94fdcda22..5fca0e922387f 100644
--- a/lib/backend/lite/lite.go
+++ b/lib/backend/lite/lite.go
@@ -437,7 +437,7 @@ func (l *Backend) CompareAndSwap(ctx context.Context, expected backend.Item, rep
if len(replaceWith.Key) == 0 {
return nil, trace.BadParameter("missing parameter Key")
}
- if !bytes.Equal(expected.Key, replaceWith.Key) {
+ if expected.Key.Compare(replaceWith.Key) != 0 {
return nil, trace.BadParameter("expected and replaceWith keys should match")
}
diff --git a/lib/backend/memory/item.go b/lib/backend/memory/item.go
index af4f3df8dea18..3713508855442 100644
--- a/lib/backend/memory/item.go
+++ b/lib/backend/memory/item.go
@@ -19,8 +19,6 @@
package memory
import (
- "bytes"
-
"github.com/google/btree"
"github.com/gravitational/teleport/lib/backend"
@@ -39,7 +37,7 @@ type btreeItem struct {
func (i *btreeItem) Less(iother btree.Item) bool {
switch other := iother.(type) {
case *btreeItem:
- return bytes.Compare(i.Key, other.Key) < 0
+ return i.Key.Compare(other.Key) < 0
case *prefixItem:
return !iother.Less(i)
default:
@@ -56,5 +54,5 @@ type prefixItem struct {
// Less is used for Btree operations
func (p *prefixItem) Less(iother btree.Item) bool {
other := iother.(*btreeItem)
- return !bytes.HasPrefix(other.Key, p.prefix)
+ return !other.Key.HasPrefix(p.prefix)
}
diff --git a/lib/backend/memory/memory.go b/lib/backend/memory/memory.go
index 86046f0a63b2d..d96992b5dce47 100644
--- a/lib/backend/memory/memory.go
+++ b/lib/backend/memory/memory.go
@@ -359,7 +359,7 @@ func (m *Memory) CompareAndSwap(ctx context.Context, expected backend.Item, repl
if len(replaceWith.Key) == 0 {
return nil, trace.BadParameter("missing parameter Key")
}
- if !bytes.Equal(expected.Key, replaceWith.Key) {
+ if expected.Key.Compare(replaceWith.Key) != 0 {
return nil, trace.BadParameter("expected and replaceWith keys should match")
}
m.Lock()
diff --git a/lib/backend/pgbk/atomicwrite.go b/lib/backend/pgbk/atomicwrite.go
index 78d08e0b648b8..aaaada25d68a9 100644
--- a/lib/backend/pgbk/atomicwrite.go
+++ b/lib/backend/pgbk/atomicwrite.go
@@ -50,12 +50,12 @@ func (b *Backend) AtomicWrite(ctx context.Context, condacts []backend.Conditiona
case backend.KindExists:
condBatchItems = append(condBatchItems, batchItem{
"SELECT EXISTS (SELECT FROM kv WHERE key = $1 AND (expires IS NULL OR expires >= now()))",
- []any{nonNil(ca.Key)},
+ []any{nonNilKey(ca.Key)},
})
case backend.KindNotExists:
condBatchItems = append(condBatchItems, batchItem{
"SELECT NOT EXISTS (SELECT FROM kv WHERE key = $1 AND (expires IS NULL OR expires >= now()))",
- []any{nonNil(ca.Key)},
+ []any{nonNilKey(ca.Key)},
})
case backend.KindRevision:
expectedRevision, ok := revisionFromString(ca.Condition.Revision)
@@ -64,7 +64,7 @@ func (b *Backend) AtomicWrite(ctx context.Context, condacts []backend.Conditiona
}
condBatchItems = append(condBatchItems, batchItem{
"SELECT EXISTS (SELECT FROM kv WHERE key = $1 AND revision = $2 AND (expires IS NULL OR expires >= now()))",
- []any{nonNil(ca.Key), expectedRevision},
+ []any{nonNilKey(ca.Key), expectedRevision},
})
default:
// condacts was already checked for validity
@@ -80,12 +80,12 @@ func (b *Backend) AtomicWrite(ctx context.Context, condacts []backend.Conditiona
"INSERT INTO kv (key, value, expires, revision) VALUES ($1, $2, $3, $4)" +
" ON CONFLICT (key) DO UPDATE SET" +
" value = excluded.value, expires = excluded.expires, revision = excluded.revision",
- []any{nonNil(ca.Key), nonNil(ca.Action.Item.Value), zeronull.Timestamptz(ca.Action.Item.Expires.UTC()), newRevision},
+ []any{nonNilKey(ca.Key), nonNil(ca.Action.Item.Value), zeronull.Timestamptz(ca.Action.Item.Expires.UTC()), newRevision},
})
case backend.KindDelete:
actBatchItems = append(actBatchItems, batchItem{
"DELETE FROM kv WHERE kv.key = $1 AND (kv.expires IS NULL OR kv.expires > now())",
- []any{nonNil(ca.Key)},
+ []any{nonNilKey(ca.Key)},
})
default:
// condacts was already checked for validity
diff --git a/lib/backend/pgbk/pgbk.go b/lib/backend/pgbk/pgbk.go
index 8d5489d60b9ca..6605113921b65 100644
--- a/lib/backend/pgbk/pgbk.go
+++ b/lib/backend/pgbk/pgbk.go
@@ -19,7 +19,6 @@
package pgbk
import (
- "bytes"
"context"
"errors"
"log/slog"
@@ -259,7 +258,7 @@ func (b *Backend) Create(ctx context.Context, i backend.Item) (*backend.Lease, e
" ON CONFLICT (key) DO UPDATE SET"+
" value = excluded.value, expires = excluded.expires, revision = excluded.revision"+
" WHERE kv.expires IS NOT NULL AND kv.expires <= now()",
- nonNil(i.Key), nonNil(i.Value), zeronull.Timestamptz(i.Expires), revision)
+ nonNilKey(i.Key), nonNil(i.Value), zeronull.Timestamptz(i.Expires), revision)
if err != nil {
return false, trace.Wrap(err)
}
@@ -286,7 +285,7 @@ func (b *Backend) Put(ctx context.Context, i backend.Item) (*backend.Lease, erro
"INSERT INTO kv (key, value, expires, revision) VALUES ($1, $2, $3, $4)"+
" ON CONFLICT (key) DO UPDATE SET"+
" value = excluded.value, expires = excluded.expires, revision = excluded.revision",
- nonNil(i.Key), nonNil(i.Value), zeronull.Timestamptz(i.Expires), revision)
+ nonNilKey(i.Key), nonNil(i.Value), zeronull.Timestamptz(i.Expires), revision)
return struct{}{}, trace.Wrap(err)
}); err != nil {
return nil, trace.Wrap(err)
@@ -298,7 +297,7 @@ func (b *Backend) Put(ctx context.Context, i backend.Item) (*backend.Lease, erro
// CompareAndSwap implements [backend.Backend].
func (b *Backend) CompareAndSwap(ctx context.Context, expected, replaceWith backend.Item) (*backend.Lease, error) {
- if !bytes.Equal(expected.Key, replaceWith.Key) {
+ if expected.Key.Compare(replaceWith.Key) != 0 {
return nil, trace.BadParameter("expected and replaceWith keys should match")
}
@@ -309,7 +308,7 @@ func (b *Backend) CompareAndSwap(ctx context.Context, expected, replaceWith back
"UPDATE kv SET value = $1, expires = $2, revision = $3"+
" WHERE kv.key = $4 AND kv.value = $5 AND (kv.expires IS NULL OR kv.expires > now())",
nonNil(replaceWith.Value), zeronull.Timestamptz(replaceWith.Expires), revision,
- nonNil(replaceWith.Key), nonNil(expected.Value))
+ nonNilKey(replaceWith.Key), nonNil(expected.Value))
if err != nil {
return false, trace.Wrap(err)
}
@@ -335,7 +334,7 @@ func (b *Backend) Update(ctx context.Context, i backend.Item) (*backend.Lease, e
tag, err := b.pool.Exec(ctx,
"UPDATE kv SET value = $1, expires = $2, revision = $3"+
" WHERE kv.key = $4 AND (kv.expires IS NULL OR kv.expires > now())",
- nonNil(i.Value), zeronull.Timestamptz(i.Expires), revision, nonNil(i.Key))
+ nonNil(i.Value), zeronull.Timestamptz(i.Expires), revision, nonNilKey(i.Key))
if err != nil {
return false, trace.Wrap(err)
}
@@ -367,7 +366,7 @@ func (b *Backend) ConditionalUpdate(ctx context.Context, i backend.Item) (*backe
"WHERE kv.key = $4 AND kv.revision = $5 AND "+
"(kv.expires IS NULL OR kv.expires > now())",
nonNil(i.Value), zeronull.Timestamptz(i.Expires), newRevision,
- nonNil(i.Key), expectedRevision)
+ nonNilKey(i.Key), expectedRevision)
if err != nil {
return false, trace.Wrap(err)
}
@@ -394,7 +393,7 @@ func (b *Backend) Get(ctx context.Context, key backend.Key) (*backend.Item, erro
var item *backend.Item
batch.Queue("SELECT kv.value, kv.expires, kv.revision FROM kv"+
- " WHERE kv.key = $1 AND (kv.expires IS NULL OR kv.expires > now())", nonNil(key),
+ " WHERE kv.key = $1 AND (kv.expires IS NULL OR kv.expires > now())", nonNilKey(key),
).QueryRow(func(row pgx.Row) error {
var value []byte
var expires time.Time
@@ -450,7 +449,7 @@ func (b *Backend) GetRange(ctx context.Context, startKey, endKey backend.Key, li
"SELECT kv.key, kv.value, kv.expires, kv.revision FROM kv"+
" WHERE kv.key BETWEEN $1 AND $2 AND (kv.expires IS NULL OR kv.expires > now())"+
" ORDER BY kv.key LIMIT $3",
- nonNil(startKey), nonNil(endKey), limit,
+ nonNilKey(startKey), nonNilKey(endKey), limit,
).Query(func(rows pgx.Rows) error {
var err error
items, err = pgx.CollectRows(rows, func(row pgx.CollectableRow) (backend.Item, error) {
@@ -488,7 +487,7 @@ func (b *Backend) GetRange(ctx context.Context, startKey, endKey backend.Key, li
func (b *Backend) Delete(ctx context.Context, key backend.Key) error {
deleted, err := pgcommon.Retry(ctx, b.log, func() (bool, error) {
tag, err := b.pool.Exec(ctx,
- "DELETE FROM kv WHERE kv.key = $1 AND (kv.expires IS NULL OR kv.expires > now())", nonNil(key))
+ "DELETE FROM kv WHERE kv.key = $1 AND (kv.expires IS NULL OR kv.expires > now())", nonNilKey(key))
if err != nil {
return false, trace.Wrap(err)
}
@@ -514,7 +513,7 @@ func (b *Backend) ConditionalDelete(ctx context.Context, key backend.Key, rev st
tag, err := b.pool.Exec(ctx,
"DELETE FROM kv WHERE kv.key = $1 AND kv.revision = $2 AND "+
"(kv.expires IS NULL OR kv.expires > now())",
- nonNil(key), expectedRevision)
+ nonNilKey(key), expectedRevision)
if err != nil {
return false, trace.Wrap(err)
}
@@ -540,7 +539,7 @@ func (b *Backend) DeleteRange(ctx context.Context, startKey, endKey backend.Key)
if _, err := pgcommon.Retry(ctx, b.log, func() (struct{}, error) {
_, err := b.pool.Exec(ctx,
"DELETE FROM kv WHERE kv.key BETWEEN $1 AND $2",
- nonNil(startKey), nonNil(endKey),
+ nonNilKey(startKey), nonNilKey(endKey),
)
return struct{}{}, trace.Wrap(err)
}); err != nil {
@@ -557,7 +556,7 @@ func (b *Backend) KeepAlive(ctx context.Context, lease backend.Lease, expires ti
tag, err := b.pool.Exec(ctx,
"UPDATE kv SET expires = $1, revision = $2"+
" WHERE kv.key = $3 AND (kv.expires IS NULL OR kv.expires > now())",
- zeronull.Timestamptz(expires.UTC()), revision, nonNil(lease.Key))
+ zeronull.Timestamptz(expires.UTC()), revision, nonNilKey(lease.Key))
if err != nil {
return false, trace.Wrap(err)
}
diff --git a/lib/backend/pgbk/utils.go b/lib/backend/pgbk/utils.go
index a3e8af54d6a71..a22a5eca4700c 100644
--- a/lib/backend/pgbk/utils.go
+++ b/lib/backend/pgbk/utils.go
@@ -22,6 +22,8 @@ import (
"encoding/binary"
"github.com/google/uuid"
+
+ "github.com/gravitational/teleport/lib/backend"
)
// revision is transparently converted to and from Postgres UUIDs.
@@ -56,6 +58,15 @@ func idFromRevision(revision revision) int64 {
return int64(u)
}
+// nonNilKey replaces an empty key with a non-nil one.
+func nonNilKey(b backend.Key) []byte {
+ if b == nil {
+ return []byte{}
+ }
+
+ return []byte(b.String())
+}
+
// nonNil replaces a nil slice with an empty, non-nil one.
func nonNil(b []byte) []byte {
if b == nil {
diff --git a/lib/services/local/access.go b/lib/services/local/access.go
index b646df85c5bc7..d7bc37716942a 100644
--- a/lib/services/local/access.go
+++ b/lib/services/local/access.go
@@ -19,7 +19,6 @@
package local
import (
- "bytes"
"context"
"strings"
"time"
@@ -113,7 +112,7 @@ func (s *AccessService) ListRoles(ctx context.Context, req *proto.ListRolesReque
return true, nil
}
- if !bytes.HasSuffix(item.Key, []byte(paramsPrefix)) {
+ if !item.Key.HasSuffix(backend.Key(paramsPrefix)) {
// Item represents a different resource type in the
// same namespace.
continue
@@ -393,13 +392,13 @@ func (s *AccessService) ReplaceRemoteLocks(ctx context.Context, clusterName stri
ID: lock.GetResourceID(),
Revision: rev,
}
- newRemoteLocksToStore[string(item.Key)] = item
+ newRemoteLocksToStore[item.Key.String()] = item
}
for _, origLockItem := range origRemoteLocks.Items {
// If one of the new remote locks to store is already known,
// perform a CompareAndSwap.
- key := string(origLockItem.Key)
+ key := origLockItem.Key.String()
if newLockItem, ok := newRemoteLocksToStore[key]; ok {
if _, err := s.CompareAndSwap(ctx, origLockItem, newLockItem); err != nil {
return trace.Wrap(err)
diff --git a/lib/services/local/dynamic_access.go b/lib/services/local/dynamic_access.go
index ebb528ce0f965..f77cd316c44e2 100644
--- a/lib/services/local/dynamic_access.go
+++ b/lib/services/local/dynamic_access.go
@@ -19,7 +19,6 @@
package local
import (
- "bytes"
"context"
"slices"
"time"
@@ -261,7 +260,7 @@ func (s *DynamicAccessService) GetAccessRequests(ctx context.Context, filter typ
}
var requests []types.AccessRequest
for _, item := range result.Items {
- if !bytes.HasSuffix(item.Key, []byte(paramsPrefix)) {
+ if !item.Key.HasSuffix(backend.Key(paramsPrefix)) {
// Item represents a different resource type in the
// same namespace.
continue
@@ -341,7 +340,7 @@ func (s *DynamicAccessService) ListAccessRequests(ctx context.Context, req *prot
return true, nil
}
- if !bytes.HasSuffix(item.Key, []byte(paramsPrefix)) {
+ if !item.Key.HasSuffix(backend.Key(paramsPrefix)) {
// Item represents a different resource type in the
// same namespace.
continue
diff --git a/lib/services/local/events.go b/lib/services/local/events.go
index 8fc889e7b7c2a..00bf810c53aae 100644
--- a/lib/services/local/events.go
+++ b/lib/services/local/events.go
@@ -19,9 +19,7 @@
package local
import (
- "bytes"
"context"
- "strings"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
@@ -372,7 +370,7 @@ func (p baseParser) prefixes() []backend.Key {
func (p baseParser) match(key backend.Key) bool {
for _, prefix := range p.matchPrefixes {
- if bytes.HasPrefix(key, prefix) {
+ if key.HasPrefix(prefix) {
return true
}
}
@@ -719,8 +717,8 @@ func (p *namespaceParser) match(key backend.Key) bool {
// namespaces are stored under key '/namespaces//params'
// and this code matches similar pattern
return p.baseParser.match(key) &&
- bytes.HasSuffix(key, []byte(paramsPrefix)) &&
- bytes.Count(key, []byte{backend.Separator}) == 3
+ key.HasSuffix(backend.Key(paramsPrefix)) &&
+ len(key.Components()) == 3
}
func (p *namespaceParser) parse(event backend.Event) (types.Resource, error) {
@@ -794,10 +792,10 @@ func (p *accessRequestParser) prefixes() []backend.Key {
}
func (p *accessRequestParser) match(key backend.Key) bool {
- if !bytes.HasPrefix(key, p.matchPrefix) {
+ if !key.HasPrefix(p.matchPrefix) {
return false
}
- if !bytes.HasSuffix(key, p.matchSuffix) {
+ if !key.HasSuffix(p.matchSuffix) {
return false
}
return true
@@ -835,8 +833,8 @@ func (p *userParser) match(key backend.Key) bool {
// users are stored under key '/web/users//params'
// and this code matches similar pattern
return p.baseParser.match(key) &&
- bytes.HasSuffix(key, []byte(paramsPrefix)) &&
- bytes.Count(key, []byte{backend.Separator}) == 4
+ key.HasSuffix(backend.Key(paramsPrefix)) &&
+ len(key.Components()) == 4
}
func (p *userParser) parse(event backend.Event) (types.Resource, error) {
@@ -1353,7 +1351,7 @@ func (p *remoteClusterParser) prefixes() []backend.Key {
}
func (p *remoteClusterParser) match(key backend.Key) bool {
- return bytes.HasPrefix(key, p.matchPrefix)
+ return key.HasPrefix(p.matchPrefix)
}
func (p *remoteClusterParser) parse(event backend.Event) (types.Resource, error) {
@@ -1416,7 +1414,7 @@ func (p *networkRestrictionsParser) prefixes() []backend.Key {
}
func (p *networkRestrictionsParser) match(key backend.Key) bool {
- return bytes.HasPrefix(key, p.matchPrefix)
+ return key.HasPrefix(p.matchPrefix)
}
func (p *networkRestrictionsParser) parse(event backend.Event) (types.Resource, error) {
@@ -1964,25 +1962,19 @@ type kubeWaitingContainerParser struct {
func (p *kubeWaitingContainerParser) parse(event backend.Event) (types.Resource, error) {
switch event.Type {
case types.OpDelete:
- // remove the first separator so no separated parts should be
- // empty strings
- key := string(event.Item.Key)
- if len(key) > 0 && key[0] == backend.Separator {
- key = key[1:]
- }
- parts := strings.Split(key, string(backend.Separator))
+ parts := event.Item.Key.Components()
if len(parts) != 6 {
return nil, trace.BadParameter("malformed key for %s event: %s", types.KindKubeWaitingContainer, event.Item.Key)
}
resource, err := kubewaitingcontainer.NewKubeWaitingContainer(
- parts[5],
+ string(parts[5]),
&kubewaitingcontainerpb.KubernetesWaitingContainerSpec{
- Username: parts[1],
- Cluster: parts[2],
- Namespace: parts[3],
- PodName: parts[4],
- ContainerName: parts[5],
+ Username: string(parts[1]),
+ Cluster: string(parts[2]),
+ Namespace: string(parts[3]),
+ PodName: string(parts[4]),
+ ContainerName: string(parts[5]),
Patch: []byte("{}"), // default to empty patch. It doesn't matter for delete ops.
PatchType: kubewaitingcontainer.JSONPatchType, // default to JSON patch. It doesn't matter for delete ops.
},
@@ -2128,7 +2120,7 @@ func resourceHeader(event backend.Event, kind, version string, offset int) (type
Kind: kind,
Version: version,
Metadata: types.Metadata{
- Name: string(name),
+ Name: name,
Namespace: apidefaults.Namespace,
},
}, nil
@@ -2144,7 +2136,7 @@ func resourceHeaderWithTemplate(event backend.Event, hdr types.ResourceHeader, o
SubKind: hdr.SubKind,
Version: hdr.Version,
Metadata: types.Metadata{
- Name: string(name),
+ Name: name,
Namespace: apidefaults.Namespace,
},
}, nil
@@ -2208,7 +2200,7 @@ func (p *deviceParser) parse(event backend.Event) (types.Resource, error) {
Kind: types.KindDevice,
Version: types.V1,
Metadata: types.Metadata{
- Name: string(name),
+ Name: name,
},
},
}
@@ -2335,19 +2327,19 @@ type EventMatcher interface {
Match(types.Event) (types.Resource, error)
}
-// base returns last element delimited by separator, index is
-// is an index of the key part to get counting from the end
-func base(key backend.Key, offset int) ([]byte, error) {
- parts := bytes.Split(key, []byte{backend.Separator})
+// base returns the key component that is offset
+// components before the last component.
+func base(key backend.Key, offset int) (string, error) {
+ parts := key.Components()
if len(parts) < offset+1 {
- return nil, trace.NotFound("failed parsing %v", string(key))
+ return "", trace.NotFound("failed parsing %v", string(key))
}
- return parts[len(parts)-offset-1], nil
+ return string(parts[len(parts)-offset-1]), nil
}
// baseTwoKeys returns two last keys
func baseTwoKeys(key backend.Key) (string, string, error) {
- parts := bytes.Split(key, []byte{backend.Separator})
+ parts := key.Components()
if len(parts) < 2 {
return "", "", trace.NotFound("failed parsing %v", string(key))
}
diff --git a/lib/services/local/generic/generic.go b/lib/services/local/generic/generic.go
index f1c8a984d3501..a0d799b2189e0 100644
--- a/lib/services/local/generic/generic.go
+++ b/lib/services/local/generic/generic.go
@@ -212,7 +212,7 @@ func (s *Service[T]) listResourcesReturnNextResourceWithKey(ctx context.Context,
next = &out[pageSize]
// Truncate the last item that was used to determine next row existence.
out = out[:pageSize]
- nextKey = trimLastKey(string(lastKey), s.backendPrefix)
+ nextKey = trimLastKey(lastKey.String(), s.backendPrefix)
}
return out, next, nextKey, nil
@@ -259,7 +259,7 @@ func (s *Service[T]) ListResourcesWithFilter(ctx context.Context, pageSize int,
var nextKey string
if len(resources) > pageSize {
- nextKey = trimLastKey(string(lastKey), s.backendPrefix)
+ nextKey = trimLastKey(lastKey.String(), s.backendPrefix)
// Truncate the last item that was used to determine next row existence.
resources = resources[:pageSize]
}
diff --git a/lib/services/local/plugin_data.go b/lib/services/local/plugin_data.go
index 2f8404c9bc378..21dad758c956e 100644
--- a/lib/services/local/plugin_data.go
+++ b/lib/services/local/plugin_data.go
@@ -19,7 +19,6 @@
package local
import (
- "bytes"
"context"
"time"
@@ -96,7 +95,7 @@ func (p *PluginDataService) getPluginData(ctx context.Context, filter types.Plug
}
var matches []types.PluginData
for _, item := range result.Items {
- if !bytes.HasSuffix(item.Key, []byte(paramsPrefix)) {
+ if !item.Key.HasSuffix(backend.Key(paramsPrefix)) {
// Item represents a different resource type in the
// same namespace.
continue
diff --git a/lib/services/local/presence.go b/lib/services/local/presence.go
index 12c5a6f8e4271..b9f879a282549 100644
--- a/lib/services/local/presence.go
+++ b/lib/services/local/presence.go
@@ -19,7 +19,6 @@
package local
import (
- "bytes"
"context"
"sort"
"time"
@@ -79,7 +78,7 @@ func (s *PresenceService) GetNamespaces() ([]types.Namespace, error) {
}
out := make([]types.Namespace, 0, len(result.Items))
for _, item := range result.Items {
- if !bytes.HasSuffix(item.Key, backend.Key(paramsPrefix)) {
+ if !item.Key.HasSuffix(backend.Key(paramsPrefix)) {
continue
}
ns, err := services.UnmarshalNamespace(
diff --git a/lib/services/local/resource.go b/lib/services/local/resource.go
index 1fb78b2da07d8..a373b76ee36ff 100644
--- a/lib/services/local/resource.go
+++ b/lib/services/local/resource.go
@@ -22,6 +22,7 @@ import (
"context"
"encoding/json"
"strings"
+ "unicode/utf8"
"github.com/gravitational/trace"
@@ -440,20 +441,21 @@ func itemFromLock(l types.Lock) (*backend.Item, error) {
// has order N cost.
// fullUsersPrefix is the entire string preceding the name of a user in a key
-var fullUsersPrefix = string(backend.NewKey(webPrefix, usersPrefix)) + "/"
+var fullUsersPrefix = backend.ExactKey(webPrefix, usersPrefix)
// splitUsernameAndSuffix is a helper for extracting usernames and suffixes from
// backend key values.
-func splitUsernameAndSuffix(key string) (name string, suffix string, err error) {
- if !strings.HasPrefix(key, fullUsersPrefix) {
+func splitUsernameAndSuffix(key backend.Key) (name string, suffix string, err error) {
+ if !key.HasPrefix(fullUsersPrefix) {
return "", "", trace.BadParameter("expected format '%s//', got '%s'", fullUsersPrefix, key)
}
- key = strings.TrimPrefix(key, fullUsersPrefix)
- idx := strings.Index(key, "/")
- if idx < 1 || idx >= len(key) {
+ k := key.TrimPrefix(fullUsersPrefix)
+
+ components := k.Components()
+ if len(components) < 2 {
return "", "", trace.BadParameter("expected format /, got %q", key)
}
- return key[:idx], key[idx+1:], nil
+ return string(components[0]), k.String()[len(components[0])+utf8.RuneLen(backend.Separator):], nil
}
// collectUserItems handles the case where multiple items pertain to the same user resource.
@@ -462,12 +464,11 @@ func splitUsernameAndSuffix(key string) (name string, suffix string, err error)
func collectUserItems(items []backend.Item) (users map[string]userItems, rem []backend.Item, err error) {
users = make(map[string]userItems)
for _, item := range items {
- key := string(item.Key)
- if !strings.HasPrefix(key, fullUsersPrefix) {
+ if !item.Key.HasPrefix(fullUsersPrefix) {
rem = append(rem, item)
continue
}
- name, suffix, err := splitUsernameAndSuffix(key)
+ name, suffix, err := splitUsernameAndSuffix(item.Key)
if err != nil {
return nil, nil, err
}
diff --git a/lib/services/local/trust.go b/lib/services/local/trust.go
index ab02aec40064c..013b40dcbc3aa 100644
--- a/lib/services/local/trust.go
+++ b/lib/services/local/trust.go
@@ -203,7 +203,7 @@ func (s *CA) DeleteCertAuthorities(ctx context.Context, ids ...types.CertAuthID)
if err := id.Check(); err != nil {
return trace.Wrap(err)
}
- for _, key := range [][]byte{activeCAKey(id), inactiveCAKey(id)} {
+ for _, key := range []backend.Key{activeCAKey(id), inactiveCAKey(id)} {
condacts = append(condacts, backend.ConditionalAction{
Key: key,
Condition: backend.Whatever(),
diff --git a/lib/services/local/users.go b/lib/services/local/users.go
index f6a6d69b05af9..9056433554aee 100644
--- a/lib/services/local/users.go
+++ b/lib/services/local/users.go
@@ -171,7 +171,7 @@ func (s *IdentityService) ListUsersExt(ctx context.Context, req *userspb.ListUse
// the next user in the list while still allowing listing to operate
// without missing any users.
func nextUserToken(user types.User) string {
- return string(backend.RangeEnd(backend.ExactKey(user.GetName())))[utf8.RuneLen(backend.Separator):]
+ return backend.RangeEnd(backend.ExactKey(user.GetName())).String()[utf8.RuneLen(backend.Separator):]
}
// streamUsersWithSecrets is a helper that converts a stream of backend items over the user key range into a stream
@@ -185,7 +185,7 @@ func (s *IdentityService) streamUsersWithSecrets(itemStream stream.Stream[backen
var current collector
collectorStream := stream.FilterMap(itemStream, func(item backend.Item) (collector, bool) {
- name, suffix, err := splitUsernameAndSuffix(string(item.Key))
+ name, suffix, err := splitUsernameAndSuffix(item.Key)
if err != nil {
s.log.Warnf("Failed to extract name/suffix for user item at %q: %v", item.Key, err)
return collector{}, false
@@ -243,7 +243,7 @@ func (s *IdentityService) streamUsersWithSecrets(itemStream stream.Stream[backen
func (s *IdentityService) streamUsersWithoutSecrets(itemStream stream.Stream[backend.Item]) stream.Stream[*types.UserV2] {
suffix := backend.Key(paramsPrefix)
userStream := stream.FilterMap(itemStream, func(item backend.Item) (*types.UserV2, bool) {
- if !bytes.HasSuffix(item.Key, suffix) {
+ if !item.Key.HasSuffix(suffix) {
return nil, false
}
@@ -271,7 +271,7 @@ func (s *IdentityService) GetUsers(ctx context.Context, withSecrets bool) ([]typ
}
var out []types.User
for _, item := range result.Items {
- if !bytes.HasSuffix(item.Key, backend.Key(paramsPrefix)) {
+ if !item.Key.HasSuffix(backend.Key(paramsPrefix)) {
continue
}
u, err := services.UnmarshalUser(
@@ -609,8 +609,8 @@ func (s *IdentityService) getUserWithSecrets(ctx context.Context, user string) (
var items userItems
for _, item := range result.Items {
- suffix := bytes.TrimPrefix(item.Key, startKey)
- items.Set(string(suffix), item) // Result of Set i
+ suffix := item.Key.TrimPrefix(startKey)
+ items.Set(suffix.String(), item) // Result of Set i
}
u, err := userFromUserItems(user, items)
diff --git a/lib/services/local/usertoken.go b/lib/services/local/usertoken.go
index 5a56adb739636..948206dbbac16 100644
--- a/lib/services/local/usertoken.go
+++ b/lib/services/local/usertoken.go
@@ -19,7 +19,6 @@
package local
import (
- "bytes"
"context"
"github.com/gravitational/trace"
@@ -39,7 +38,7 @@ func (s *IdentityService) GetUserTokens(ctx context.Context) ([]types.UserToken,
var tokens []types.UserToken
for _, item := range result.Items {
- if !bytes.HasSuffix(item.Key, []byte(paramsPrefix)) {
+ if !item.Key.HasSuffix(backend.Key(paramsPrefix)) {
continue
}
diff --git a/lib/services/unified_resource.go b/lib/services/unified_resource.go
index 1410612dfc0ce..4b39d62285e18 100644
--- a/lib/services/unified_resource.go
+++ b/lib/services/unified_resource.go
@@ -19,7 +19,6 @@
package services
import (
- "bytes"
"context"
"strings"
"sync"
@@ -136,7 +135,7 @@ func (c *UnifiedResourceCache) putLocked(resource resource) {
// from those trees before adding a new one. This can happen
// when a node's hostname changes
oldSortKey := makeResourceSortKey(oldResource)
- if string(oldSortKey.byName) != string(sortKey.byName) {
+ if oldSortKey.byName.Compare(sortKey.byName) != 0 {
c.deleteSortKey(oldSortKey)
}
}
@@ -159,10 +158,10 @@ func putResources[T resource](cache *UnifiedResourceCache, resources []T) {
func (c *UnifiedResourceCache) deleteSortKey(sortKey resourceSortKey) error {
if _, ok := c.nameTree.Delete(&item{Key: sortKey.byName}); !ok {
- return trace.NotFound("key %q is not found in unified cache name sort tree", string(sortKey.byName))
+ return trace.NotFound("key %q is not found in unified cache name sort tree", sortKey.byName.String())
}
if _, ok := c.typeTree.Delete(&item{Key: sortKey.byType}); !ok {
- return trace.NotFound("key %q is not found in unified cache type sort tree", string(sortKey.byType))
+ return trace.NotFound("key %q is not found in unified cache type sort tree", sortKey.byType.String())
}
return nil
}
@@ -240,7 +239,7 @@ func (c *UnifiedResourceCache) getRange(ctx context.Context, startKey backend.Ke
// do we have all we need? set nextKey and stop iterating
// we do this after the matchFn to make sure they have access to the "next" node
if req.Limit > 0 && len(res) >= int(req.Limit) {
- nextKey = string(item.Key)
+ nextKey = item.Key.String()
return false
}
res = append(res, resourceFromMap)
@@ -690,7 +689,7 @@ func (c *UnifiedResourceCache) defineCollectorAsInitialized() {
func (i *item) Less(iother btree.Item) bool {
switch other := iother.(type) {
case *item:
- return bytes.Compare(i.Key, other.Key) < 0
+ return i.Key.Compare(other.Key) < 0
case *prefixItem:
return !iother.Less(i)
default:
@@ -707,7 +706,7 @@ type prefixItem struct {
// Less is used for Btree operations
func (p *prefixItem) Less(iother btree.Item) bool {
other := iother.(*item)
- return !bytes.HasPrefix(other.Key, p.prefix)
+ return !other.Key.HasPrefix(p.prefix)
}
type resource interface {