mirror of
https://github.com/rancher/steve.git
synced 2025-06-30 08:42:06 +00:00
Previous SQLite-related code used context.Background() and context.TODO() because it was not developed with context awareness. This commit propagates the main Steve context so that it can be used when interacting with SQL context-aware functions. This PR removes all production-code use of context.Background() and context.TODO() and replaces test-code use of TODO with Background. Contributes to rancher/rancher#47825
264 lines
7.8 KiB
Go
264 lines
7.8 KiB
Go
package informer
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/rancher/steve/pkg/sqlcache/db"
|
|
"github.com/rancher/steve/pkg/sqlcache/db/transaction"
|
|
"k8s.io/client-go/tools/cache"
|
|
)
|
|
|
|
const (
|
|
selectQueryFmt = `
|
|
SELECT object, objectnonce, dekid FROM "%[1]s"
|
|
WHERE key IN (
|
|
SELECT key FROM "%[1]s_indices"
|
|
WHERE name = ? AND value IN (?%s)
|
|
)
|
|
`
|
|
createTableFmt = `CREATE TABLE IF NOT EXISTS "%[1]s_indices" (
|
|
name TEXT NOT NULL,
|
|
value TEXT NOT NULL,
|
|
key TEXT NOT NULL REFERENCES "%[1]s"(key) ON DELETE CASCADE,
|
|
PRIMARY KEY (name, value, key)
|
|
)`
|
|
createIndexFmt = `CREATE INDEX IF NOT EXISTS "%[1]s_indices_index" ON "%[1]s_indices"(name, value)`
|
|
|
|
deleteIndicesFmt = `DELETE FROM "%s_indices" WHERE key = ?`
|
|
addIndexFmt = `INSERT INTO "%s_indices" (name, value, key) VALUES (?, ?, ?) ON CONFLICT DO NOTHING`
|
|
listByIndexFmt = `SELECT object, objectnonce, dekid FROM "%[1]s"
|
|
WHERE key IN (
|
|
SELECT key FROM "%[1]s_indices"
|
|
WHERE name = ? AND value = ?
|
|
)`
|
|
listKeyByIndexFmt = `SELECT DISTINCT key FROM "%s_indices" WHERE name = ? AND value = ?`
|
|
listIndexValuesFmt = `SELECT DISTINCT value FROM "%s_indices" WHERE name = ?`
|
|
)
|
|
|
|
// Indexer is a SQLite-backed cache.Indexer which builds upon Store adding an index table
|
|
type Indexer struct {
|
|
ctx context.Context
|
|
|
|
Store
|
|
indexers cache.Indexers
|
|
indexersLock sync.RWMutex
|
|
|
|
deleteIndicesQuery string
|
|
addIndexQuery string
|
|
listByIndexQuery string
|
|
listKeysByIndexQuery string
|
|
listIndexValuesQuery string
|
|
|
|
deleteIndicesStmt *sql.Stmt
|
|
addIndexStmt *sql.Stmt
|
|
listByIndexStmt *sql.Stmt
|
|
listKeysByIndexStmt *sql.Stmt
|
|
listIndexValuesStmt *sql.Stmt
|
|
}
|
|
|
|
var _ cache.Indexer = (*Indexer)(nil)
|
|
|
|
type Store interface {
|
|
db.Client
|
|
cache.Store
|
|
|
|
GetByKey(key string) (item any, exists bool, err error)
|
|
GetName() string
|
|
RegisterAfterUpsert(f func(key string, obj any, tx transaction.Client) error)
|
|
RegisterAfterDelete(f func(key string, tx transaction.Client) error)
|
|
GetShouldEncrypt() bool
|
|
GetType() reflect.Type
|
|
}
|
|
|
|
// NewIndexer returns a cache.Indexer backed by SQLite for objects of the given example type
|
|
func NewIndexer(ctx context.Context, indexers cache.Indexers, s Store) (*Indexer, error) {
|
|
dbName := db.Sanitize(s.GetName())
|
|
|
|
err := s.WithTransaction(ctx, true, func(tx transaction.Client) error {
|
|
createTableQuery := fmt.Sprintf(createTableFmt, dbName)
|
|
_, err := tx.Exec(createTableQuery)
|
|
if err != nil {
|
|
return &db.QueryError{QueryString: createTableQuery, Err: err}
|
|
}
|
|
createIndexQuery := fmt.Sprintf(createIndexFmt, dbName)
|
|
_, err = tx.Exec(createIndexQuery)
|
|
if err != nil {
|
|
return &db.QueryError{QueryString: createIndexQuery, Err: err}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
i := &Indexer{
|
|
ctx: ctx,
|
|
Store: s,
|
|
indexers: indexers,
|
|
}
|
|
i.RegisterAfterUpsert(i.AfterUpsert)
|
|
|
|
i.deleteIndicesQuery = fmt.Sprintf(deleteIndicesFmt, db.Sanitize(s.GetName()))
|
|
i.addIndexQuery = fmt.Sprintf(addIndexFmt, db.Sanitize(s.GetName()))
|
|
i.listByIndexQuery = fmt.Sprintf(listByIndexFmt, db.Sanitize(s.GetName()))
|
|
i.listKeysByIndexQuery = fmt.Sprintf(listKeyByIndexFmt, db.Sanitize(s.GetName()))
|
|
i.listIndexValuesQuery = fmt.Sprintf(listIndexValuesFmt, db.Sanitize(s.GetName()))
|
|
|
|
i.deleteIndicesStmt = s.Prepare(i.deleteIndicesQuery)
|
|
i.addIndexStmt = s.Prepare(i.addIndexQuery)
|
|
i.listByIndexStmt = s.Prepare(i.listByIndexQuery)
|
|
i.listKeysByIndexStmt = s.Prepare(i.listKeysByIndexQuery)
|
|
i.listIndexValuesStmt = s.Prepare(i.listIndexValuesQuery)
|
|
|
|
return i, nil
|
|
}
|
|
|
|
/* Core methods */
|
|
|
|
// AfterUpsert updates indices of an object
|
|
func (i *Indexer) AfterUpsert(key string, obj any, tx transaction.Client) error {
|
|
// delete all
|
|
_, err := tx.Stmt(i.deleteIndicesStmt).Exec(key)
|
|
if err != nil {
|
|
return &db.QueryError{QueryString: i.deleteIndicesQuery, Err: err}
|
|
}
|
|
|
|
// re-insert all values
|
|
i.indexersLock.RLock()
|
|
defer i.indexersLock.RUnlock()
|
|
for indexName, indexFunc := range i.indexers {
|
|
values, err := indexFunc(obj)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, value := range values {
|
|
_, err = tx.Stmt(i.addIndexStmt).Exec(indexName, value, key)
|
|
if err != nil {
|
|
return &db.QueryError{QueryString: i.addIndexQuery, Err: err}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
/* Satisfy cache.Indexer */
|
|
|
|
// Index returns a list of items that match the given object on the index function
|
|
func (i *Indexer) Index(indexName string, obj any) (result []any, err error) {
|
|
i.indexersLock.RLock()
|
|
defer i.indexersLock.RUnlock()
|
|
indexFunc := i.indexers[indexName]
|
|
if indexFunc == nil {
|
|
return nil, fmt.Errorf("index with name %s does not exist", indexName)
|
|
}
|
|
|
|
values, err := indexFunc(obj)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(values) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// typical case
|
|
if len(values) == 1 {
|
|
return i.ByIndex(indexName, values[0])
|
|
}
|
|
|
|
// atypical case - more than one value to lookup
|
|
// HACK: sql.Statement.Query does not allow to pass slices in as of go 1.19 - create an ad-hoc statement
|
|
query := fmt.Sprintf(selectQueryFmt, db.Sanitize(i.GetName()), strings.Repeat(", ?", len(values)-1))
|
|
stmt := i.Prepare(query)
|
|
|
|
defer func() {
|
|
cerr := i.CloseStmt(stmt)
|
|
if cerr != nil {
|
|
err = errors.Join(err, &db.QueryError{QueryString: query, Err: cerr})
|
|
}
|
|
}()
|
|
// HACK: Query will accept []any but not []string
|
|
params := []any{indexName}
|
|
for _, value := range values {
|
|
params = append(params, value)
|
|
}
|
|
|
|
rows, err := i.QueryForRows(i.ctx, stmt, params...)
|
|
if err != nil {
|
|
return nil, &db.QueryError{QueryString: query, Err: err}
|
|
}
|
|
return i.ReadObjects(rows, i.GetType(), i.GetShouldEncrypt())
|
|
}
|
|
|
|
// ByIndex returns the stored objects whose set of indexed values
|
|
// for the named index includes the given indexed value
|
|
func (i *Indexer) ByIndex(indexName, indexedValue string) ([]any, error) {
|
|
rows, err := i.QueryForRows(i.ctx, i.listByIndexStmt, indexName, indexedValue)
|
|
if err != nil {
|
|
return nil, &db.QueryError{QueryString: i.listByIndexQuery, Err: err}
|
|
}
|
|
return i.ReadObjects(rows, i.GetType(), i.GetShouldEncrypt())
|
|
}
|
|
|
|
// IndexKeys returns a list of the Store keys of the objects whose indexed values in the given index include the given indexed value
|
|
func (i *Indexer) IndexKeys(indexName, indexedValue string) ([]string, error) {
|
|
i.indexersLock.RLock()
|
|
defer i.indexersLock.RUnlock()
|
|
indexFunc := i.indexers[indexName]
|
|
if indexFunc == nil {
|
|
return nil, fmt.Errorf("Index with name %s does not exist", indexName)
|
|
}
|
|
|
|
rows, err := i.QueryForRows(i.ctx, i.listKeysByIndexStmt, indexName, indexedValue)
|
|
if err != nil {
|
|
return nil, &db.QueryError{QueryString: i.listKeysByIndexQuery, Err: err}
|
|
}
|
|
return i.ReadStrings(rows)
|
|
}
|
|
|
|
// ListIndexFuncValues wraps safeListIndexFuncValues and panics in case of I/O errors
|
|
func (i *Indexer) ListIndexFuncValues(name string) []string {
|
|
result, err := i.safeListIndexFuncValues(name)
|
|
if err != nil {
|
|
panic(fmt.Errorf("unexpected error in safeListIndexFuncValues: %w", err))
|
|
}
|
|
return result
|
|
}
|
|
|
|
// safeListIndexFuncValues returns all the indexed values of the given index
|
|
func (i *Indexer) safeListIndexFuncValues(indexName string) ([]string, error) {
|
|
rows, err := i.QueryForRows(i.ctx, i.listIndexValuesStmt, indexName)
|
|
if err != nil {
|
|
return nil, &db.QueryError{QueryString: i.listIndexValuesQuery, Err: err}
|
|
}
|
|
return i.ReadStrings(rows)
|
|
}
|
|
|
|
// GetIndexers returns the indexers
|
|
func (i *Indexer) GetIndexers() cache.Indexers {
|
|
i.indexersLock.RLock()
|
|
defer i.indexersLock.RUnlock()
|
|
return i.indexers
|
|
}
|
|
|
|
// AddIndexers adds more indexers to this Store. If you call this after you already have data
|
|
// in the Store, the results are undefined.
|
|
func (i *Indexer) AddIndexers(newIndexers cache.Indexers) error {
|
|
i.indexersLock.Lock()
|
|
defer i.indexersLock.Unlock()
|
|
if i.indexers == nil {
|
|
i.indexers = make(map[string]cache.IndexFunc)
|
|
}
|
|
for k, v := range newIndexers {
|
|
i.indexers[k] = v
|
|
}
|
|
return nil
|
|
}
|