dynamic host volumes: search endpoint (#24531)

Add support for dynamic host volumes to the search endpoint. Like many other
objects with UUID identifiers, we're not supporting fuzzy search here, just
prefix search on the fuzzy search endpoint.

Because the search endpoint only returns IDs, we need to seperate CSI volumes
and host volumes for it to be useful. The new context is called `"host_volumes"`
to disambiguate it from `"volumes"`. In future versions of Nomad we should
consider deprecating the `"volumes"` context in lieu of a `"csi_volumes"`
context.

Ref: https://github.com/hashicorp/nomad/pull/24479
This commit is contained in:
Tim Gross
2024-11-22 13:23:19 -05:00
parent 298460dcd9
commit 926925ba16
6 changed files with 148 additions and 2 deletions

View File

@@ -23,6 +23,7 @@ const (
Plugins Context = "plugins" Plugins Context = "plugins"
Variables Context = "vars" Variables Context = "vars"
Volumes Context = "volumes" Volumes Context = "volumes"
HostVolumes Context = "host_volumes"
// These Context types are used to associate a search result from a lower // These Context types are used to associate a search result from a lower
// level Nomad object with one of the higher level Context types above. // level Nomad object with one of the higher level Context types above.

View File

@@ -41,6 +41,7 @@ var (
structs.ScalingPolicies, structs.ScalingPolicies,
structs.Variables, structs.Variables,
structs.Namespaces, structs.Namespaces,
structs.HostVolumes,
} }
) )
@@ -84,6 +85,8 @@ func (s *Search) getPrefixMatches(iter memdb.ResultIterator, prefix string) ([]s
id = t.ID id = t.ID
case *structs.CSIVolume: case *structs.CSIVolume:
id = t.ID id = t.ID
case *structs.HostVolume:
id = t.ID
case *structs.ScalingPolicy: case *structs.ScalingPolicy:
id = t.ID id = t.ID
case *structs.Namespace: case *structs.Namespace:
@@ -405,6 +408,8 @@ func getResourceIter(context structs.Context, aclObj *acl.ACL, namespace, prefix
return store.ScalingPoliciesByIDPrefix(ws, namespace, prefix) return store.ScalingPoliciesByIDPrefix(ws, namespace, prefix)
case structs.Volumes: case structs.Volumes:
return store.CSIVolumesByIDPrefix(ws, namespace, prefix) return store.CSIVolumesByIDPrefix(ws, namespace, prefix)
case structs.HostVolumes:
return store.HostVolumesByIDPrefix(ws, namespace, prefix, state.SortDefault)
case structs.Namespaces: case structs.Namespaces:
iter, err := store.NamespacesByNamePrefix(ws, prefix) iter, err := store.NamespacesByNamePrefix(ws, prefix)
if err != nil { if err != nil {
@@ -684,6 +689,8 @@ func sufficientSearchPerms(aclObj *acl.ACL, namespace string, context structs.Co
acl.NamespaceCapabilityCSIReadVolume, acl.NamespaceCapabilityCSIReadVolume,
acl.NamespaceCapabilityListJobs, acl.NamespaceCapabilityListJobs,
acl.NamespaceCapabilityReadJob)(aclObj, namespace) acl.NamespaceCapabilityReadJob)(aclObj, namespace)
case structs.HostVolumes:
return acl.NamespaceValidator(acl.NamespaceCapabilityHostVolumeRead)(aclObj, namespace)
case structs.Variables: case structs.Variables:
return aclObj.AllowVariableSearch(namespace) return aclObj.AllowVariableSearch(namespace)
case structs.Plugins: case structs.Plugins:
@@ -774,7 +781,8 @@ func (s *Search) FuzzySearch(args *structs.FuzzySearchRequest, reply *structs.Fu
for _, ctx := range prefixContexts { for _, ctx := range prefixContexts {
switch ctx { switch ctx {
// only apply on the types that use UUID prefix searching // only apply on the types that use UUID prefix searching
case structs.Evals, structs.Deployments, structs.ScalingPolicies, structs.Volumes, structs.Quotas, structs.Recommendations: case structs.Evals, structs.Deployments, structs.ScalingPolicies,
structs.Volumes, structs.HostVolumes, structs.Quotas, structs.Recommendations:
iter, err := getResourceIter(ctx, aclObj, namespace, roundUUIDDownIfOdd(args.Prefix, args.Context), ws, state) iter, err := getResourceIter(ctx, aclObj, namespace, roundUUIDDownIfOdd(args.Prefix, args.Context), ws, state)
if err != nil { if err != nil {
if !s.silenceError(err) { if !s.silenceError(err) {
@@ -790,7 +798,9 @@ func (s *Search) FuzzySearch(args *structs.FuzzySearchRequest, reply *structs.Fu
for _, ctx := range fuzzyContexts { for _, ctx := range fuzzyContexts {
switch ctx { switch ctx {
// skip the types that use UUID prefix searching // skip the types that use UUID prefix searching
case structs.Evals, structs.Deployments, structs.ScalingPolicies, structs.Volumes, structs.Quotas, structs.Recommendations: case structs.Evals, structs.Deployments, structs.ScalingPolicies,
structs.Volumes, structs.HostVolumes, structs.Quotas,
structs.Recommendations:
continue continue
default: default:
iter, err := getFuzzyResourceIterator(ctx, aclObj, namespace, ws, state) iter, err := getFuzzyResourceIterator(ctx, aclObj, namespace, ws, state)
@@ -927,6 +937,11 @@ func filteredSearchContexts(aclObj *acl.ACL, namespace string, context structs.C
if volRead { if volRead {
available = append(available, c) available = append(available, c)
} }
case structs.HostVolumes:
if acl.NamespaceValidator(
acl.NamespaceCapabilityHostVolumeRead)(aclObj, namespace) {
available = append(available, c)
}
case structs.Plugins: case structs.Plugins:
if aclObj.AllowPluginList() { if aclObj.AllowPluginList() {
available = append(available, c) available = append(available, c)

View File

@@ -1039,6 +1039,53 @@ func TestSearch_PrefixSearch_CSIVolume(t *testing.T) {
require.False(t, resp.Truncations[structs.Volumes]) require.False(t, resp.Truncations[structs.Volumes])
} }
func TestSearch_PrefixSearch_HostVolume(t *testing.T) {
ci.Parallel(t)
srv, cleanup := TestServer(t, func(c *Config) {
c.NumSchedulers = 0
})
defer cleanup()
codec := rpcClient(t, srv)
testutil.WaitForLeader(t, srv.RPC)
store := srv.fsm.State()
index, _ := store.LatestIndex()
node := mock.Node()
index++
must.NoError(t, store.UpsertNode(structs.MsgTypeTestSetup, index, node))
id := uuid.Generate()
index++
err := store.UpsertHostVolumes(index, []*structs.HostVolume{{
ID: id,
Name: "example",
Namespace: structs.DefaultNamespace,
PluginID: "glade",
NodeID: node.ID,
NodePool: node.NodePool,
}})
must.NoError(t, err)
req := &structs.SearchRequest{
Prefix: id[:6],
Context: structs.HostVolumes,
QueryOptions: structs.QueryOptions{
Region: "global",
Namespace: structs.DefaultNamespace,
},
}
var resp structs.SearchResponse
must.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
must.Len(t, 1, resp.Matches[structs.HostVolumes])
must.Len(t, 0, resp.Matches[structs.Volumes])
must.Eq(t, id, resp.Matches[structs.HostVolumes][0])
must.False(t, resp.Truncations[structs.HostVolumes])
}
func TestSearch_PrefixSearch_Namespace(t *testing.T) { func TestSearch_PrefixSearch_Namespace(t *testing.T) {
ci.Parallel(t) ci.Parallel(t)
@@ -1932,6 +1979,52 @@ func TestSearch_FuzzySearch_CSIVolume(t *testing.T) {
require.False(t, resp.Truncations[structs.Volumes]) require.False(t, resp.Truncations[structs.Volumes])
} }
func TestSearch_FuzzySearch_HostVolume(t *testing.T) {
ci.Parallel(t)
srv, cleanup := TestServer(t, func(c *Config) {
c.NumSchedulers = 0
})
defer cleanup()
codec := rpcClient(t, srv)
testutil.WaitForLeader(t, srv.RPC)
store := srv.fsm.State()
index, _ := store.LatestIndex()
node := mock.Node()
index++
must.NoError(t, store.UpsertNode(structs.MsgTypeTestSetup, index, node))
id := uuid.Generate()
index++
err := store.UpsertHostVolumes(index, []*structs.HostVolume{{
ID: id,
Name: "example",
Namespace: structs.DefaultNamespace,
PluginID: "glade",
NodeID: node.ID,
NodePool: node.NodePool,
}})
must.NoError(t, err)
req := &structs.FuzzySearchRequest{
Text: id[0:3], // volumes are prefix searched
Context: structs.HostVolumes,
QueryOptions: structs.QueryOptions{
Region: "global",
Namespace: structs.DefaultNamespace,
},
}
var resp structs.FuzzySearchResponse
must.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
must.Len(t, 1, resp.Matches[structs.HostVolumes])
must.Eq(t, id, resp.Matches[structs.HostVolumes][0].ID)
must.False(t, resp.Truncations[structs.HostVolumes])
}
func TestSearch_FuzzySearch_Namespace(t *testing.T) { func TestSearch_FuzzySearch_Namespace(t *testing.T) {
ci.Parallel(t) ci.Parallel(t)

View File

@@ -5,6 +5,7 @@ package state
import ( import (
"fmt" "fmt"
"strings"
memdb "github.com/hashicorp/go-memdb" memdb "github.com/hashicorp/go-memdb"
"github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/nomad/structs"
@@ -156,6 +157,30 @@ func (s *StateStore) HostVolumes(ws memdb.WatchSet, sort SortOption) (memdb.Resu
return s.hostVolumesIter(ws, indexID, sort) return s.hostVolumesIter(ws, indexID, sort)
} }
// HostVolumesByIDPrefix retrieves all host volumes by ID prefix. Because the ID
// index is namespaced, we need to handle the wildcard namespace here as well.
func (s *StateStore) HostVolumesByIDPrefix(ws memdb.WatchSet, ns, prefix string, sort SortOption) (memdb.ResultIterator, error) {
if ns != structs.AllNamespacesSentinel {
return s.hostVolumesIter(ws, "id_prefix", sort, ns, prefix)
}
// for wildcard namespace, wrap the iterator in a filter function that
// filters all volumes by prefix
iter, err := s.hostVolumesIter(ws, indexID, sort)
if err != nil {
return nil, err
}
wrappedIter := memdb.NewFilterIterator(iter, func(raw any) bool {
vol, ok := raw.(*structs.HostVolume)
if !ok {
return true
}
return !strings.HasPrefix(vol.ID, prefix)
})
return wrappedIter, nil
}
// HostVolumesByName retrieves all host volumes of the same name // HostVolumesByName retrieves all host volumes of the same name
func (s *StateStore) HostVolumesByName(ws memdb.WatchSet, ns, name string, sort SortOption) (memdb.ResultIterator, error) { func (s *StateStore) HostVolumesByName(ws memdb.WatchSet, ns, name string, sort SortOption) (memdb.ResultIterator, error) {
return s.hostVolumesIter(ws, "name_prefix", sort, ns, name) return s.hostVolumesIter(ws, "name_prefix", sort, ns, name)

View File

@@ -163,6 +163,17 @@ func TestStateStore_HostVolumes_CRUD(t *testing.T) {
must.NoError(t, err) must.NoError(t, err)
got = consumeIter(iter) got = consumeIter(iter)
must.MapLen(t, 3, got, must.Sprint(`expected 3 volumes remain`)) must.MapLen(t, 3, got, must.Sprint(`expected 3 volumes remain`))
prefix := vol.ID[:30] // sufficiently long prefix to avoid flakes
iter, err = store.HostVolumesByIDPrefix(nil, "*", prefix, SortDefault)
must.NoError(t, err)
got = consumeIter(iter)
must.MapLen(t, 1, got, must.Sprint(`expected only one volume to match prefix`))
iter, err = store.HostVolumesByIDPrefix(nil, vol.Namespace, prefix, SortDefault)
must.NoError(t, err)
got = consumeIter(iter)
must.MapLen(t, 1, got, must.Sprint(`expected only one volume to match prefix`))
} }
func TestStateStore_UpdateHostVolumesFromFingerprint(t *testing.T) { func TestStateStore_UpdateHostVolumesFromFingerprint(t *testing.T) {

View File

@@ -22,6 +22,7 @@ const (
Plugins Context = "plugins" Plugins Context = "plugins"
Variables Context = "vars" Variables Context = "vars"
Volumes Context = "volumes" Volumes Context = "volumes"
HostVolumes Context = "host_volumes"
// Subtypes used in fuzzy matching. // Subtypes used in fuzzy matching.
Groups Context = "groups" Groups Context = "groups"