b747b27afd
registerSchema creates some indirection which is not necessary in this case. newDBSchema can call each of the tables. Enterprise tables can be added from the existing withEnterpriseSchema shim.
394 lines
12 KiB
Go
394 lines
12 KiB
Go
package state
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
|
|
"github.com/hashicorp/go-memdb"
|
|
|
|
"github.com/hashicorp/consul/agent/consul/prepared_query"
|
|
"github.com/hashicorp/consul/agent/structs"
|
|
)
|
|
|
|
// preparedQueriesTableSchema returns a new table schema used for storing
|
|
// prepared queries.
|
|
func preparedQueriesTableSchema() *memdb.TableSchema {
|
|
return &memdb.TableSchema{
|
|
Name: "prepared-queries",
|
|
Indexes: map[string]*memdb.IndexSchema{
|
|
"id": {
|
|
Name: "id",
|
|
AllowMissing: false,
|
|
Unique: true,
|
|
Indexer: &memdb.UUIDFieldIndex{
|
|
Field: "ID",
|
|
},
|
|
},
|
|
"name": {
|
|
Name: "name",
|
|
AllowMissing: true,
|
|
Unique: true,
|
|
Indexer: &memdb.StringFieldIndex{
|
|
Field: "Name",
|
|
Lowercase: true,
|
|
},
|
|
},
|
|
"template": {
|
|
Name: "template",
|
|
AllowMissing: true,
|
|
Unique: true,
|
|
Indexer: &PreparedQueryIndex{},
|
|
},
|
|
"session": {
|
|
Name: "session",
|
|
AllowMissing: true,
|
|
Unique: false,
|
|
Indexer: &memdb.UUIDFieldIndex{
|
|
Field: "Session",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// validUUID is used to check if a given string looks like a UUID
|
|
var validUUID = regexp.MustCompile(`(?i)^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$`)
|
|
|
|
// isUUID returns true if the given string is a valid UUID.
|
|
func isUUID(str string) bool {
|
|
const uuidLen = 36
|
|
if len(str) != uuidLen {
|
|
return false
|
|
}
|
|
|
|
return validUUID.MatchString(str)
|
|
}
|
|
|
|
// queryWrapper is an internal structure that is used to store a query alongside
|
|
// its compiled template, which can be nil.
|
|
type queryWrapper struct {
|
|
// We embed the PreparedQuery structure so that the UUID field indexer
|
|
// can see the ID directly.
|
|
*structs.PreparedQuery
|
|
|
|
// ct is the compiled template, or nil if the query isn't a template. The
|
|
// state store manages this and keeps it up to date every time the query
|
|
// changes.
|
|
ct *prepared_query.CompiledTemplate
|
|
}
|
|
|
|
// toPreparedQuery unwraps the internal form of a prepared query and returns
|
|
// the regular struct.
|
|
func toPreparedQuery(wrapped interface{}) *structs.PreparedQuery {
|
|
if wrapped == nil {
|
|
return nil
|
|
}
|
|
return wrapped.(*queryWrapper).PreparedQuery
|
|
}
|
|
|
|
// PreparedQueries is used to pull all the prepared queries from the snapshot.
|
|
func (s *Snapshot) PreparedQueries() (structs.PreparedQueries, error) {
|
|
queries, err := s.tx.Get("prepared-queries", "id")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var ret structs.PreparedQueries
|
|
for wrapped := queries.Next(); wrapped != nil; wrapped = queries.Next() {
|
|
ret = append(ret, toPreparedQuery(wrapped))
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
// PreparedQuery is used when restoring from a snapshot. For general inserts,
|
|
// use PreparedQuerySet.
|
|
func (s *Restore) PreparedQuery(query *structs.PreparedQuery) error {
|
|
// If this is a template, compile it, otherwise leave the compiled
|
|
// template field nil.
|
|
var ct *prepared_query.CompiledTemplate
|
|
if prepared_query.IsTemplate(query) {
|
|
var err error
|
|
ct, err = prepared_query.Compile(query)
|
|
if err != nil {
|
|
return fmt.Errorf("failed compiling template: %s", err)
|
|
}
|
|
}
|
|
|
|
// Insert the wrapped query.
|
|
if err := s.tx.Insert("prepared-queries", &queryWrapper{query, ct}); err != nil {
|
|
return fmt.Errorf("failed restoring prepared query: %s", err)
|
|
}
|
|
if err := indexUpdateMaxTxn(s.tx, query.ModifyIndex, "prepared-queries"); err != nil {
|
|
return fmt.Errorf("failed updating index: %s", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// PreparedQuerySet is used to create or update a prepared query.
|
|
func (s *Store) PreparedQuerySet(idx uint64, query *structs.PreparedQuery) error {
|
|
tx := s.db.WriteTxn(idx)
|
|
defer tx.Abort()
|
|
|
|
if err := preparedQuerySetTxn(tx, idx, query); err != nil {
|
|
return err
|
|
}
|
|
|
|
return tx.Commit()
|
|
}
|
|
|
|
// preparedQuerySetTxn is the inner method used to insert a prepared query with
|
|
// the proper indexes into the state store.
|
|
func preparedQuerySetTxn(tx *txn, idx uint64, query *structs.PreparedQuery) error {
|
|
// Check that the ID is set.
|
|
if query.ID == "" {
|
|
return ErrMissingQueryID
|
|
}
|
|
|
|
// Check for an existing query.
|
|
wrapped, err := tx.First("prepared-queries", "id", query.ID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed prepared query lookup: %s", err)
|
|
}
|
|
existing := toPreparedQuery(wrapped)
|
|
|
|
// Set the indexes.
|
|
if existing != nil {
|
|
query.CreateIndex = existing.CreateIndex
|
|
query.ModifyIndex = idx
|
|
} else {
|
|
query.CreateIndex = idx
|
|
query.ModifyIndex = idx
|
|
}
|
|
|
|
// Verify that the query name doesn't already exist, or that we are
|
|
// updating the same instance that has this name. If this is a template
|
|
// and the name is empty then we make sure there's not an empty template
|
|
// already registered.
|
|
if query.Name != "" {
|
|
wrapped, err := tx.First("prepared-queries", "name", query.Name)
|
|
if err != nil {
|
|
return fmt.Errorf("failed prepared query lookup: %s", err)
|
|
}
|
|
other := toPreparedQuery(wrapped)
|
|
if other != nil && (existing == nil || existing.ID != other.ID) {
|
|
return fmt.Errorf("name '%s' aliases an existing query name", query.Name)
|
|
}
|
|
} else if prepared_query.IsTemplate(query) {
|
|
wrapped, err := tx.First("prepared-queries", "template", query.Name)
|
|
if err != nil {
|
|
return fmt.Errorf("failed prepared query lookup: %s", err)
|
|
}
|
|
other := toPreparedQuery(wrapped)
|
|
if other != nil && (existing == nil || existing.ID != other.ID) {
|
|
return fmt.Errorf("a query template with an empty name already exists")
|
|
}
|
|
}
|
|
|
|
// Verify that the name doesn't alias any existing ID. We allow queries
|
|
// to be looked up by ID *or* name so we don't want anyone to try to
|
|
// register a query with a name equal to some other query's ID in an
|
|
// attempt to hijack it. We also look up by ID *then* name in order to
|
|
// prevent this, but it seems prudent to prevent these types of rogue
|
|
// queries from ever making it into the state store. Note that we have
|
|
// to see if the name looks like a UUID before checking since the UUID
|
|
// index will complain if we look up something that's not formatted
|
|
// like one.
|
|
if isUUID(query.Name) {
|
|
wrapped, err := tx.First("prepared-queries", "id", query.Name)
|
|
if err != nil {
|
|
return fmt.Errorf("failed prepared query lookup: %s", err)
|
|
}
|
|
if wrapped != nil {
|
|
return fmt.Errorf("name '%s' aliases an existing query ID", query.Name)
|
|
}
|
|
}
|
|
|
|
// Verify that the session exists.
|
|
if query.Session != "" {
|
|
sess, err := firstWithTxn(tx, "sessions", "id", query.Session, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid session: %v", err)
|
|
}
|
|
if sess == nil {
|
|
return fmt.Errorf("invalid session %#v", query.Session)
|
|
}
|
|
}
|
|
|
|
// We do not verify the service here, nor the token, if any. These are
|
|
// checked at execute time and not doing integrity checking on them
|
|
// helps avoid bootstrapping chicken and egg problems.
|
|
|
|
// If this is a template, compile it, otherwise leave the compiled
|
|
// template field nil.
|
|
var ct *prepared_query.CompiledTemplate
|
|
if prepared_query.IsTemplate(query) {
|
|
var err error
|
|
ct, err = prepared_query.Compile(query)
|
|
if err != nil {
|
|
return fmt.Errorf("failed compiling template: %s", err)
|
|
}
|
|
}
|
|
|
|
// Insert the wrapped query.
|
|
if err := tx.Insert("prepared-queries", &queryWrapper{query, ct}); err != nil {
|
|
return fmt.Errorf("failed inserting prepared query: %s", err)
|
|
}
|
|
if err := tx.Insert("index", &IndexEntry{"prepared-queries", idx}); err != nil {
|
|
return fmt.Errorf("failed updating index: %s", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// PreparedQueryDelete deletes the given query by ID.
|
|
func (s *Store) PreparedQueryDelete(idx uint64, queryID string) error {
|
|
tx := s.db.WriteTxn(idx)
|
|
defer tx.Abort()
|
|
|
|
if err := preparedQueryDeleteTxn(tx, idx, queryID); err != nil {
|
|
return fmt.Errorf("failed prepared query delete: %s", err)
|
|
}
|
|
|
|
return tx.Commit()
|
|
}
|
|
|
|
// preparedQueryDeleteTxn is the inner method used to delete a prepared query
|
|
// with the proper indexes into the state store.
|
|
func preparedQueryDeleteTxn(tx WriteTxn, idx uint64, queryID string) error {
|
|
// Pull the query.
|
|
wrapped, err := tx.First("prepared-queries", "id", queryID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed prepared query lookup: %s", err)
|
|
}
|
|
if wrapped == nil {
|
|
return nil
|
|
}
|
|
|
|
// Delete the query and update the index.
|
|
if err := tx.Delete("prepared-queries", wrapped); err != nil {
|
|
return fmt.Errorf("failed prepared query delete: %s", err)
|
|
}
|
|
if err := tx.Insert("index", &IndexEntry{"prepared-queries", idx}); err != nil {
|
|
return fmt.Errorf("failed updating index: %s", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// PreparedQueryGet returns the given prepared query by ID.
|
|
func (s *Store) PreparedQueryGet(ws memdb.WatchSet, queryID string) (uint64, *structs.PreparedQuery, error) {
|
|
tx := s.db.Txn(false)
|
|
defer tx.Abort()
|
|
|
|
// Get the table index.
|
|
idx := maxIndexTxn(tx, "prepared-queries")
|
|
|
|
// Look up the query by its ID.
|
|
watchCh, wrapped, err := tx.FirstWatch("prepared-queries", "id", queryID)
|
|
if err != nil {
|
|
return 0, nil, fmt.Errorf("failed prepared query lookup: %s", err)
|
|
}
|
|
ws.Add(watchCh)
|
|
return idx, toPreparedQuery(wrapped), nil
|
|
}
|
|
|
|
// PreparedQueryResolve returns the given prepared query by looking up an ID or
|
|
// Name. If the query was looked up by name and it's a template, then the
|
|
// template will be rendered before it is returned.
|
|
func (s *Store) PreparedQueryResolve(queryIDOrName string, source structs.QuerySource) (uint64, *structs.PreparedQuery, error) {
|
|
tx := s.db.Txn(false)
|
|
defer tx.Abort()
|
|
|
|
// Get the table index.
|
|
idx := maxIndexTxn(tx, "prepared-queries")
|
|
|
|
// Explicitly ban an empty query. This will never match an ID and the
|
|
// schema is set up so it will never match a query with an empty name,
|
|
// but we check it here to be explicit about it (we'd never want to
|
|
// return the results from the first query w/o a name).
|
|
if queryIDOrName == "" {
|
|
return 0, nil, ErrMissingQueryID
|
|
}
|
|
|
|
// Try first by ID if it looks like they gave us an ID. We check the
|
|
// format before trying this because the UUID index will complain if
|
|
// we look up something that's not formatted like one.
|
|
if isUUID(queryIDOrName) {
|
|
wrapped, err := tx.First("prepared-queries", "id", queryIDOrName)
|
|
if err != nil {
|
|
return 0, nil, fmt.Errorf("failed prepared query lookup: %s", err)
|
|
}
|
|
if wrapped != nil {
|
|
query := toPreparedQuery(wrapped)
|
|
if prepared_query.IsTemplate(query) {
|
|
return idx, nil, fmt.Errorf("prepared query templates can only be resolved up by name, not by ID")
|
|
}
|
|
return idx, query, nil
|
|
}
|
|
}
|
|
|
|
// prep will check to see if the query is a template and render it
|
|
// first, otherwise it will just return a regular query.
|
|
prep := func(wrapped interface{}) (uint64, *structs.PreparedQuery, error) {
|
|
wrapper := wrapped.(*queryWrapper)
|
|
if prepared_query.IsTemplate(wrapper.PreparedQuery) {
|
|
render, err := wrapper.ct.Render(queryIDOrName, source)
|
|
if err != nil {
|
|
return idx, nil, err
|
|
}
|
|
return idx, render, nil
|
|
}
|
|
return idx, wrapper.PreparedQuery, nil
|
|
}
|
|
|
|
// Next, look for an exact name match. This is the common case for static
|
|
// prepared queries, and could also apply to templates.
|
|
{
|
|
wrapped, err := tx.First("prepared-queries", "name", queryIDOrName)
|
|
if err != nil {
|
|
return 0, nil, fmt.Errorf("failed prepared query lookup: %s", err)
|
|
}
|
|
if wrapped != nil {
|
|
return prep(wrapped)
|
|
}
|
|
}
|
|
|
|
// Next, look for the longest prefix match among the prepared query
|
|
// templates.
|
|
{
|
|
wrapped, err := tx.LongestPrefix("prepared-queries", "template_prefix", queryIDOrName)
|
|
if err != nil {
|
|
return 0, nil, fmt.Errorf("failed prepared query lookup: %s", err)
|
|
}
|
|
if wrapped != nil {
|
|
return prep(wrapped)
|
|
}
|
|
}
|
|
|
|
return idx, nil, nil
|
|
}
|
|
|
|
// PreparedQueryList returns all the prepared queries.
|
|
func (s *Store) PreparedQueryList(ws memdb.WatchSet) (uint64, structs.PreparedQueries, error) {
|
|
tx := s.db.Txn(false)
|
|
defer tx.Abort()
|
|
|
|
// Get the table index.
|
|
idx := maxIndexTxn(tx, "prepared-queries")
|
|
|
|
// Query all of the prepared queries in the state store.
|
|
queries, err := tx.Get("prepared-queries", "id")
|
|
if err != nil {
|
|
return 0, nil, fmt.Errorf("failed prepared query lookup: %s", err)
|
|
}
|
|
ws.Add(queries.WatchCh())
|
|
|
|
// Go over all of the queries and build the response.
|
|
var result structs.PreparedQueries
|
|
for wrapped := queries.Next(); wrapped != nil; wrapped = queries.Next() {
|
|
result = append(result, toPreparedQuery(wrapped))
|
|
}
|
|
return idx, result, nil
|
|
}
|