open-nomad/nomad/state/schema.go
Tim Gross bf57d76ec7
allow ACL policies to be associated with workload identity (#14140)
The original design for workload identities and ACLs allows for operators to
extend the automatic capabilities of a workload by using a specially-named
policy. This has shown to be potentially unsafe because of naming collisions, so
instead we'll allow operators to explicitly attach a policy to a workload
identity.

This changeset adds workload identity fields to ACL policy objects and threads
that all the way down to the command line. It also a new secondary index to the
ACL policy table on namespace and job so that claim resolution can efficiently
query for related policies.
2022-08-22 16:41:21 -04:00

1411 lines
34 KiB
Go

package state
import (
"fmt"
"sync"
memdb "github.com/hashicorp/go-memdb"
"github.com/hashicorp/nomad/nomad/structs"
)
const (
tableIndex = "index"
TableNamespaces = "namespaces"
TableServiceRegistrations = "service_registrations"
TableSecureVariables = "secure_variables"
TableSecureVariablesQuotas = "secure_variables_quota"
TableRootKeyMeta = "secure_variables_root_key_meta"
)
const (
indexID = "id"
indexJob = "job"
indexNodeID = "node_id"
indexAllocID = "alloc_id"
indexServiceName = "service_name"
indexKeyID = "key_id"
indexPath = "path"
)
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,
deploymentSchema,
periodicLaunchTableSchema,
evalTableSchema,
allocTableSchema,
vaultAccessorTableSchema,
siTokenAccessorTableSchema,
aclPolicyTableSchema,
aclTokenTableSchema,
oneTimeTokenTableSchema,
autopilotConfigTableSchema,
schedulerConfigTableSchema,
clusterMetaTableSchema,
csiVolumeTableSchema,
csiPluginTableSchema,
scalingPolicyTableSchema,
scalingEventTableSchema,
namespaceTableSchema,
serviceRegistrationsTableSchema,
secureVariablesTableSchema,
secureVariablesQuotasTableSchema,
secureVariablesRootKeyMetaSchema,
}...)
}
// 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",
},
},
},
},
},
}
}
// 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",
},
},
},
}
}
// 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",
},
},
},
}
}
// 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",
},
},
},
}
}
// secureVariablesTableSchema returns the MemDB schema for Nomad
// secure variables.
func secureVariablesTableSchema() *memdb.TableSchema {
return &memdb.TableSchema{
Name: TableSecureVariables,
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: &secureVariableKeyIDFieldIndexer{},
},
indexPath: {
Name: indexPath,
AllowMissing: false,
Unique: false,
Indexer: &memdb.StringFieldIndex{
Field: "Path",
},
},
},
}
}
type secureVariableKeyIDFieldIndexer struct{}
// FromArgs implements go-memdb/Indexer and is used to build an exact
// index lookup based on arguments
func (s *secureVariableKeyIDFieldIndexer) 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 *secureVariableKeyIDFieldIndexer) 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 *secureVariableKeyIDFieldIndexer) FromObject(obj interface{}) (bool, []byte, error) {
variable, ok := obj.(*structs.SecureVariableEncrypted)
if !ok {
return false, nil, fmt.Errorf("object %#v is not a SecureVariable", obj)
}
keyID := variable.KeyID
if keyID == "" {
return false, nil, nil
}
// Add the null character as a terminator
keyID += "\x00"
return true, []byte(keyID), nil
}
// secureVariablesQuotasTableSchema returns the MemDB schema for Nomad
// secure variables quotas tracking
func secureVariablesQuotasTableSchema() *memdb.TableSchema {
return &memdb.TableSchema{
Name: TableSecureVariablesQuotas,
Indexes: map[string]*memdb.IndexSchema{
indexID: {
Name: indexID,
AllowMissing: false,
Unique: true,
Indexer: &memdb.StringFieldIndex{
Field: "Namespace",
Lowercase: true,
},
},
},
}
}
// secureVariablesRootKeyMetaSchema returns the MemDB schema for Nomad
// secure variables root keys
func secureVariablesRootKeyMetaSchema() *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,
},
},
},
}
}