open-nomad/nomad/state/schema.go
Seth Hoenig ba728f8f97
api: enable support for setting original job source (#16763)
* api: enable support for setting original source alongside job

This PR adds support for setting job source material along with
the registration of a job.

This includes a new HTTP endpoint and a new RPC endpoint for
making queries for the original source of a job. The
HTTP endpoint is /v1/job/<id>/submission?version=<version> and
the RPC method is Job.GetJobSubmission.

The job source (if submitted, and doing so is always optional), is
stored in the job_submission memdb table, separately from the
actual job. This way we do not incur overhead of reading the large
string field throughout normal job operations.

The server config now includes job_max_source_size for configuring
the maximum size the job source may be, before the server simply
drops the source material. This should help prevent Bad Things from
happening when huge jobs are submitted. If the value is set to 0,
all job source material will be dropped.

* api: avoid writing var content to disk for parsing

* api: move submission validation into RPC layer

* api: return an error if updating a job submission without namespace or job id

* api: be exact about the job index we associate a submission with (modify)

* api: reword api docs scheduling

* api: prune all but the last 6 job submissions

* api: protect against nil job submission in job validation

* api: set max job source size in test server

* api: fixups from pr
2023-04-11 08:45:08 -05:00

1614 lines
40 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package state
import (
"fmt"
"sync"
"github.com/hashicorp/go-memdb"
"github.com/hashicorp/nomad/nomad/state/indexer"
"github.com/hashicorp/nomad/nomad/structs"
)
const (
tableIndex = "index"
TableNamespaces = "namespaces"
TableServiceRegistrations = "service_registrations"
TableVariables = "variables"
TableVariablesQuotas = "variables_quota"
TableRootKeyMeta = "root_key_meta"
TableACLRoles = "acl_roles"
TableACLAuthMethods = "acl_auth_methods"
TableACLBindingRules = "acl_binding_rules"
TableAllocs = "allocs"
)
const (
indexID = "id"
indexJob = "job"
indexNodeID = "node_id"
indexAllocID = "alloc_id"
indexServiceName = "service_name"
indexExpiresGlobal = "expires-global"
indexExpiresLocal = "expires-local"
indexKeyID = "key_id"
indexPath = "path"
indexName = "name"
indexSigningKey = "signing_key"
indexAuthMethod = "auth_method"
)
var (
schemaFactories SchemaFactories
factoriesLock sync.Mutex
)
// SchemaFactory is the factory method for returning a TableSchema
type SchemaFactory func() *memdb.TableSchema
type SchemaFactories []SchemaFactory
// RegisterSchemaFactories is used to register a table schema.
func RegisterSchemaFactories(factories ...SchemaFactory) {
factoriesLock.Lock()
defer factoriesLock.Unlock()
schemaFactories = append(schemaFactories, factories...)
}
func GetFactories() SchemaFactories {
return schemaFactories
}
func init() {
// Register all schemas
RegisterSchemaFactories([]SchemaFactory{
indexTableSchema,
nodeTableSchema,
jobTableSchema,
jobSummarySchema,
jobVersionSchema,
jobSubmissionSchema,
deploymentSchema,
periodicLaunchTableSchema,
evalTableSchema,
allocTableSchema,
vaultAccessorTableSchema,
siTokenAccessorTableSchema,
aclPolicyTableSchema,
aclTokenTableSchema,
oneTimeTokenTableSchema,
autopilotConfigTableSchema,
schedulerConfigTableSchema,
clusterMetaTableSchema,
csiVolumeTableSchema,
csiPluginTableSchema,
scalingPolicyTableSchema,
scalingEventTableSchema,
namespaceTableSchema,
serviceRegistrationsTableSchema,
variablesTableSchema,
variablesQuotasTableSchema,
variablesRootKeyMetaSchema,
aclRolesTableSchema,
aclAuthMethodsTableSchema,
bindingRulesTableSchema,
}...)
}
// stateStoreSchema is used to return the schema for the state store
func stateStoreSchema() *memdb.DBSchema {
// Create the root DB schema
db := &memdb.DBSchema{
Tables: make(map[string]*memdb.TableSchema),
}
// Add each of the tables
for _, schemaFn := range GetFactories() {
schema := schemaFn()
if _, ok := db.Tables[schema.Name]; ok {
panic(fmt.Sprintf("duplicate table name: %s", schema.Name))
}
db.Tables[schema.Name] = schema
}
return db
}
// indexTableSchema is used for tracking the most recent index used for each table.
func indexTableSchema() *memdb.TableSchema {
return &memdb.TableSchema{
Name: "index",
Indexes: map[string]*memdb.IndexSchema{
"id": {
Name: "id",
AllowMissing: false,
Unique: true,
Indexer: &memdb.StringFieldIndex{
Field: "Key",
Lowercase: true,
},
},
},
}
}
// nodeTableSchema returns the MemDB schema for the nodes table.
// This table is used to store all the client nodes that are registered.
func nodeTableSchema() *memdb.TableSchema {
return &memdb.TableSchema{
Name: "nodes",
Indexes: map[string]*memdb.IndexSchema{
// Primary index is used for node management
// and simple direct lookup. ID is required to be
// unique.
"id": {
Name: "id",
AllowMissing: false,
Unique: true,
Indexer: &memdb.UUIDFieldIndex{
Field: "ID",
},
},
"secret_id": {
Name: "secret_id",
AllowMissing: false,
Unique: true,
Indexer: &memdb.UUIDFieldIndex{
Field: "SecretID",
},
},
},
}
}
// jobTableSchema returns the MemDB schema for the jobs table.
// This table is used to store all the jobs that have been submitted.
func jobTableSchema() *memdb.TableSchema {
return &memdb.TableSchema{
Name: "jobs",
Indexes: map[string]*memdb.IndexSchema{
// Primary index is used for job management
// and simple direct lookup. ID is required to be
// unique within a namespace.
"id": {
Name: "id",
AllowMissing: false,
Unique: true,
// Use a compound index so the tuple of (Namespace, ID) is
// uniquely identifying
Indexer: &memdb.CompoundIndex{
Indexes: []memdb.Indexer{
&memdb.StringFieldIndex{
Field: "Namespace",
},
&memdb.StringFieldIndex{
Field: "ID",
},
},
},
},
"type": {
Name: "type",
AllowMissing: false,
Unique: false,
Indexer: &memdb.StringFieldIndex{
Field: "Type",
Lowercase: false,
},
},
"gc": {
Name: "gc",
AllowMissing: false,
Unique: false,
Indexer: &memdb.ConditionalIndex{
Conditional: jobIsGCable,
},
},
"periodic": {
Name: "periodic",
AllowMissing: false,
Unique: false,
Indexer: &memdb.ConditionalIndex{
Conditional: jobIsPeriodic,
},
},
},
}
}
// jobSummarySchema returns the memdb schema for the job summary table
func jobSummarySchema() *memdb.TableSchema {
return &memdb.TableSchema{
Name: "job_summary",
Indexes: map[string]*memdb.IndexSchema{
"id": {
Name: "id",
AllowMissing: false,
Unique: true,
// Use a compound index so the tuple of (Namespace, JobID) is
// uniquely identifying
Indexer: &memdb.CompoundIndex{
Indexes: []memdb.Indexer{
&memdb.StringFieldIndex{
Field: "Namespace",
},
&memdb.StringFieldIndex{
Field: "JobID",
},
},
},
},
},
}
}
// jobVersionSchema returns the memdb schema for the job version table which
// keeps a historical view of job versions.
func jobVersionSchema() *memdb.TableSchema {
return &memdb.TableSchema{
Name: "job_version",
Indexes: map[string]*memdb.IndexSchema{
"id": {
Name: "id",
AllowMissing: false,
Unique: true,
// Use a compound index so the tuple of (Namespace, ID, Version) is
// uniquely identifying
Indexer: &memdb.CompoundIndex{
Indexes: []memdb.Indexer{
&memdb.StringFieldIndex{
Field: "Namespace",
},
&memdb.StringFieldIndex{
Field: "ID",
Lowercase: true,
},
&memdb.UintFieldIndex{
Field: "Version",
},
},
},
},
},
}
}
// jobSubmissionSchema returns the memdb table schema of job submissions
// which contain the original source material of each job, per version.
// Unique index by Namespace, JobID, and Version.
func jobSubmissionSchema() *memdb.TableSchema {
return &memdb.TableSchema{
Name: "job_submission",
Indexes: map[string]*memdb.IndexSchema{
"id": {
Name: "id",
AllowMissing: false,
Unique: true,
Indexer: &memdb.CompoundIndex{
Indexes: []memdb.Indexer{
&memdb.StringFieldIndex{
Field: "Namespace",
},
&memdb.StringFieldIndex{
Field: "JobID",
Lowercase: true,
},
&memdb.UintFieldIndex{
Field: "Version",
},
},
},
},
"by_jobID": {
Name: "by_jobID",
AllowMissing: false,
Unique: false,
Indexer: &memdb.CompoundIndex{
Indexes: []memdb.Indexer{
&memdb.StringFieldIndex{
Field: "Namespace",
},
&memdb.StringFieldIndex{
Field: "JobID",
Lowercase: true,
},
},
},
},
},
}
}
// jobIsGCable satisfies the ConditionalIndexFunc interface and creates an index
// on whether a job is eligible for garbage collection.
func jobIsGCable(obj interface{}) (bool, error) {
j, ok := obj.(*structs.Job)
if !ok {
return false, fmt.Errorf("Unexpected type: %v", obj)
}
// If the job is periodic or parameterized it is only garbage collectable if
// it is stopped.
periodic := j.Periodic != nil && j.Periodic.Enabled
parameterized := j.IsParameterized()
if periodic || parameterized {
return j.Stop, nil
}
// If the job isn't dead it isn't eligible
if j.Status != structs.JobStatusDead {
return false, nil
}
// Any job that is stopped is eligible for garbage collection
if j.Stop {
return true, nil
}
switch j.Type {
// Otherwise, batch and sysbatch jobs are eligible because they complete on
// their own without a user stopping them.
case structs.JobTypeBatch, structs.JobTypeSysBatch:
return true, nil
default:
// other job types may not be GC until stopped
return false, nil
}
}
// jobIsPeriodic satisfies the ConditionalIndexFunc interface and creates an index
// on whether a job is periodic.
func jobIsPeriodic(obj interface{}) (bool, error) {
j, ok := obj.(*structs.Job)
if !ok {
return false, fmt.Errorf("Unexpected type: %v", obj)
}
if j.Periodic != nil && j.Periodic.Enabled {
return true, nil
}
return false, nil
}
// deploymentSchema returns the MemDB schema tracking a job's deployments
func deploymentSchema() *memdb.TableSchema {
return &memdb.TableSchema{
Name: "deployment",
Indexes: map[string]*memdb.IndexSchema{
// id index is used for direct lookup of an deployment by ID.
"id": {
Name: "id",
AllowMissing: false,
Unique: true,
Indexer: &memdb.UUIDFieldIndex{
Field: "ID",
},
},
// create index is used for listing deploy, ordering them by
// creation chronology. (Use a reverse iterator for newest first).
//
// There may be more than one deployment per CreateIndex.
"create": {
Name: "create",
AllowMissing: false,
Unique: true,
Indexer: &memdb.CompoundIndex{
Indexes: []memdb.Indexer{
&memdb.UintFieldIndex{
Field: "CreateIndex",
},
&memdb.StringFieldIndex{
Field: "ID",
},
},
},
},
// namespace is used to lookup evaluations by namespace.
"namespace": {
Name: "namespace",
AllowMissing: false,
Unique: false,
Indexer: &memdb.StringFieldIndex{
Field: "Namespace",
},
},
// namespace_create index is used to lookup deployments by namespace
// in their original chronological order based on CreateIndex.
//
// Use a prefix iterator (namespace_create_prefix) to iterate deployments
// of a Namespace in order of CreateIndex.
//
// There may be more than one deployment per CreateIndex.
"namespace_create": {
Name: "namespace_create",
AllowMissing: false,
Unique: true,
Indexer: &memdb.CompoundIndex{
AllowMissing: false,
Indexes: []memdb.Indexer{
&memdb.StringFieldIndex{
Field: "Namespace",
},
&memdb.UintFieldIndex{
Field: "CreateIndex",
},
&memdb.StringFieldIndex{
Field: "ID",
},
},
},
},
// job index is used to lookup deployments by job
"job": {
Name: "job",
AllowMissing: false,
Unique: false,
// Use a compound index so the tuple of (Namespace, JobID) is
// uniquely identifying
Indexer: &memdb.CompoundIndex{
Indexes: []memdb.Indexer{
&memdb.StringFieldIndex{
Field: "Namespace",
},
&memdb.StringFieldIndex{
Field: "JobID",
},
},
},
},
},
}
}
// periodicLaunchTableSchema returns the MemDB schema tracking the most recent
// launch time for a periodic job.
func periodicLaunchTableSchema() *memdb.TableSchema {
return &memdb.TableSchema{
Name: "periodic_launch",
Indexes: map[string]*memdb.IndexSchema{
// Primary index is used for job management
// and simple direct lookup. ID is required to be
// unique.
"id": {
Name: "id",
AllowMissing: false,
Unique: true,
// Use a compound index so the tuple of (Namespace, JobID) is
// uniquely identifying
Indexer: &memdb.CompoundIndex{
Indexes: []memdb.Indexer{
&memdb.StringFieldIndex{
Field: "Namespace",
},
&memdb.StringFieldIndex{
Field: "ID",
},
},
},
},
},
}
}
// evalTableSchema returns the MemDB schema for the eval table.
// This table is used to store all the evaluations that are pending
// or recently completed.
func evalTableSchema() *memdb.TableSchema {
return &memdb.TableSchema{
Name: "evals",
Indexes: map[string]*memdb.IndexSchema{
// id index is used for direct lookup of an evaluation by ID.
"id": {
Name: "id",
AllowMissing: false,
Unique: true,
Indexer: &memdb.UUIDFieldIndex{
Field: "ID",
},
},
// create index is used for listing evaluations, ordering them by
// creation chronology. (Use a reverse iterator for newest first).
"create": {
Name: "create",
AllowMissing: false,
Unique: true,
Indexer: &memdb.CompoundIndex{
Indexes: []memdb.Indexer{
&memdb.UintFieldIndex{
Field: "CreateIndex",
},
&memdb.StringFieldIndex{
Field: "ID",
},
},
},
},
// job index is used to lookup evaluations by job ID.
"job": {
Name: "job",
AllowMissing: false,
Unique: false,
Indexer: &memdb.CompoundIndex{
Indexes: []memdb.Indexer{
&memdb.StringFieldIndex{
Field: "Namespace",
},
&memdb.StringFieldIndex{
Field: "JobID",
Lowercase: true,
},
&memdb.StringFieldIndex{
Field: "Status",
Lowercase: true,
},
},
},
},
// namespace is used to lookup evaluations by namespace.
"namespace": {
Name: "namespace",
AllowMissing: false,
Unique: false,
Indexer: &memdb.StringFieldIndex{
Field: "Namespace",
},
},
// namespace_create index is used to lookup evaluations by namespace
// in their original chronological order based on CreateIndex.
//
// Use a prefix iterator (namespace_prefix) on a Namespace to iterate
// those evaluations in order of CreateIndex.
"namespace_create": {
Name: "namespace_create",
AllowMissing: false,
Unique: true,
Indexer: &memdb.CompoundIndex{
AllowMissing: false,
Indexes: []memdb.Indexer{
&memdb.StringFieldIndex{
Field: "Namespace",
},
&memdb.UintFieldIndex{
Field: "CreateIndex",
},
&memdb.StringFieldIndex{
Field: "ID",
},
},
},
},
},
}
}
// allocTableSchema returns the MemDB schema for the allocation table.
// This table is used to store all the task allocations between task groups
// and nodes.
func allocTableSchema() *memdb.TableSchema {
return &memdb.TableSchema{
Name: "allocs",
Indexes: map[string]*memdb.IndexSchema{
// id index is used for direct lookup of allocation by ID.
"id": {
Name: "id",
AllowMissing: false,
Unique: true,
Indexer: &memdb.UUIDFieldIndex{
Field: "ID",
},
},
// create index is used for listing allocations, ordering them by
// creation chronology. (Use a reverse iterator for newest first).
"create": {
Name: "create",
AllowMissing: false,
Unique: true,
Indexer: &memdb.CompoundIndex{
Indexes: []memdb.Indexer{
&memdb.UintFieldIndex{
Field: "CreateIndex",
},
&memdb.StringFieldIndex{
Field: "ID",
},
},
},
},
// namespace is used to lookup evaluations by namespace.
// todo(shoenig): i think we can deprecate this and other like it
"namespace": {
Name: "namespace",
AllowMissing: false,
Unique: false,
Indexer: &memdb.StringFieldIndex{
Field: "Namespace",
},
},
// namespace_create index is used to lookup evaluations by namespace
// in their original chronological order based on CreateIndex.
//
// Use a prefix iterator (namespace_prefix) on a Namespace to iterate
// those evaluations in order of CreateIndex.
"namespace_create": {
Name: "namespace_create",
AllowMissing: false,
Unique: true,
Indexer: &memdb.CompoundIndex{
AllowMissing: false,
Indexes: []memdb.Indexer{
&memdb.StringFieldIndex{
Field: "Namespace",
},
&memdb.UintFieldIndex{
Field: "CreateIndex",
},
&memdb.StringFieldIndex{
Field: "ID",
},
},
},
},
// Node index is used to lookup allocations by node
"node": {
Name: "node",
AllowMissing: true, // Missing is allow for failed allocations
Unique: false,
Indexer: &memdb.CompoundIndex{
Indexes: []memdb.Indexer{
&memdb.StringFieldIndex{
Field: "NodeID",
Lowercase: true,
},
// Conditional indexer on if allocation is terminal
&memdb.ConditionalIndex{
Conditional: func(obj interface{}) (bool, error) {
// Cast to allocation
alloc, ok := obj.(*structs.Allocation)
if !ok {
return false, fmt.Errorf("wrong type, got %t should be Allocation", obj)
}
// Check if the allocation is terminal
return alloc.TerminalStatus(), nil
},
},
},
},
},
// Job index is used to lookup allocations by job
"job": {
Name: "job",
AllowMissing: false,
Unique: false,
Indexer: &memdb.CompoundIndex{
Indexes: []memdb.Indexer{
&memdb.StringFieldIndex{
Field: "Namespace",
},
&memdb.StringFieldIndex{
Field: "JobID",
},
},
},
},
// Eval index is used to lookup allocations by eval
"eval": {
Name: "eval",
AllowMissing: false,
Unique: false,
Indexer: &memdb.UUIDFieldIndex{
Field: "EvalID",
},
},
// Deployment index is used to lookup allocations by deployment
"deployment": {
Name: "deployment",
AllowMissing: true,
Unique: false,
Indexer: &memdb.UUIDFieldIndex{
Field: "DeploymentID",
},
},
// signing_key index is used to lookup live allocations by signing
// key ID
indexSigningKey: {
Name: indexSigningKey,
AllowMissing: true, // terminal allocations won't be indexed
Unique: false,
Indexer: &memdb.CompoundIndex{
Indexes: []memdb.Indexer{
&memdb.StringFieldIndex{
Field: "SigningKeyID",
},
&memdb.ConditionalIndex{
Conditional: func(obj interface{}) (bool, error) {
alloc, ok := obj.(*structs.Allocation)
if !ok {
return false, fmt.Errorf(
"wrong type, got %t should be Allocation", obj)
}
// note: this isn't alloc.TerminalStatus(),
// because we only want to consider the key
// unused if the allocation is terminal on both
// server and client
return !(alloc.ClientTerminalStatus() && alloc.ServerTerminalStatus()), nil
},
},
},
},
},
},
}
}
// vaultAccessorTableSchema returns the MemDB schema for the Vault Accessor
// Table. This table tracks Vault accessors for tokens created on behalf of
// allocations required Vault tokens.
func vaultAccessorTableSchema() *memdb.TableSchema {
return &memdb.TableSchema{
Name: "vault_accessors",
Indexes: map[string]*memdb.IndexSchema{
// The primary index is the accessor id
"id": {
Name: "id",
AllowMissing: false,
Unique: true,
Indexer: &memdb.StringFieldIndex{
Field: "Accessor",
},
},
"alloc_id": {
Name: "alloc_id",
AllowMissing: false,
Unique: false,
Indexer: &memdb.StringFieldIndex{
Field: "AllocID",
},
},
"node_id": {
Name: "node_id",
AllowMissing: false,
Unique: false,
Indexer: &memdb.StringFieldIndex{
Field: "NodeID",
},
},
},
}
}
// siTokenAccessorTableSchema returns the MemDB schema for the Service Identity
// token accessor table. This table tracks accessors for tokens created on behalf
// of allocations with Consul connect enabled tasks that need SI tokens.
func siTokenAccessorTableSchema() *memdb.TableSchema {
return &memdb.TableSchema{
Name: siTokenAccessorTable,
Indexes: map[string]*memdb.IndexSchema{
// The primary index is the accessor id
"id": {
Name: "id",
AllowMissing: false,
Unique: true,
Indexer: &memdb.StringFieldIndex{
Field: "AccessorID",
},
},
"alloc_id": {
Name: "alloc_id",
AllowMissing: false,
Unique: false,
Indexer: &memdb.StringFieldIndex{
Field: "AllocID",
},
},
"node_id": {
Name: "node_id",
AllowMissing: false,
Unique: false,
Indexer: &memdb.StringFieldIndex{
Field: "NodeID",
},
},
},
}
}
// aclPolicyTableSchema returns the MemDB schema for the policy table.
// This table is used to store the policies which are referenced by tokens
func aclPolicyTableSchema() *memdb.TableSchema {
return &memdb.TableSchema{
Name: "acl_policy",
Indexes: map[string]*memdb.IndexSchema{
"id": {
Name: "id",
AllowMissing: false,
Unique: true,
Indexer: &memdb.StringFieldIndex{
Field: "Name",
},
},
"job": {
Name: "job",
AllowMissing: true,
Unique: false,
Indexer: &ACLPolicyJobACLFieldIndex{},
},
},
}
}
// ACLPolicyJobACLFieldIndex is used to extract the policy's JobACL field and
// build an index on it.
type ACLPolicyJobACLFieldIndex struct{}
// FromObject is used to extract an index value from an
// object or to indicate that the index value is missing.
func (a *ACLPolicyJobACLFieldIndex) FromObject(obj interface{}) (bool, []byte, error) {
policy, ok := obj.(*structs.ACLPolicy)
if !ok {
return false, nil, fmt.Errorf("object %#v is not an ACLPolicy", obj)
}
if policy.JobACL == nil {
return false, nil, nil
}
ns := policy.JobACL.Namespace
if ns == "" {
return false, nil, nil
}
jobID := policy.JobACL.JobID
if jobID == "" {
return false, nil, fmt.Errorf(
"object %#v is not a valid ACLPolicy: JobACL.JobID without Namespace", obj)
}
val := ns + "\x00" + jobID + "\x00"
return true, []byte(val), nil
}
// FromArgs is used to build an exact index lookup based on arguments
func (a *ACLPolicyJobACLFieldIndex) FromArgs(args ...interface{}) ([]byte, error) {
if len(args) != 2 {
return nil, fmt.Errorf("must provide two arguments")
}
arg0, ok := args[0].(string)
if !ok {
return nil, fmt.Errorf("argument must be a string: %#v", args[0])
}
arg1, ok := args[1].(string)
if !ok {
return nil, fmt.Errorf("argument must be a string: %#v", args[0])
}
// Add the null character as a terminator
arg0 += "\x00" + arg1 + "\x00"
return []byte(arg0), nil
}
// PrefixFromArgs returns a prefix that should be used for scanning based on the arguments
func (a *ACLPolicyJobACLFieldIndex) PrefixFromArgs(args ...interface{}) ([]byte, error) {
val, err := a.FromArgs(args...)
if err != nil {
return nil, err
}
// Strip the null terminator, the rest is a prefix
n := len(val)
if n > 0 {
return val[:n-1], nil
}
return val, nil
}
// aclTokenTableSchema returns the MemDB schema for the tokens table.
// This table is used to store the bearer tokens which are used to authenticate
func aclTokenTableSchema() *memdb.TableSchema {
return &memdb.TableSchema{
Name: "acl_token",
Indexes: map[string]*memdb.IndexSchema{
"id": {
Name: "id",
AllowMissing: false,
Unique: true,
Indexer: &memdb.UUIDFieldIndex{
Field: "AccessorID",
},
},
"create": {
Name: "create",
AllowMissing: false,
Unique: true,
Indexer: &memdb.CompoundIndex{
Indexes: []memdb.Indexer{
&memdb.UintFieldIndex{
Field: "CreateIndex",
},
&memdb.StringFieldIndex{
Field: "AccessorID",
},
},
},
},
"secret": {
Name: "secret",
AllowMissing: false,
Unique: true,
Indexer: &memdb.UUIDFieldIndex{
Field: "SecretID",
},
},
"global": {
Name: "global",
AllowMissing: false,
Unique: false,
Indexer: &memdb.FieldSetIndex{
Field: "Global",
},
},
indexExpiresGlobal: {
Name: indexExpiresGlobal,
AllowMissing: true,
Unique: false,
Indexer: indexer.SingleIndexer{
ReadIndex: indexer.ReadIndex(indexer.IndexFromTimeQuery),
WriteIndex: indexer.WriteIndex(indexExpiresGlobalFromACLToken),
},
},
indexExpiresLocal: {
Name: indexExpiresLocal,
AllowMissing: true,
Unique: false,
Indexer: indexer.SingleIndexer{
ReadIndex: indexer.ReadIndex(indexer.IndexFromTimeQuery),
WriteIndex: indexer.WriteIndex(indexExpiresLocalFromACLToken),
},
},
},
}
}
func indexExpiresLocalFromACLToken(raw interface{}) ([]byte, error) {
return indexExpiresFromACLToken(raw, false)
}
func indexExpiresGlobalFromACLToken(raw interface{}) ([]byte, error) {
return indexExpiresFromACLToken(raw, true)
}
// indexExpiresFromACLToken implements the indexer.WriteIndex interface and
// allows us to use an ACL tokens ExpirationTime as an index, if it is a
// non-default value. This allows for efficient lookups when trying to deal
// with removal of expired tokens from state.
func indexExpiresFromACLToken(raw interface{}, global bool) ([]byte, error) {
p, ok := raw.(*structs.ACLToken)
if !ok {
return nil, fmt.Errorf("unexpected type %T for structs.ACLToken index", raw)
}
if p.Global != global {
return nil, indexer.ErrMissingValueForIndex
}
if !p.HasExpirationTime() {
return nil, indexer.ErrMissingValueForIndex
}
if p.ExpirationTime.Unix() < 0 {
return nil, fmt.Errorf("token expiration time cannot be before the unix epoch: %s", p.ExpirationTime)
}
var b indexer.IndexBuilder
b.Time(*p.ExpirationTime)
return b.Bytes(), nil
}
// oneTimeTokenTableSchema returns the MemDB schema for the tokens table.
// This table is used to store one-time tokens for ACL tokens
func oneTimeTokenTableSchema() *memdb.TableSchema {
return &memdb.TableSchema{
Name: "one_time_token",
Indexes: map[string]*memdb.IndexSchema{
"secret": {
Name: "secret",
AllowMissing: false,
Unique: true,
Indexer: &memdb.UUIDFieldIndex{
Field: "OneTimeSecretID",
},
},
"id": {
Name: "id",
AllowMissing: false,
Unique: true,
Indexer: &memdb.UUIDFieldIndex{
Field: "AccessorID",
},
},
},
}
}
// singletonRecord can be used to describe tables which should contain only 1 entry.
// Example uses include storing node config or cluster metadata blobs.
var singletonRecord = &memdb.ConditionalIndex{
Conditional: func(interface{}) (bool, error) { return true, nil },
}
// schedulerConfigTableSchema returns the MemDB schema for the scheduler config table.
// This table is used to store configuration options for the scheduler
func schedulerConfigTableSchema() *memdb.TableSchema {
return &memdb.TableSchema{
Name: "scheduler_config",
Indexes: map[string]*memdb.IndexSchema{
"id": {
Name: "id",
AllowMissing: true,
Unique: true,
Indexer: singletonRecord, // we store only 1 scheduler config
},
},
}
}
// clusterMetaTableSchema returns the MemDB schema for the scheduler config table.
func clusterMetaTableSchema() *memdb.TableSchema {
return &memdb.TableSchema{
Name: "cluster_meta",
Indexes: map[string]*memdb.IndexSchema{
"id": {
Name: "id",
AllowMissing: false,
Unique: true,
Indexer: singletonRecord, // we store only 1 cluster metadata
},
},
}
}
// CSIVolumes are identified by id globally, and searchable by driver
func csiVolumeTableSchema() *memdb.TableSchema {
return &memdb.TableSchema{
Name: "csi_volumes",
Indexes: map[string]*memdb.IndexSchema{
"id": {
Name: "id",
AllowMissing: false,
Unique: true,
Indexer: &memdb.CompoundIndex{
Indexes: []memdb.Indexer{
&memdb.StringFieldIndex{
Field: "Namespace",
},
&memdb.StringFieldIndex{
Field: "ID",
},
},
},
},
"plugin_id": {
Name: "plugin_id",
AllowMissing: false,
Unique: false,
Indexer: &memdb.StringFieldIndex{
Field: "PluginID",
},
},
},
}
}
// CSIPlugins are identified by id globally, and searchable by driver
func csiPluginTableSchema() *memdb.TableSchema {
return &memdb.TableSchema{
Name: "csi_plugins",
Indexes: map[string]*memdb.IndexSchema{
"id": {
Name: "id",
AllowMissing: false,
Unique: true,
Indexer: &memdb.StringFieldIndex{
Field: "ID",
},
},
},
}
}
// StringFieldIndex is used to extract a field from an object
// using reflection and builds an index on that field.
type ScalingPolicyTargetFieldIndex struct {
Field string
// AllowMissing controls if the field should be ignored if the field is
// not provided.
AllowMissing bool
}
// FromObject is used to extract an index value from an
// object or to indicate that the index value is missing.
func (s *ScalingPolicyTargetFieldIndex) FromObject(obj interface{}) (bool, []byte, error) {
policy, ok := obj.(*structs.ScalingPolicy)
if !ok {
return false, nil, fmt.Errorf("object %#v is not a ScalingPolicy", obj)
}
if policy.Target == nil {
return false, nil, nil
}
val, ok := policy.Target[s.Field]
if !ok && !s.AllowMissing {
return false, nil, nil
}
// Add the null character as a terminator
val += "\x00"
return true, []byte(val), nil
}
// FromArgs is used to build an exact index lookup based on arguments
func (s *ScalingPolicyTargetFieldIndex) FromArgs(args ...interface{}) ([]byte, error) {
if len(args) != 1 {
return nil, fmt.Errorf("must provide only a single argument")
}
arg, ok := args[0].(string)
if !ok {
return nil, fmt.Errorf("argument must be a string: %#v", args[0])
}
// Add the null character as a terminator
arg += "\x00"
return []byte(arg), nil
}
// PrefixFromArgs returns a prefix that should be used for scanning based on the arguments
func (s *ScalingPolicyTargetFieldIndex) PrefixFromArgs(args ...interface{}) ([]byte, error) {
val, err := s.FromArgs(args...)
if err != nil {
return nil, err
}
// Strip the null terminator, the rest is a prefix
n := len(val)
if n > 0 {
return val[:n-1], nil
}
return val, nil
}
// scalingPolicyTableSchema returns the MemDB schema for the policy table.
func scalingPolicyTableSchema() *memdb.TableSchema {
return &memdb.TableSchema{
Name: "scaling_policy",
Indexes: map[string]*memdb.IndexSchema{
// Primary index is used for simple direct lookup.
"id": {
Name: "id",
AllowMissing: false,
Unique: true,
// UUID is uniquely identifying
Indexer: &memdb.StringFieldIndex{
Field: "ID",
},
},
// Target index is used for listing by namespace or job, or looking up a specific target.
// A given task group can have only a single scaling policies, so this is guaranteed to be unique.
"target": {
Name: "target",
Unique: false,
// Use a compound index so the tuple of (Namespace, Job, Group, Task) is
// used when looking for a policy
Indexer: &memdb.CompoundIndex{
Indexes: []memdb.Indexer{
&ScalingPolicyTargetFieldIndex{
Field: "Namespace",
AllowMissing: true,
},
&ScalingPolicyTargetFieldIndex{
Field: "Job",
AllowMissing: true,
},
&ScalingPolicyTargetFieldIndex{
Field: "Group",
AllowMissing: true,
},
&ScalingPolicyTargetFieldIndex{
Field: "Task",
AllowMissing: true,
},
},
},
},
// Type index is used for listing by policy type
"type": {
Name: "type",
AllowMissing: false,
Unique: false,
Indexer: &memdb.StringFieldIndex{
Field: "Type",
},
},
// Used to filter by enabled
"enabled": {
Name: "enabled",
AllowMissing: false,
Unique: false,
Indexer: &memdb.FieldSetIndex{
Field: "Enabled",
},
},
},
}
}
// scalingEventTableSchema returns the memdb schema for job scaling events
func scalingEventTableSchema() *memdb.TableSchema {
return &memdb.TableSchema{
Name: "scaling_event",
Indexes: map[string]*memdb.IndexSchema{
"id": {
Name: "id",
AllowMissing: false,
Unique: true,
// Use a compound index so the tuple of (Namespace, JobID) is
// uniquely identifying
Indexer: &memdb.CompoundIndex{
Indexes: []memdb.Indexer{
&memdb.StringFieldIndex{
Field: "Namespace",
},
&memdb.StringFieldIndex{
Field: "JobID",
},
},
},
},
// TODO: need to figure out whether we want to index these or the jobs or ...
// "error": {
// Name: "error",
// AllowMissing: false,
// Unique: false,
// Indexer: &memdb.FieldSetIndex{
// Field: "Error",
// },
// },
},
}
}
// namespaceTableSchema returns the MemDB schema for the namespace table.
func namespaceTableSchema() *memdb.TableSchema {
return &memdb.TableSchema{
Name: TableNamespaces,
Indexes: map[string]*memdb.IndexSchema{
"id": {
Name: "id",
AllowMissing: false,
Unique: true,
Indexer: &memdb.StringFieldIndex{
Field: "Name",
},
},
"quota": {
Name: "quota",
AllowMissing: true,
Unique: false,
Indexer: &memdb.StringFieldIndex{
Field: "Quota",
},
},
},
}
}
// serviceRegistrationsTableSchema returns the MemDB schema for Nomad native
// service registrations.
func serviceRegistrationsTableSchema() *memdb.TableSchema {
return &memdb.TableSchema{
Name: TableServiceRegistrations,
Indexes: map[string]*memdb.IndexSchema{
// The serviceID in combination with namespace forms a unique
// identifier for a service registration. This is used to look up
// and delete services in individual isolation.
indexID: {
Name: indexID,
AllowMissing: false,
Unique: true,
Indexer: &memdb.CompoundIndex{
Indexes: []memdb.Indexer{
&memdb.StringFieldIndex{
Field: "Namespace",
},
&memdb.StringFieldIndex{
Field: "ID",
},
},
},
},
indexServiceName: {
Name: indexServiceName,
AllowMissing: false,
Unique: false,
Indexer: &memdb.CompoundIndex{
Indexes: []memdb.Indexer{
&memdb.StringFieldIndex{
Field: "Namespace",
},
&memdb.StringFieldIndex{
Field: "ServiceName",
},
},
},
},
indexJob: {
Name: indexJob,
AllowMissing: false,
Unique: false,
Indexer: &memdb.CompoundIndex{
Indexes: []memdb.Indexer{
&memdb.StringFieldIndex{
Field: "Namespace",
},
&memdb.StringFieldIndex{
Field: "JobID",
},
},
},
},
// The nodeID index allows lookups and deletions to be performed
// for an entire node. This is primarily used when a node becomes
// lost.
indexNodeID: {
Name: indexNodeID,
AllowMissing: false,
Unique: false,
Indexer: &memdb.StringFieldIndex{
Field: "NodeID",
},
},
indexAllocID: {
Name: indexAllocID,
AllowMissing: false,
Unique: false,
Indexer: &memdb.StringFieldIndex{
Field: "AllocID",
},
},
},
}
}
// variablesTableSchema returns the MemDB schema for Nomad variables.
func variablesTableSchema() *memdb.TableSchema {
return &memdb.TableSchema{
Name: TableVariables,
Indexes: map[string]*memdb.IndexSchema{
indexID: {
Name: indexID,
AllowMissing: false,
Unique: true,
Indexer: &memdb.CompoundIndex{
Indexes: []memdb.Indexer{
&memdb.StringFieldIndex{
Field: "Namespace",
},
&memdb.StringFieldIndex{
Field: "Path",
},
},
},
},
indexKeyID: {
Name: indexKeyID,
AllowMissing: false,
Indexer: &variableKeyIDFieldIndexer{},
},
indexPath: {
Name: indexPath,
AllowMissing: false,
Unique: false,
Indexer: &memdb.StringFieldIndex{
Field: "Path",
},
},
},
}
}
type variableKeyIDFieldIndexer struct{}
// FromArgs implements go-memdb/Indexer and is used to build an exact
// index lookup based on arguments
func (s *variableKeyIDFieldIndexer) FromArgs(args ...interface{}) ([]byte, error) {
if len(args) != 1 {
return nil, fmt.Errorf("must provide only a single argument")
}
arg, ok := args[0].(string)
if !ok {
return nil, fmt.Errorf("argument must be a string: %#v", args[0])
}
// Add the null character as a terminator
arg += "\x00"
return []byte(arg), nil
}
// PrefixFromArgs implements go-memdb/PrefixIndexer and returns a
// prefix that should be used for scanning based on the arguments
func (s *variableKeyIDFieldIndexer) PrefixFromArgs(args ...interface{}) ([]byte, error) {
val, err := s.FromArgs(args...)
if err != nil {
return nil, err
}
// Strip the null terminator, the rest is a prefix
n := len(val)
if n > 0 {
return val[:n-1], nil
}
return val, nil
}
// FromObject implements go-memdb/SingleIndexer and is used to extract
// an index value from an object or to indicate that the index value
// is missing.
func (s *variableKeyIDFieldIndexer) FromObject(obj interface{}) (bool, []byte, error) {
variable, ok := obj.(*structs.VariableEncrypted)
if !ok {
return false, nil, fmt.Errorf("object %#v is not a Variable", obj)
}
keyID := variable.KeyID
if keyID == "" {
return false, nil, nil
}
// Add the null character as a terminator
keyID += "\x00"
return true, []byte(keyID), nil
}
// variablesQuotasTableSchema returns the MemDB schema for Nomad variables
// quotas tracking
func variablesQuotasTableSchema() *memdb.TableSchema {
return &memdb.TableSchema{
Name: TableVariablesQuotas,
Indexes: map[string]*memdb.IndexSchema{
indexID: {
Name: indexID,
AllowMissing: false,
Unique: true,
Indexer: &memdb.StringFieldIndex{
Field: "Namespace",
Lowercase: true,
},
},
},
}
}
// variablesRootKeyMetaSchema returns the MemDB schema for Nomad root keys
func variablesRootKeyMetaSchema() *memdb.TableSchema {
return &memdb.TableSchema{
Name: TableRootKeyMeta,
Indexes: map[string]*memdb.IndexSchema{
indexID: {
Name: indexID,
AllowMissing: false,
Unique: true,
Indexer: &memdb.StringFieldIndex{
Field: "KeyID",
Lowercase: true,
},
},
},
}
}
func aclRolesTableSchema() *memdb.TableSchema {
return &memdb.TableSchema{
Name: TableACLRoles,
Indexes: map[string]*memdb.IndexSchema{
indexID: {
Name: indexID,
AllowMissing: false,
Unique: true,
Indexer: &memdb.StringFieldIndex{
Field: "ID",
},
},
indexName: {
Name: indexName,
AllowMissing: false,
Unique: true,
Indexer: &memdb.StringFieldIndex{
Field: "Name",
},
},
},
}
}
func aclAuthMethodsTableSchema() *memdb.TableSchema {
return &memdb.TableSchema{
Name: TableACLAuthMethods,
Indexes: map[string]*memdb.IndexSchema{
indexID: {
Name: indexID,
AllowMissing: false,
Unique: true,
Indexer: &memdb.StringFieldIndex{
Field: "Name",
},
},
},
}
}
func bindingRulesTableSchema() *memdb.TableSchema {
return &memdb.TableSchema{
Name: TableACLBindingRules,
Indexes: map[string]*memdb.IndexSchema{
indexID: {
Name: indexID,
AllowMissing: false,
Unique: true,
Indexer: &memdb.StringFieldIndex{
Field: "ID",
},
},
indexAuthMethod: {
Name: indexAuthMethod,
AllowMissing: false,
Unique: false,
Indexer: &memdb.StringFieldIndex{
Field: "AuthMethod",
},
},
},
}
}