1
0
Fork 0
mirror of https://github.com/kubernetes-sigs/node-feature-discovery.git synced 2024-12-14 11:57:51 +00:00

Add NodeFeatureGroup CRD

The NodeFeatureGroup is an NFD-specific custom resource that is designed for
grouping nodes based on their features. NFD-Master watches for NodeFeatureGroup
objects in the cluster and updates the status of the NodeFeatureGroup object
with the list of nodes that match the feature group rules. The NodeFeatureGroup
rules follow the same syntax as the NodeFeatureRule rules.

Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
This commit is contained in:
Carlos Eduardo Arango Gutierrez 2024-04-17 14:33:01 +02:00
parent 6644b6a7f6
commit 47c054e1db
No known key found for this signature in database
GPG key ID: F19CDD72A695437A
38 changed files with 2063 additions and 218 deletions

View file

@ -4,8 +4,6 @@ FROM ${BUILDER_IMAGE} as builder
# Install tools # Install tools
RUN go install github.com/vektra/mockery/v2@v2.42.0 && \ RUN go install github.com/vektra/mockery/v2@v2.42.0 && \
go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.14.0 && \ go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.14.0 && \
git clone https://github.com/kubernetes/code-generator -b v0.29.0 --depth 1 && \
go install k8s.io/code-generator/cmd/go-to-protobuf/...@v0.29.0 && \
go install golang.org/x/tools/cmd/goimports@v0.11.0 && \ go install golang.org/x/tools/cmd/goimports@v0.11.0 && \
go install github.com/golang/protobuf/protoc-gen-go@v1.4.3 go install github.com/golang/protobuf/protoc-gen-go@v1.4.3

View file

@ -32,6 +32,10 @@ func (c *FakeNfdV1alpha1) NodeFeatures(namespace string) v1alpha1.NodeFeatureInt
return &FakeNodeFeatures{c, namespace} return &FakeNodeFeatures{c, namespace}
} }
func (c *FakeNfdV1alpha1) NodeFeatureGroups(namespace string) v1alpha1.NodeFeatureGroupInterface {
return &FakeNodeFeatureGroups{c, namespace}
}
func (c *FakeNfdV1alpha1) NodeFeatureRules() v1alpha1.NodeFeatureRuleInterface { func (c *FakeNfdV1alpha1) NodeFeatureRules() v1alpha1.NodeFeatureRuleInterface {
return &FakeNodeFeatureRules{c} return &FakeNodeFeatureRules{c}
} }

View file

@ -0,0 +1,141 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by client-gen. DO NOT EDIT.
package fake
import (
"context"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
labels "k8s.io/apimachinery/pkg/labels"
types "k8s.io/apimachinery/pkg/types"
watch "k8s.io/apimachinery/pkg/watch"
testing "k8s.io/client-go/testing"
v1alpha1 "sigs.k8s.io/node-feature-discovery/api/nfd/v1alpha1"
)
// FakeNodeFeatureGroups implements NodeFeatureGroupInterface
type FakeNodeFeatureGroups struct {
Fake *FakeNfdV1alpha1
ns string
}
var nodefeaturegroupsResource = v1alpha1.SchemeGroupVersion.WithResource("nodefeaturegroups")
var nodefeaturegroupsKind = v1alpha1.SchemeGroupVersion.WithKind("NodeFeatureGroup")
// Get takes name of the nodeFeatureGroup, and returns the corresponding nodeFeatureGroup object, and an error if there is any.
func (c *FakeNodeFeatureGroups) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.NodeFeatureGroup, err error) {
obj, err := c.Fake.
Invokes(testing.NewGetAction(nodefeaturegroupsResource, c.ns, name), &v1alpha1.NodeFeatureGroup{})
if obj == nil {
return nil, err
}
return obj.(*v1alpha1.NodeFeatureGroup), err
}
// List takes label and field selectors, and returns the list of NodeFeatureGroups that match those selectors.
func (c *FakeNodeFeatureGroups) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.NodeFeatureGroupList, err error) {
obj, err := c.Fake.
Invokes(testing.NewListAction(nodefeaturegroupsResource, nodefeaturegroupsKind, c.ns, opts), &v1alpha1.NodeFeatureGroupList{})
if obj == nil {
return nil, err
}
label, _, _ := testing.ExtractFromListOptions(opts)
if label == nil {
label = labels.Everything()
}
list := &v1alpha1.NodeFeatureGroupList{ListMeta: obj.(*v1alpha1.NodeFeatureGroupList).ListMeta}
for _, item := range obj.(*v1alpha1.NodeFeatureGroupList).Items {
if label.Matches(labels.Set(item.Labels)) {
list.Items = append(list.Items, item)
}
}
return list, err
}
// Watch returns a watch.Interface that watches the requested nodeFeatureGroups.
func (c *FakeNodeFeatureGroups) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
return c.Fake.
InvokesWatch(testing.NewWatchAction(nodefeaturegroupsResource, c.ns, opts))
}
// Create takes the representation of a nodeFeatureGroup and creates it. Returns the server's representation of the nodeFeatureGroup, and an error, if there is any.
func (c *FakeNodeFeatureGroups) Create(ctx context.Context, nodeFeatureGroup *v1alpha1.NodeFeatureGroup, opts v1.CreateOptions) (result *v1alpha1.NodeFeatureGroup, err error) {
obj, err := c.Fake.
Invokes(testing.NewCreateAction(nodefeaturegroupsResource, c.ns, nodeFeatureGroup), &v1alpha1.NodeFeatureGroup{})
if obj == nil {
return nil, err
}
return obj.(*v1alpha1.NodeFeatureGroup), err
}
// Update takes the representation of a nodeFeatureGroup and updates it. Returns the server's representation of the nodeFeatureGroup, and an error, if there is any.
func (c *FakeNodeFeatureGroups) Update(ctx context.Context, nodeFeatureGroup *v1alpha1.NodeFeatureGroup, opts v1.UpdateOptions) (result *v1alpha1.NodeFeatureGroup, err error) {
obj, err := c.Fake.
Invokes(testing.NewUpdateAction(nodefeaturegroupsResource, c.ns, nodeFeatureGroup), &v1alpha1.NodeFeatureGroup{})
if obj == nil {
return nil, err
}
return obj.(*v1alpha1.NodeFeatureGroup), err
}
// UpdateStatus was generated because the type contains a Status member.
// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
func (c *FakeNodeFeatureGroups) UpdateStatus(ctx context.Context, nodeFeatureGroup *v1alpha1.NodeFeatureGroup, opts v1.UpdateOptions) (*v1alpha1.NodeFeatureGroup, error) {
obj, err := c.Fake.
Invokes(testing.NewUpdateSubresourceAction(nodefeaturegroupsResource, "status", c.ns, nodeFeatureGroup), &v1alpha1.NodeFeatureGroup{})
if obj == nil {
return nil, err
}
return obj.(*v1alpha1.NodeFeatureGroup), err
}
// Delete takes name of the nodeFeatureGroup and deletes it. Returns an error if one occurs.
func (c *FakeNodeFeatureGroups) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
_, err := c.Fake.
Invokes(testing.NewDeleteActionWithOptions(nodefeaturegroupsResource, c.ns, name, opts), &v1alpha1.NodeFeatureGroup{})
return err
}
// DeleteCollection deletes a collection of objects.
func (c *FakeNodeFeatureGroups) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
action := testing.NewDeleteCollectionAction(nodefeaturegroupsResource, c.ns, listOpts)
_, err := c.Fake.Invokes(action, &v1alpha1.NodeFeatureGroupList{})
return err
}
// Patch applies the patch and returns the patched nodeFeatureGroup.
func (c *FakeNodeFeatureGroups) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.NodeFeatureGroup, err error) {
obj, err := c.Fake.
Invokes(testing.NewPatchSubresourceAction(nodefeaturegroupsResource, c.ns, name, pt, data, subresources...), &v1alpha1.NodeFeatureGroup{})
if obj == nil {
return nil, err
}
return obj.(*v1alpha1.NodeFeatureGroup), err
}

View file

@ -20,4 +20,6 @@ package v1alpha1
type NodeFeatureExpansion interface{} type NodeFeatureExpansion interface{}
type NodeFeatureGroupExpansion interface{}
type NodeFeatureRuleExpansion interface{} type NodeFeatureRuleExpansion interface{}

View file

@ -29,6 +29,7 @@ import (
type NfdV1alpha1Interface interface { type NfdV1alpha1Interface interface {
RESTClient() rest.Interface RESTClient() rest.Interface
NodeFeaturesGetter NodeFeaturesGetter
NodeFeatureGroupsGetter
NodeFeatureRulesGetter NodeFeatureRulesGetter
} }
@ -41,6 +42,10 @@ func (c *NfdV1alpha1Client) NodeFeatures(namespace string) NodeFeatureInterface
return newNodeFeatures(c, namespace) return newNodeFeatures(c, namespace)
} }
func (c *NfdV1alpha1Client) NodeFeatureGroups(namespace string) NodeFeatureGroupInterface {
return newNodeFeatureGroups(c, namespace)
}
func (c *NfdV1alpha1Client) NodeFeatureRules() NodeFeatureRuleInterface { func (c *NfdV1alpha1Client) NodeFeatureRules() NodeFeatureRuleInterface {
return newNodeFeatureRules(c) return newNodeFeatureRules(c)
} }

View file

@ -0,0 +1,195 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by client-gen. DO NOT EDIT.
package v1alpha1
import (
"context"
"time"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
types "k8s.io/apimachinery/pkg/types"
watch "k8s.io/apimachinery/pkg/watch"
rest "k8s.io/client-go/rest"
scheme "sigs.k8s.io/node-feature-discovery/api/generated/clientset/versioned/scheme"
v1alpha1 "sigs.k8s.io/node-feature-discovery/api/nfd/v1alpha1"
)
// NodeFeatureGroupsGetter has a method to return a NodeFeatureGroupInterface.
// A group's client should implement this interface.
type NodeFeatureGroupsGetter interface {
NodeFeatureGroups(namespace string) NodeFeatureGroupInterface
}
// NodeFeatureGroupInterface has methods to work with NodeFeatureGroup resources.
type NodeFeatureGroupInterface interface {
Create(ctx context.Context, nodeFeatureGroup *v1alpha1.NodeFeatureGroup, opts v1.CreateOptions) (*v1alpha1.NodeFeatureGroup, error)
Update(ctx context.Context, nodeFeatureGroup *v1alpha1.NodeFeatureGroup, opts v1.UpdateOptions) (*v1alpha1.NodeFeatureGroup, error)
UpdateStatus(ctx context.Context, nodeFeatureGroup *v1alpha1.NodeFeatureGroup, opts v1.UpdateOptions) (*v1alpha1.NodeFeatureGroup, error)
Delete(ctx context.Context, name string, opts v1.DeleteOptions) error
DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error
Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.NodeFeatureGroup, error)
List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.NodeFeatureGroupList, error)
Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error)
Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.NodeFeatureGroup, err error)
NodeFeatureGroupExpansion
}
// nodeFeatureGroups implements NodeFeatureGroupInterface
type nodeFeatureGroups struct {
client rest.Interface
ns string
}
// newNodeFeatureGroups returns a NodeFeatureGroups
func newNodeFeatureGroups(c *NfdV1alpha1Client, namespace string) *nodeFeatureGroups {
return &nodeFeatureGroups{
client: c.RESTClient(),
ns: namespace,
}
}
// Get takes name of the nodeFeatureGroup, and returns the corresponding nodeFeatureGroup object, and an error if there is any.
func (c *nodeFeatureGroups) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.NodeFeatureGroup, err error) {
result = &v1alpha1.NodeFeatureGroup{}
err = c.client.Get().
Namespace(c.ns).
Resource("nodefeaturegroups").
Name(name).
VersionedParams(&options, scheme.ParameterCodec).
Do(ctx).
Into(result)
return
}
// List takes label and field selectors, and returns the list of NodeFeatureGroups that match those selectors.
func (c *nodeFeatureGroups) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.NodeFeatureGroupList, err error) {
var timeout time.Duration
if opts.TimeoutSeconds != nil {
timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
}
result = &v1alpha1.NodeFeatureGroupList{}
err = c.client.Get().
Namespace(c.ns).
Resource("nodefeaturegroups").
VersionedParams(&opts, scheme.ParameterCodec).
Timeout(timeout).
Do(ctx).
Into(result)
return
}
// Watch returns a watch.Interface that watches the requested nodeFeatureGroups.
func (c *nodeFeatureGroups) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
var timeout time.Duration
if opts.TimeoutSeconds != nil {
timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
}
opts.Watch = true
return c.client.Get().
Namespace(c.ns).
Resource("nodefeaturegroups").
VersionedParams(&opts, scheme.ParameterCodec).
Timeout(timeout).
Watch(ctx)
}
// Create takes the representation of a nodeFeatureGroup and creates it. Returns the server's representation of the nodeFeatureGroup, and an error, if there is any.
func (c *nodeFeatureGroups) Create(ctx context.Context, nodeFeatureGroup *v1alpha1.NodeFeatureGroup, opts v1.CreateOptions) (result *v1alpha1.NodeFeatureGroup, err error) {
result = &v1alpha1.NodeFeatureGroup{}
err = c.client.Post().
Namespace(c.ns).
Resource("nodefeaturegroups").
VersionedParams(&opts, scheme.ParameterCodec).
Body(nodeFeatureGroup).
Do(ctx).
Into(result)
return
}
// Update takes the representation of a nodeFeatureGroup and updates it. Returns the server's representation of the nodeFeatureGroup, and an error, if there is any.
func (c *nodeFeatureGroups) Update(ctx context.Context, nodeFeatureGroup *v1alpha1.NodeFeatureGroup, opts v1.UpdateOptions) (result *v1alpha1.NodeFeatureGroup, err error) {
result = &v1alpha1.NodeFeatureGroup{}
err = c.client.Put().
Namespace(c.ns).
Resource("nodefeaturegroups").
Name(nodeFeatureGroup.Name).
VersionedParams(&opts, scheme.ParameterCodec).
Body(nodeFeatureGroup).
Do(ctx).
Into(result)
return
}
// UpdateStatus was generated because the type contains a Status member.
// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
func (c *nodeFeatureGroups) UpdateStatus(ctx context.Context, nodeFeatureGroup *v1alpha1.NodeFeatureGroup, opts v1.UpdateOptions) (result *v1alpha1.NodeFeatureGroup, err error) {
result = &v1alpha1.NodeFeatureGroup{}
err = c.client.Put().
Namespace(c.ns).
Resource("nodefeaturegroups").
Name(nodeFeatureGroup.Name).
SubResource("status").
VersionedParams(&opts, scheme.ParameterCodec).
Body(nodeFeatureGroup).
Do(ctx).
Into(result)
return
}
// Delete takes name of the nodeFeatureGroup and deletes it. Returns an error if one occurs.
func (c *nodeFeatureGroups) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
return c.client.Delete().
Namespace(c.ns).
Resource("nodefeaturegroups").
Name(name).
Body(&opts).
Do(ctx).
Error()
}
// DeleteCollection deletes a collection of objects.
func (c *nodeFeatureGroups) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
var timeout time.Duration
if listOpts.TimeoutSeconds != nil {
timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second
}
return c.client.Delete().
Namespace(c.ns).
Resource("nodefeaturegroups").
VersionedParams(&listOpts, scheme.ParameterCodec).
Timeout(timeout).
Body(&opts).
Do(ctx).
Error()
}
// Patch applies the patch and returns the patched nodeFeatureGroup.
func (c *nodeFeatureGroups) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.NodeFeatureGroup, err error) {
result = &v1alpha1.NodeFeatureGroup{}
err = c.client.Patch(pt).
Namespace(c.ns).
Resource("nodefeaturegroups").
Name(name).
SubResource(subresources...).
VersionedParams(&opts, scheme.ParameterCodec).
Body(data).
Do(ctx).
Into(result)
return
}

View file

@ -55,6 +55,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource
// Group=nfd.k8s-sigs.io, Version=v1alpha1 // Group=nfd.k8s-sigs.io, Version=v1alpha1
case v1alpha1.SchemeGroupVersion.WithResource("nodefeatures"): case v1alpha1.SchemeGroupVersion.WithResource("nodefeatures"):
return &genericInformer{resource: resource.GroupResource(), informer: f.Nfd().V1alpha1().NodeFeatures().Informer()}, nil return &genericInformer{resource: resource.GroupResource(), informer: f.Nfd().V1alpha1().NodeFeatures().Informer()}, nil
case v1alpha1.SchemeGroupVersion.WithResource("nodefeaturegroups"):
return &genericInformer{resource: resource.GroupResource(), informer: f.Nfd().V1alpha1().NodeFeatureGroups().Informer()}, nil
case v1alpha1.SchemeGroupVersion.WithResource("nodefeaturerules"): case v1alpha1.SchemeGroupVersion.WithResource("nodefeaturerules"):
return &genericInformer{resource: resource.GroupResource(), informer: f.Nfd().V1alpha1().NodeFeatureRules().Informer()}, nil return &genericInformer{resource: resource.GroupResource(), informer: f.Nfd().V1alpha1().NodeFeatureRules().Informer()}, nil

View file

@ -26,6 +26,8 @@ import (
type Interface interface { type Interface interface {
// NodeFeatures returns a NodeFeatureInformer. // NodeFeatures returns a NodeFeatureInformer.
NodeFeatures() NodeFeatureInformer NodeFeatures() NodeFeatureInformer
// NodeFeatureGroups returns a NodeFeatureGroupInformer.
NodeFeatureGroups() NodeFeatureGroupInformer
// NodeFeatureRules returns a NodeFeatureRuleInformer. // NodeFeatureRules returns a NodeFeatureRuleInformer.
NodeFeatureRules() NodeFeatureRuleInformer NodeFeatureRules() NodeFeatureRuleInformer
} }
@ -46,6 +48,11 @@ func (v *version) NodeFeatures() NodeFeatureInformer {
return &nodeFeatureInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} return &nodeFeatureInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
} }
// NodeFeatureGroups returns a NodeFeatureGroupInformer.
func (v *version) NodeFeatureGroups() NodeFeatureGroupInformer {
return &nodeFeatureGroupInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
}
// NodeFeatureRules returns a NodeFeatureRuleInformer. // NodeFeatureRules returns a NodeFeatureRuleInformer.
func (v *version) NodeFeatureRules() NodeFeatureRuleInformer { func (v *version) NodeFeatureRules() NodeFeatureRuleInformer {
return &nodeFeatureRuleInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} return &nodeFeatureRuleInformer{factory: v.factory, tweakListOptions: v.tweakListOptions}

View file

@ -0,0 +1,90 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by informer-gen. DO NOT EDIT.
package v1alpha1
import (
"context"
time "time"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
watch "k8s.io/apimachinery/pkg/watch"
cache "k8s.io/client-go/tools/cache"
versioned "sigs.k8s.io/node-feature-discovery/api/generated/clientset/versioned"
internalinterfaces "sigs.k8s.io/node-feature-discovery/api/generated/informers/externalversions/internalinterfaces"
v1alpha1 "sigs.k8s.io/node-feature-discovery/api/generated/listers/nfd/v1alpha1"
nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/api/nfd/v1alpha1"
)
// NodeFeatureGroupInformer provides access to a shared informer and lister for
// NodeFeatureGroups.
type NodeFeatureGroupInformer interface {
Informer() cache.SharedIndexInformer
Lister() v1alpha1.NodeFeatureGroupLister
}
type nodeFeatureGroupInformer struct {
factory internalinterfaces.SharedInformerFactory
tweakListOptions internalinterfaces.TweakListOptionsFunc
namespace string
}
// NewNodeFeatureGroupInformer constructs a new informer for NodeFeatureGroup type.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewNodeFeatureGroupInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
return NewFilteredNodeFeatureGroupInformer(client, namespace, resyncPeriod, indexers, nil)
}
// NewFilteredNodeFeatureGroupInformer constructs a new informer for NodeFeatureGroup type.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewFilteredNodeFeatureGroupInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
return cache.NewSharedIndexInformer(
&cache.ListWatch{
ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
}
return client.NfdV1alpha1().NodeFeatureGroups(namespace).List(context.TODO(), options)
},
WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
}
return client.NfdV1alpha1().NodeFeatureGroups(namespace).Watch(context.TODO(), options)
},
},
&nfdv1alpha1.NodeFeatureGroup{},
resyncPeriod,
indexers,
)
}
func (f *nodeFeatureGroupInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
return NewFilteredNodeFeatureGroupInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
}
func (f *nodeFeatureGroupInformer) Informer() cache.SharedIndexInformer {
return f.factory.InformerFor(&nfdv1alpha1.NodeFeatureGroup{}, f.defaultInformer)
}
func (f *nodeFeatureGroupInformer) Lister() v1alpha1.NodeFeatureGroupLister {
return v1alpha1.NewNodeFeatureGroupLister(f.Informer().GetIndexer())
}

View file

@ -26,6 +26,14 @@ type NodeFeatureListerExpansion interface{}
// NodeFeatureNamespaceLister. // NodeFeatureNamespaceLister.
type NodeFeatureNamespaceListerExpansion interface{} type NodeFeatureNamespaceListerExpansion interface{}
// NodeFeatureGroupListerExpansion allows custom methods to be added to
// NodeFeatureGroupLister.
type NodeFeatureGroupListerExpansion interface{}
// NodeFeatureGroupNamespaceListerExpansion allows custom methods to be added to
// NodeFeatureGroupNamespaceLister.
type NodeFeatureGroupNamespaceListerExpansion interface{}
// NodeFeatureRuleListerExpansion allows custom methods to be added to // NodeFeatureRuleListerExpansion allows custom methods to be added to
// NodeFeatureRuleLister. // NodeFeatureRuleLister.
type NodeFeatureRuleListerExpansion interface{} type NodeFeatureRuleListerExpansion interface{}

View file

@ -0,0 +1,99 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by lister-gen. DO NOT EDIT.
package v1alpha1
import (
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/tools/cache"
v1alpha1 "sigs.k8s.io/node-feature-discovery/api/nfd/v1alpha1"
)
// NodeFeatureGroupLister helps list NodeFeatureGroups.
// All objects returned here must be treated as read-only.
type NodeFeatureGroupLister interface {
// List lists all NodeFeatureGroups in the indexer.
// Objects returned here must be treated as read-only.
List(selector labels.Selector) (ret []*v1alpha1.NodeFeatureGroup, err error)
// NodeFeatureGroups returns an object that can list and get NodeFeatureGroups.
NodeFeatureGroups(namespace string) NodeFeatureGroupNamespaceLister
NodeFeatureGroupListerExpansion
}
// nodeFeatureGroupLister implements the NodeFeatureGroupLister interface.
type nodeFeatureGroupLister struct {
indexer cache.Indexer
}
// NewNodeFeatureGroupLister returns a new NodeFeatureGroupLister.
func NewNodeFeatureGroupLister(indexer cache.Indexer) NodeFeatureGroupLister {
return &nodeFeatureGroupLister{indexer: indexer}
}
// List lists all NodeFeatureGroups in the indexer.
func (s *nodeFeatureGroupLister) List(selector labels.Selector) (ret []*v1alpha1.NodeFeatureGroup, err error) {
err = cache.ListAll(s.indexer, selector, func(m interface{}) {
ret = append(ret, m.(*v1alpha1.NodeFeatureGroup))
})
return ret, err
}
// NodeFeatureGroups returns an object that can list and get NodeFeatureGroups.
func (s *nodeFeatureGroupLister) NodeFeatureGroups(namespace string) NodeFeatureGroupNamespaceLister {
return nodeFeatureGroupNamespaceLister{indexer: s.indexer, namespace: namespace}
}
// NodeFeatureGroupNamespaceLister helps list and get NodeFeatureGroups.
// All objects returned here must be treated as read-only.
type NodeFeatureGroupNamespaceLister interface {
// List lists all NodeFeatureGroups in the indexer for a given namespace.
// Objects returned here must be treated as read-only.
List(selector labels.Selector) (ret []*v1alpha1.NodeFeatureGroup, err error)
// Get retrieves the NodeFeatureGroup from the indexer for a given namespace and name.
// Objects returned here must be treated as read-only.
Get(name string) (*v1alpha1.NodeFeatureGroup, error)
NodeFeatureGroupNamespaceListerExpansion
}
// nodeFeatureGroupNamespaceLister implements the NodeFeatureGroupNamespaceLister
// interface.
type nodeFeatureGroupNamespaceLister struct {
indexer cache.Indexer
namespace string
}
// List lists all NodeFeatureGroups in the indexer for a given namespace.
func (s nodeFeatureGroupNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.NodeFeatureGroup, err error) {
err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) {
ret = append(ret, m.(*v1alpha1.NodeFeatureGroup))
})
return ret, err
}
// Get retrieves the NodeFeatureGroup from the indexer for a given namespace and name.
func (s nodeFeatureGroupNamespaceLister) Get(name string) (*v1alpha1.NodeFeatureGroup, error) {
obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name)
if err != nil {
return nil, err
}
if !exists {
return nil, errors.NewNotFound(v1alpha1.Resource("nodefeaturegroup"), name)
}
return obj.(*v1alpha1.NodeFeatureGroup), nil
}

View file

@ -42,6 +42,7 @@ func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion, scheme.AddKnownTypes(SchemeGroupVersion,
&NodeFeature{}, &NodeFeature{},
&NodeFeatureRule{}, &NodeFeatureRule{},
&NodeFeatureGroup{},
) )
metav1.AddToGroupVersion(scheme, SchemeGroupVersion) metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil return nil

View file

@ -131,6 +131,63 @@ type NodeFeatureRuleSpec struct {
Rules []Rule `json:"rules"` Rules []Rule `json:"rules"`
} }
// NodeFeatureGroup resource holds Node pools by featureGroup
// +kubebuilder:object:root=true
// +kubebuilder:resource:scope=Namespaced,shortName=nfg
// +kubebuilder:subresource:status
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +genclient
type NodeFeatureGroup struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec NodeFeatureGroupSpec `json:"spec"`
Status NodeFeatureGroupStatus `json:"status,omitempty"`
}
// NodeFeatureGroupSpec describes a NodeFeatureGroup object.
type NodeFeatureGroupSpec struct {
Rules []GroupRule `json:"featureGroupRules"`
}
type NodeFeatureGroupStatus struct {
// Nodes is a list of FeatureGroupNode in the cluster that match the featureGroupRules
// +optional
// +patchMergeKey=name
// +patchStrategy=merge
// +listType=map
// +listMapKey=name
Nodes []FeatureGroupNode `json:"nodes"`
}
type FeatureGroupNode struct {
Name string `json:"name"`
}
// NodeFeatureGroupList contains a list of NodeFeatureGroup objects.
// +kubebuilder:object:root=true
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type NodeFeatureGroupList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata"`
Items []NodeFeatureGroup `json:"items"`
}
// GroupRule defines a rule for nodegroup filtering.
type GroupRule struct {
// Name of the rule.
Name string `json:"name"`
// MatchFeatures specifies a set of matcher terms all of which must match.
// +optional
MatchFeatures FeatureMatcher `json:"matchFeatures"`
// MatchAny specifies a list of matchers one of which must match.
// +optional
MatchAny []MatchAnyElem `json:"matchAny"`
}
// Rule defines a rule for node customization such as labeling. // Rule defines a rule for node customization such as labeling.
type Rule struct { type Rule struct {
// Name of the rule. // Name of the rule.

View file

@ -49,6 +49,22 @@ func (in *AttributeFeatureSet) DeepCopy() *AttributeFeatureSet {
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *FeatureGroupNode) DeepCopyInto(out *FeatureGroupNode) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureGroupNode.
func (in *FeatureGroupNode) DeepCopy() *FeatureGroupNode {
if in == nil {
return nil
}
out := new(FeatureGroupNode)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in FeatureMatcher) DeepCopyInto(out *FeatureMatcher) { func (in FeatureMatcher) DeepCopyInto(out *FeatureMatcher) {
{ {
@ -171,6 +187,36 @@ func (in *FlagFeatureSet) DeepCopy() *FlagFeatureSet {
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GroupRule) DeepCopyInto(out *GroupRule) {
*out = *in
if in.MatchFeatures != nil {
in, out := &in.MatchFeatures, &out.MatchFeatures
*out = make(FeatureMatcher, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.MatchAny != nil {
in, out := &in.MatchAny, &out.MatchAny
*out = make([]MatchAnyElem, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GroupRule.
func (in *GroupRule) DeepCopy() *GroupRule {
if in == nil {
return nil
}
out := new(GroupRule)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *InstanceFeature) DeepCopyInto(out *InstanceFeature) { func (in *InstanceFeature) DeepCopyInto(out *InstanceFeature) {
*out = *in *out = *in
@ -354,6 +400,111 @@ func (in *NodeFeature) DeepCopyObject() runtime.Object {
return nil return nil
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NodeFeatureGroup) DeepCopyInto(out *NodeFeatureGroup) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeFeatureGroup.
func (in *NodeFeatureGroup) DeepCopy() *NodeFeatureGroup {
if in == nil {
return nil
}
out := new(NodeFeatureGroup)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *NodeFeatureGroup) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NodeFeatureGroupList) DeepCopyInto(out *NodeFeatureGroupList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]NodeFeatureGroup, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeFeatureGroupList.
func (in *NodeFeatureGroupList) DeepCopy() *NodeFeatureGroupList {
if in == nil {
return nil
}
out := new(NodeFeatureGroupList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *NodeFeatureGroupList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NodeFeatureGroupSpec) DeepCopyInto(out *NodeFeatureGroupSpec) {
*out = *in
if in.Rules != nil {
in, out := &in.Rules, &out.Rules
*out = make([]GroupRule, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeFeatureGroupSpec.
func (in *NodeFeatureGroupSpec) DeepCopy() *NodeFeatureGroupSpec {
if in == nil {
return nil
}
out := new(NodeFeatureGroupSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NodeFeatureGroupStatus) DeepCopyInto(out *NodeFeatureGroupStatus) {
*out = *in
if in.Nodes != nil {
in, out := &in.Nodes, &out.Nodes
*out = make([]FeatureGroupNode, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeFeatureGroupStatus.
func (in *NodeFeatureGroupStatus) DeepCopy() *NodeFeatureGroupStatus {
if in == nil {
return nil
}
out := new(NodeFeatureGroupStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NodeFeatureList) DeepCopyInto(out *NodeFeatureList) { func (in *NodeFeatureList) DeepCopyInto(out *NodeFeatureList) {
*out = *in *out = *in

View file

@ -117,6 +117,271 @@ spec:
--- ---
apiVersion: apiextensions.k8s.io/v1 apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.14.0
name: nodefeaturegroups.nfd.k8s-sigs.io
spec:
group: nfd.k8s-sigs.io
names:
kind: NodeFeatureGroup
listKind: NodeFeatureGroupList
plural: nodefeaturegroups
shortNames:
- nfg
singular: nodefeaturegroup
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: NodeFeatureGroup resource holds Node pools by featureGroup
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: NodeFeatureGroupSpec describes a NodeFeatureGroup object.
properties:
featureGroupRules:
items:
description: GroupRule defines a rule for nodegroup filtering.
properties:
matchAny:
description: MatchAny specifies a list of matchers one of which
must match.
items:
description: MatchAnyElem specifies one sub-matcher of MatchAny.
properties:
matchFeatures:
description: MatchFeatures specifies a set of matcher
terms all of which must match.
items:
description: |-
FeatureMatcherTerm defines requirements against one feature set. All
requirements (specified as MatchExpressions) are evaluated against each
element in the feature set.
properties:
feature:
description: Feature is the name of the feature
set to match against.
type: string
matchExpressions:
additionalProperties:
description: |-
MatchExpression specifies an expression to evaluate against a set of input
values. It contains an operator that is applied when matching the input and
an array of values that the operator evaluates the input against.
properties:
op:
description: Op is the operator to be applied.
enum:
- In
- NotIn
- InRegexp
- Exists
- DoesNotExist
- Gt
- Lt
- GtLt
- IsTrue
- IsFalse
type: string
value:
description: |-
Value is the list of values that the operand evaluates the input
against. Value should be empty if the operator is Exists, DoesNotExist,
IsTrue or IsFalse. Value should contain exactly one element if the
operator is Gt or Lt and exactly two elements if the operator is GtLt.
In other cases Value should contain at least one element.
items:
type: string
type: array
required:
- op
type: object
description: |-
MatchExpressions is the set of per-element expressions evaluated. These
match against the value of the specified elements.
type: object
matchName:
description: |-
MatchName in an expression that is matched against the name of each
element in the feature set.
properties:
op:
description: Op is the operator to be applied.
enum:
- In
- NotIn
- InRegexp
- Exists
- DoesNotExist
- Gt
- Lt
- GtLt
- IsTrue
- IsFalse
type: string
value:
description: |-
Value is the list of values that the operand evaluates the input
against. Value should be empty if the operator is Exists, DoesNotExist,
IsTrue or IsFalse. Value should contain exactly one element if the
operator is Gt or Lt and exactly two elements if the operator is GtLt.
In other cases Value should contain at least one element.
items:
type: string
type: array
required:
- op
type: object
required:
- feature
type: object
type: array
required:
- matchFeatures
type: object
type: array
matchFeatures:
description: MatchFeatures specifies a set of matcher terms
all of which must match.
items:
description: |-
FeatureMatcherTerm defines requirements against one feature set. All
requirements (specified as MatchExpressions) are evaluated against each
element in the feature set.
properties:
feature:
description: Feature is the name of the feature set to
match against.
type: string
matchExpressions:
additionalProperties:
description: |-
MatchExpression specifies an expression to evaluate against a set of input
values. It contains an operator that is applied when matching the input and
an array of values that the operator evaluates the input against.
properties:
op:
description: Op is the operator to be applied.
enum:
- In
- NotIn
- InRegexp
- Exists
- DoesNotExist
- Gt
- Lt
- GtLt
- IsTrue
- IsFalse
type: string
value:
description: |-
Value is the list of values that the operand evaluates the input
against. Value should be empty if the operator is Exists, DoesNotExist,
IsTrue or IsFalse. Value should contain exactly one element if the
operator is Gt or Lt and exactly two elements if the operator is GtLt.
In other cases Value should contain at least one element.
items:
type: string
type: array
required:
- op
type: object
description: |-
MatchExpressions is the set of per-element expressions evaluated. These
match against the value of the specified elements.
type: object
matchName:
description: |-
MatchName in an expression that is matched against the name of each
element in the feature set.
properties:
op:
description: Op is the operator to be applied.
enum:
- In
- NotIn
- InRegexp
- Exists
- DoesNotExist
- Gt
- Lt
- GtLt
- IsTrue
- IsFalse
type: string
value:
description: |-
Value is the list of values that the operand evaluates the input
against. Value should be empty if the operator is Exists, DoesNotExist,
IsTrue or IsFalse. Value should contain exactly one element if the
operator is Gt or Lt and exactly two elements if the operator is GtLt.
In other cases Value should contain at least one element.
items:
type: string
type: array
required:
- op
type: object
required:
- feature
type: object
type: array
name:
description: Name of the rule.
type: string
required:
- name
type: object
type: array
required:
- featureGroupRules
type: object
status:
properties:
nodes:
description: Nodes is a list of FeatureGroupNode in the cluster that
match the featureGroupRules
items:
properties:
name:
type: string
required:
- name
type: object
type: array
x-kubernetes-list-map-keys:
- name
x-kubernetes-list-type: map
type: object
required:
- spec
type: object
served: true
storage: true
subresources:
status: {}
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata: metadata:
annotations: annotations:
controller-gen.kubebuilder.io/version: v0.14.0 controller-gen.kubebuilder.io/version: v0.14.0

View file

@ -18,10 +18,18 @@ rules:
resources: resources:
- nodefeatures - nodefeatures
- nodefeaturerules - nodefeaturerules
- nodefeaturegroups
verbs: verbs:
- get - get
- list - list
- watch - watch
- apiGroups:
- nfd.k8s-sigs.io
resources:
- nodefeaturegroup/status
verbs:
- patch
- update
- apiGroups: - apiGroups:
- coordination.k8s.io - coordination.k8s.io
resources: resources:

View file

@ -117,6 +117,271 @@ spec:
--- ---
apiVersion: apiextensions.k8s.io/v1 apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.14.0
name: nodefeaturegroups.nfd.k8s-sigs.io
spec:
group: nfd.k8s-sigs.io
names:
kind: NodeFeatureGroup
listKind: NodeFeatureGroupList
plural: nodefeaturegroups
shortNames:
- nfg
singular: nodefeaturegroup
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: NodeFeatureGroup resource holds Node pools by featureGroup
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: NodeFeatureGroupSpec describes a NodeFeatureGroup object.
properties:
featureGroupRules:
items:
description: GroupRule defines a rule for nodegroup filtering.
properties:
matchAny:
description: MatchAny specifies a list of matchers one of which
must match.
items:
description: MatchAnyElem specifies one sub-matcher of MatchAny.
properties:
matchFeatures:
description: MatchFeatures specifies a set of matcher
terms all of which must match.
items:
description: |-
FeatureMatcherTerm defines requirements against one feature set. All
requirements (specified as MatchExpressions) are evaluated against each
element in the feature set.
properties:
feature:
description: Feature is the name of the feature
set to match against.
type: string
matchExpressions:
additionalProperties:
description: |-
MatchExpression specifies an expression to evaluate against a set of input
values. It contains an operator that is applied when matching the input and
an array of values that the operator evaluates the input against.
properties:
op:
description: Op is the operator to be applied.
enum:
- In
- NotIn
- InRegexp
- Exists
- DoesNotExist
- Gt
- Lt
- GtLt
- IsTrue
- IsFalse
type: string
value:
description: |-
Value is the list of values that the operand evaluates the input
against. Value should be empty if the operator is Exists, DoesNotExist,
IsTrue or IsFalse. Value should contain exactly one element if the
operator is Gt or Lt and exactly two elements if the operator is GtLt.
In other cases Value should contain at least one element.
items:
type: string
type: array
required:
- op
type: object
description: |-
MatchExpressions is the set of per-element expressions evaluated. These
match against the value of the specified elements.
type: object
matchName:
description: |-
MatchName in an expression that is matched against the name of each
element in the feature set.
properties:
op:
description: Op is the operator to be applied.
enum:
- In
- NotIn
- InRegexp
- Exists
- DoesNotExist
- Gt
- Lt
- GtLt
- IsTrue
- IsFalse
type: string
value:
description: |-
Value is the list of values that the operand evaluates the input
against. Value should be empty if the operator is Exists, DoesNotExist,
IsTrue or IsFalse. Value should contain exactly one element if the
operator is Gt or Lt and exactly two elements if the operator is GtLt.
In other cases Value should contain at least one element.
items:
type: string
type: array
required:
- op
type: object
required:
- feature
type: object
type: array
required:
- matchFeatures
type: object
type: array
matchFeatures:
description: MatchFeatures specifies a set of matcher terms
all of which must match.
items:
description: |-
FeatureMatcherTerm defines requirements against one feature set. All
requirements (specified as MatchExpressions) are evaluated against each
element in the feature set.
properties:
feature:
description: Feature is the name of the feature set to
match against.
type: string
matchExpressions:
additionalProperties:
description: |-
MatchExpression specifies an expression to evaluate against a set of input
values. It contains an operator that is applied when matching the input and
an array of values that the operator evaluates the input against.
properties:
op:
description: Op is the operator to be applied.
enum:
- In
- NotIn
- InRegexp
- Exists
- DoesNotExist
- Gt
- Lt
- GtLt
- IsTrue
- IsFalse
type: string
value:
description: |-
Value is the list of values that the operand evaluates the input
against. Value should be empty if the operator is Exists, DoesNotExist,
IsTrue or IsFalse. Value should contain exactly one element if the
operator is Gt or Lt and exactly two elements if the operator is GtLt.
In other cases Value should contain at least one element.
items:
type: string
type: array
required:
- op
type: object
description: |-
MatchExpressions is the set of per-element expressions evaluated. These
match against the value of the specified elements.
type: object
matchName:
description: |-
MatchName in an expression that is matched against the name of each
element in the feature set.
properties:
op:
description: Op is the operator to be applied.
enum:
- In
- NotIn
- InRegexp
- Exists
- DoesNotExist
- Gt
- Lt
- GtLt
- IsTrue
- IsFalse
type: string
value:
description: |-
Value is the list of values that the operand evaluates the input
against. Value should be empty if the operator is Exists, DoesNotExist,
IsTrue or IsFalse. Value should contain exactly one element if the
operator is Gt or Lt and exactly two elements if the operator is GtLt.
In other cases Value should contain at least one element.
items:
type: string
type: array
required:
- op
type: object
required:
- feature
type: object
type: array
name:
description: Name of the rule.
type: string
required:
- name
type: object
type: array
required:
- featureGroupRules
type: object
status:
properties:
nodes:
description: Nodes is a list of FeatureGroupNode in the cluster that
match the featureGroupRules
items:
properties:
name:
type: string
required:
- name
type: object
type: array
x-kubernetes-list-map-keys:
- name
x-kubernetes-list-type: map
type: object
required:
- spec
type: object
served: true
storage: true
subresources:
status: {}
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata: metadata:
annotations: annotations:
controller-gen.kubebuilder.io/version: v0.14.0 controller-gen.kubebuilder.io/version: v0.14.0

View file

@ -21,10 +21,18 @@ rules:
resources: resources:
- nodefeatures - nodefeatures
- nodefeaturerules - nodefeaturerules
- nodefeaturegroups
verbs: verbs:
- get - get
- list - list
- watch - watch
- apiGroups:
- nfd.k8s-sigs.io
resources:
- nodefeaturegroups/status
verbs:
- patch
- update
- apiGroups: - apiGroups:
- coordination.k8s.io - coordination.k8s.io
resources: resources:

View file

@ -14,6 +14,7 @@ enableNodeFeatureApi: true
featureGates: featureGates:
NodeFeatureAPI: true NodeFeatureAPI: true
NodeFeatureGroupAPI: false
priorityClassName: "" priorityClassName: ""

View file

@ -18,6 +18,7 @@ The feature gates are set using the `-feature-gates` command line flag or
| --------------------- | ------- | ------ | ------- | ------ | | --------------------- | ------- | ------ | ------- | ------ |
| `NodeFeatureAPI` | true | Beta | V0.14 | | | `NodeFeatureAPI` | true | Beta | V0.14 | |
| `DisableAutoPrefix` | false | Alpha | V0.16 | | | `DisableAutoPrefix` | false | Alpha | V0.16 | |
| `NodeFeatureGroupAPI` | false | Alpha | V0.16 | |
## NodeFeatureAPI ## NodeFeatureAPI
@ -27,6 +28,14 @@ server. The Node Feature API is used to expose node-specific hardware and
software features to the Kubernetes scheduler. The Node Feature API is a beta software features to the Kubernetes scheduler. The Node Feature API is a beta
feature and is enabled by default. feature and is enabled by default.
## NodeFeatureGroupAPI
The `NodeFeatureGroupAPI` feature gate enables the Node Feature Group API.
When enabled, NFD will register the Node Feature Group API with the Kubernetes API
server. The Node Feature Group API is used to create node groups based on
hardware and software features. The Node Feature Group API is an alpha feature
and is disabled by default.
## DisableAutoPrefix ## DisableAutoPrefix
The `DisableAutoPrefix` feature gate controls the automatic prefixing of names. The `DisableAutoPrefix` feature gate controls the automatic prefixing of names.

View file

@ -51,6 +51,28 @@ spec:
vendor-xpu-present: "true" vendor-xpu-present: "true"
``` ```
## NodeFeatureGroup
NodeFeatureGroup is an NFD-specific custom resource that is designed for
grouping nodes based on their features. NFD-Master watches for NodeFeatureGroup
objects in the cluster and updates the status of the NodeFeatureGroup object
with the list of nodes that match the feature group rules. The NodeFeatureGroup
rules follow the same syntax as the NodeFeatureRule rules.
```yaml
apiVersion: nfd.k8s-sigs.io/v1alpha1
kind: NodeFeatureGroup
metadata:
name: node-feature-group-example
spec:
featureGroupRules:
- name: "node has kernel version discovered"
matchFeatures:
- feature: kernel.version
matchExpressions:
major: {op: Exists}
```
## NodeFeatureRule ## NodeFeatureRule
NodeFeatureRule is an NFD-specific custom resource that is designed for NodeFeatureRule is an NFD-specific custom resource that is designed for

View file

@ -187,6 +187,49 @@ to specify taints in the NodeFeatureRule object.
> not tolerate the taint are evicted immediately from the node including the > not tolerate the taint are evicted immediately from the node including the
> nfd-worker pod. > nfd-worker pod.
## NodeFeatureGroup custom resource
`NodeFeatureGroup` objects provide a way to create node groups that share the
same set of features. The `NodeFeatureGroup` object spec consists of a list of
`NodeFeatureRule` that follow the same format as the `NodeFeatureRule`,
but the difference in this case is that nodes that match any of the rules in the
`NodeFeatureGroup` will be listed in the `NodeFeatureGroup` status.
### A NodeFeatureGroup example
Consider the following referential example:
```yaml
apiVersion: nfd.k8s-sigs.io/v1alpha1
kind: NodeFeatureGroup
metadata:
name: node-feature-group-example
spec:
featureGroupRules:
- name: "kernel version"
matchFeatures:
- feature: kernel.version
matchExpressions:
major: {op: In, value: ["6"]}
status:
nodes:
- name: node-1
- name: node-2
- name: node-3
```
The object specifies a group of nodes that share the same
`kernel.version.major` (Linux kernel v6.x).
Create a `NodeFeatureGroup` with a yaml file:
```bash
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/node-feature-discovery/{{ site.release }}/examples/nodefeaturegroup.yaml
```
See [Feature rule format](#feature-rule-format) for detailed description of
available fields and how to write group filtering rules.
## Local feature source ## Local feature source
NFD-Worker has a special feature source named `local` which is an integration NFD-Worker has a special feature source named `local` which is an integration

View file

@ -0,0 +1,11 @@
apiVersion: nfd.k8s-sigs.io/v1alpha1
kind: NodeFeatureGroup
metadata:
name: node-feature-group-example
spec:
featureGroupRules:
- name: "kernel version"
matchFeatures:
- feature: kernel.version
matchExpressions:
major: {op: In, value: ["6"]}

View file

@ -27,6 +27,10 @@ function cleanup() {
"${GO_CMD}" mod tidy "${GO_CMD}" mod tidy
} }
# Temporal work around until https://github.com/kubernetes/kubernetes/pull/125051 is merged
# and added to a release.
find api/generated/ -name 'nodefeature*' | xargs rm
trap cleanup EXIT trap cleanup EXIT
GO_CMD=${1:-go} GO_CMD=${1:-go}
NFD_ROOT=$(realpath $(dirname ${BASH_SOURCE[0]})/..) NFD_ROOT=$(realpath $(dirname ${BASH_SOURCE[0]})/..)

View file

@ -108,6 +108,41 @@ func Execute(r *nfdv1alpha1.Rule, features *nfdv1alpha1.Features) (RuleOutput, e
return ret, nil return ret, nil
} }
// ExecuteGroupRule executes the GroupRule against a set of input features, and return true if the
// rule matches.
func ExecuteGroupRule(r *nfdv1alpha1.GroupRule, features *nfdv1alpha1.Features) (bool, error) {
matched := false
if len(r.MatchAny) > 0 {
// Logical OR over the matchAny matchers
for _, matcher := range r.MatchAny {
if isMatch, matches, err := evaluateMatchAnyElem(&matcher, features); err != nil {
return false, err
} else if isMatch {
matched = true
klog.V(4).InfoS("matchAny matched", "ruleName", r.Name, "matchedFeatures", utils.DelayedDumper(matches))
// there's no need to evaluate other matchers in MatchAny
// One match is enough for MatchAny
break
}
}
if !matched {
return false, nil
}
}
if len(r.MatchFeatures) > 0 {
if isMatch, _, err := evaluateFeatureMatcher(&r.MatchFeatures, features); err != nil {
return false, err
} else if !isMatch {
klog.V(2).InfoS("rule did not match", "ruleName", r.Name)
return false, nil
}
}
klog.V(2).InfoS("rule matched", "ruleName", r.Name)
return true, nil
}
func executeLabelsTemplate(r *nfdv1alpha1.Rule, in matchedFeatures, out map[string]string) error { func executeLabelsTemplate(r *nfdv1alpha1.Rule, in matchedFeatures, out map[string]string) error {
if r.LabelsTemplate == "" { if r.LabelsTemplate == "" {
return nil return nil

View file

@ -21,8 +21,9 @@ import (
) )
const ( const (
NodeFeatureAPI featuregate.Feature = "NodeFeatureAPI" NodeFeatureAPI featuregate.Feature = "NodeFeatureAPI"
DisableAutoPrefix featuregate.Feature = "DisableAutoPrefix" DisableAutoPrefix featuregate.Feature = "DisableAutoPrefix"
NodeFeatureGroupAPI featuregate.Feature = "NodeFeatureGroupAPI"
) )
var ( var (
@ -34,6 +35,7 @@ var (
) )
var DefaultNFDFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{ var DefaultNFDFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
NodeFeatureAPI: {Default: true, PreRelease: featuregate.Beta}, NodeFeatureAPI: {Default: true, PreRelease: featuregate.Beta},
DisableAutoPrefix: {Default: false, PreRelease: featuregate.Alpha}, DisableAutoPrefix: {Default: false, PreRelease: featuregate.Alpha},
NodeFeatureGroupAPI: {Default: false, PreRelease: featuregate.Alpha},
} }

View file

@ -23,15 +23,16 @@ import (
// When adding metric names, see https://prometheus.io/docs/practices/naming/#metric-names // When adding metric names, see https://prometheus.io/docs/practices/naming/#metric-names
const ( const (
buildInfoQuery = "nfd_master_build_info" buildInfoQuery = "nfd_master_build_info"
nodeUpdateRequestsQuery = "nfd_node_update_requests_total" nodeUpdateRequestsQuery = "nfd_node_update_requests_total"
nodeUpdatesQuery = "nfd_node_updates_total" nodeUpdatesQuery = "nfd_node_updates_total"
nodeUpdateFailuresQuery = "nfd_node_update_failures_total" nodeFeatureGroupUpdateRequestsQuery = "nfd_node_feature_group_update_requests_total"
nodeLabelsRejectedQuery = "nfd_node_labels_rejected_total" nodeUpdateFailuresQuery = "nfd_node_update_failures_total"
nodeERsRejectedQuery = "nfd_node_extendedresources_rejected_total" nodeLabelsRejectedQuery = "nfd_node_labels_rejected_total"
nodeTaintsRejectedQuery = "nfd_node_taints_rejected_total" nodeERsRejectedQuery = "nfd_node_extendedresources_rejected_total"
nfrProcessingTimeQuery = "nfd_nodefeaturerule_processing_duration_seconds" nodeTaintsRejectedQuery = "nfd_node_taints_rejected_total"
nfrProcessingErrorsQuery = "nfd_nodefeaturerule_processing_errors_total" nfrProcessingTimeQuery = "nfd_nodefeaturerule_processing_duration_seconds"
nfrProcessingErrorsQuery = "nfd_nodefeaturerule_processing_errors_total"
) )
var ( var (
@ -46,6 +47,10 @@ var (
Name: nodeUpdateRequestsQuery, Name: nodeUpdateRequestsQuery,
Help: "Number of node update requests processed by the master.", Help: "Number of node update requests processed by the master.",
}) })
nodeFeatureGroupUpdateRequests = prometheus.NewCounter(prometheus.CounterOpts{
Name: nodeFeatureGroupUpdateRequestsQuery,
Help: "Number of cluster feature update requests processed by the master.",
})
nodeUpdates = prometheus.NewCounter(prometheus.CounterOpts{ nodeUpdates = prometheus.NewCounter(prometheus.CounterOpts{
Name: nodeUpdatesQuery, Name: nodeUpdatesQuery,
Help: "Number of nodes updated by the master.", Help: "Number of nodes updated by the master.",

View file

@ -35,18 +35,22 @@ import (
) )
type nfdController struct { type nfdController struct {
featureLister nfdlisters.NodeFeatureLister featureLister nfdlisters.NodeFeatureLister
ruleLister nfdlisters.NodeFeatureRuleLister ruleLister nfdlisters.NodeFeatureRuleLister
featureGroupLister nfdlisters.NodeFeatureGroupLister
stopChan chan struct{} stopChan chan struct{}
updateAllNodesChan chan struct{} updateAllNodesChan chan struct{}
updateOneNodeChan chan string updateOneNodeChan chan string
updateAllNodeFeatureGroupsChan chan struct{}
updateNodeFeatureGroupChan chan string
} }
type nfdApiControllerOptions struct { type nfdApiControllerOptions struct {
DisableNodeFeature bool DisableNodeFeature bool
ResyncPeriod time.Duration DisableNodeFeatureGroup bool
ResyncPeriod time.Duration
} }
func init() { func init() {
@ -55,9 +59,11 @@ func init() {
func newNfdController(config *restclient.Config, nfdApiControllerOptions nfdApiControllerOptions) (*nfdController, error) { func newNfdController(config *restclient.Config, nfdApiControllerOptions nfdApiControllerOptions) (*nfdController, error) {
c := &nfdController{ c := &nfdController{
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
updateAllNodesChan: make(chan struct{}, 1), updateAllNodesChan: make(chan struct{}, 1),
updateOneNodeChan: make(chan string), updateOneNodeChan: make(chan string),
updateAllNodeFeatureGroupsChan: make(chan struct{}, 1),
updateNodeFeatureGroupChan: make(chan string),
} }
nfdClient := nfdclientset.NewForConfigOrDie(config) nfdClient := nfdclientset.NewForConfigOrDie(config)
@ -73,16 +79,25 @@ func newNfdController(config *restclient.Config, nfdApiControllerOptions nfdApiC
nfr := obj.(*nfdv1alpha1.NodeFeature) nfr := obj.(*nfdv1alpha1.NodeFeature)
klog.V(2).InfoS("NodeFeature added", "nodefeature", klog.KObj(nfr)) klog.V(2).InfoS("NodeFeature added", "nodefeature", klog.KObj(nfr))
c.updateOneNode("NodeFeature", nfr) c.updateOneNode("NodeFeature", nfr)
if !nfdApiControllerOptions.DisableNodeFeatureGroup {
c.updateAllNodeFeatureGroups()
}
}, },
UpdateFunc: func(oldObj, newObj interface{}) { UpdateFunc: func(oldObj, newObj interface{}) {
nfr := newObj.(*nfdv1alpha1.NodeFeature) nfr := newObj.(*nfdv1alpha1.NodeFeature)
klog.V(2).InfoS("NodeFeature updated", "nodefeature", klog.KObj(nfr)) klog.V(2).InfoS("NodeFeature updated", "nodefeature", klog.KObj(nfr))
c.updateOneNode("NodeFeature", nfr) c.updateOneNode("NodeFeature", nfr)
if !nfdApiControllerOptions.DisableNodeFeatureGroup {
c.updateAllNodeFeatureGroups()
}
}, },
DeleteFunc: func(obj interface{}) { DeleteFunc: func(obj interface{}) {
nfr := obj.(*nfdv1alpha1.NodeFeature) nfr := obj.(*nfdv1alpha1.NodeFeature)
klog.V(2).InfoS("NodeFeature deleted", "nodefeature", klog.KObj(nfr)) klog.V(2).InfoS("NodeFeature deleted", "nodefeature", klog.KObj(nfr))
c.updateOneNode("NodeFeature", nfr) c.updateOneNode("NodeFeature", nfr)
if !nfdApiControllerOptions.DisableNodeFeatureGroup {
c.updateAllNodeFeatureGroups()
}
}, },
}); err != nil { }); err != nil {
return nil, err return nil, err
@ -91,8 +106,8 @@ func newNfdController(config *restclient.Config, nfdApiControllerOptions nfdApiC
} }
// Add informer for NodeFeatureRule objects // Add informer for NodeFeatureRule objects
ruleInformer := informerFactory.Nfd().V1alpha1().NodeFeatureRules() nodeFeatureRuleInformer := informerFactory.Nfd().V1alpha1().NodeFeatureRules()
if _, err := ruleInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ if _, err := nodeFeatureRuleInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(object interface{}) { AddFunc: func(object interface{}) {
klog.V(2).InfoS("NodeFeatureRule added", "nodefeaturerule", klog.KObj(object.(metav1.Object))) klog.V(2).InfoS("NodeFeatureRule added", "nodefeaturerule", klog.KObj(object.(metav1.Object)))
if !nfdApiControllerOptions.DisableNodeFeature { if !nfdApiControllerOptions.DisableNodeFeature {
@ -117,7 +132,32 @@ func newNfdController(config *restclient.Config, nfdApiControllerOptions nfdApiC
}); err != nil { }); err != nil {
return nil, err return nil, err
} }
c.ruleLister = ruleInformer.Lister() c.ruleLister = nodeFeatureRuleInformer.Lister()
// Add informer for NodeFeatureGroup objects
if !nfdApiControllerOptions.DisableNodeFeatureGroup {
nodeFeatureGroupInformer := informerFactory.Nfd().V1alpha1().NodeFeatureGroups()
if _, err := nodeFeatureGroupInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
nfg := obj.(*nfdv1alpha1.NodeFeatureGroup)
klog.V(2).InfoS("NodeFeatureGroup added", "nodeFeatureGroup", klog.KObj(nfg))
c.updateNodeFeatureGroup(nfg.Name)
},
UpdateFunc: func(oldObj, newObj interface{}) {
nfg := newObj.(*nfdv1alpha1.NodeFeatureGroup)
klog.V(2).InfoS("NodeFeatureGroup updated", "nodeFeatureGroup", klog.KObj(nfg))
c.updateNodeFeatureGroup(nfg.Name)
},
DeleteFunc: func(obj interface{}) {
nfg := obj.(*nfdv1alpha1.NodeFeatureGroup)
klog.V(2).InfoS("NodeFeatureGroup deleted", "nodeFeatureGroup", klog.KObj(nfg))
c.updateNodeFeatureGroup(nfg.Name)
},
}); err != nil {
return nil, err
}
c.featureGroupLister = nodeFeatureGroupInformer.Lister()
}
// Start informers // Start informers
informerFactory.Start(c.stopChan) informerFactory.Start(c.stopChan)
@ -129,15 +169,6 @@ func (c *nfdController) stop() {
close(c.stopChan) close(c.stopChan)
} }
func (c *nfdController) updateOneNode(typ string, obj metav1.Object) {
nodeName, err := getNodeNameForObj(obj)
if err != nil {
klog.ErrorS(err, "failed to determine node name for object", "type", typ, "object", klog.KObj(obj))
return
}
c.updateOneNodeChan <- nodeName
}
func getNodeNameForObj(obj metav1.Object) (string, error) { func getNodeNameForObj(obj metav1.Object) (string, error) {
nodeName, ok := obj.GetLabels()[nfdv1alpha1.NodeFeatureObjNodeNameLabel] nodeName, ok := obj.GetLabels()[nfdv1alpha1.NodeFeatureObjNodeNameLabel]
if !ok { if !ok {
@ -149,9 +180,29 @@ func getNodeNameForObj(obj metav1.Object) (string, error) {
return nodeName, nil return nodeName, nil
} }
func (c *nfdController) updateOneNode(typ string, obj metav1.Object) {
nodeName, err := getNodeNameForObj(obj)
if err != nil {
klog.ErrorS(err, "failed to determine node name for object", "type", typ, "object", klog.KObj(obj))
return
}
c.updateOneNodeChan <- nodeName
}
func (c *nfdController) updateAllNodes() { func (c *nfdController) updateAllNodes() {
select { select {
case c.updateAllNodesChan <- struct{}{}: case c.updateAllNodesChan <- struct{}{}:
default: default:
} }
} }
func (c *nfdController) updateNodeFeatureGroup(nodeFeatureGroup string) {
c.updateNodeFeatureGroupChan <- nodeFeatureGroup
}
func (c *nfdController) updateAllNodeFeatureGroups() {
select {
case c.updateAllNodeFeatureGroupsChan <- struct{}{}:
default:
}
}

View file

@ -775,10 +775,10 @@ func BenchmarkNfdAPIUpdateAllNodes(b *testing.B) {
fakeMaster := newFakeMaster(WithKubernetesClient(fakeCli)) fakeMaster := newFakeMaster(WithKubernetesClient(fakeCli))
fakeMaster.nfdController = newFakeNfdAPIController(fakenfdclient.NewSimpleClientset()) fakeMaster.nfdController = newFakeNfdAPIController(fakenfdclient.NewSimpleClientset())
nodeUpdaterPool := newNodeUpdaterPool(fakeMaster) updaterPool := newUpdaterPool(fakeMaster)
fakeMaster.nodeUpdaterPool = nodeUpdaterPool fakeMaster.updaterPool = updaterPool
nodeUpdaterPool.start(10) updaterPool.start(10)
b.ResetTimer() b.ResetTimer()

View file

@ -40,7 +40,9 @@ import (
"google.golang.org/grpc/health/grpc_health_v1" "google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/grpc/peer" "google.golang.org/grpc/peer"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
apiequality "k8s.io/apimachinery/pkg/api/equality"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
k8sLabels "k8s.io/apimachinery/pkg/labels" k8sLabels "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
k8sclient "k8s.io/client-go/kubernetes" k8sclient "k8s.io/client-go/kubernetes"
@ -54,6 +56,7 @@ import (
taintutils "k8s.io/kubernetes/pkg/util/taints" taintutils "k8s.io/kubernetes/pkg/util/taints"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
nfdclientset "sigs.k8s.io/node-feature-discovery/api/generated/clientset/versioned"
nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/api/nfd/v1alpha1" nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/api/nfd/v1alpha1"
"sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/nodefeaturerule" "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/nodefeaturerule"
"sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/validate" "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/validate"
@ -144,17 +147,18 @@ type NfdMaster interface {
type nfdMaster struct { type nfdMaster struct {
*nfdController *nfdController
args Args args Args
namespace string namespace string
nodeName string nodeName string
configFilePath string configFilePath string
server *grpc.Server server *grpc.Server
healthServer *grpc.Server healthServer *grpc.Server
stop chan struct{} stop chan struct{}
ready chan struct{} ready chan struct{}
kubeconfig *restclient.Config kubeconfig *restclient.Config
k8sClient k8sclient.Interface k8sClient k8sclient.Interface
nodeUpdaterPool *nodeUpdaterPool nfdClient *nfdclientset.Clientset
updaterPool *updaterPool
deniedNs deniedNs
config *NFDConfig config *NFDConfig
} }
@ -211,7 +215,21 @@ func NewNfdMaster(opts ...NfdMasterOption) (NfdMaster, error) {
nfd.k8sClient = cli nfd.k8sClient = cli
} }
nfd.nodeUpdaterPool = newNodeUpdaterPool(nfd) // nfdClient
if nfd.kubeconfig != nil {
kubeconfig, err := utils.GetKubeconfig(nfd.args.Kubeconfig)
if err != nil {
return nfd, err
}
nfd.kubeconfig = kubeconfig
nfdClient, err := nfdclientset.NewForConfig(nfd.kubeconfig)
if err != nil {
return nfd, err
}
nfd.nfdClient = nfdClient
}
nfd.updaterPool = newUpdaterPool(nfd)
return nfd, nil return nfd, nil
} }
@ -283,7 +301,7 @@ func (m *nfdMaster) Run() error {
} }
} }
m.nodeUpdaterPool.start(m.config.NfdApiParallelism) m.updaterPool.start(m.config.NfdApiParallelism)
// Create watcher for config file // Create watcher for config file
configWatch, err := utils.CreateFsWatcher(time.Second, m.configFilePath) configWatch, err := utils.CreateFsWatcher(time.Second, m.configFilePath)
@ -354,10 +372,10 @@ func (m *nfdMaster) Run() error {
return err return err
} }
// Stop the nodeUpdaterPool so that no node updates are underway // Stop the updaterPool so that no node updates are underway
// while we reconfigure the NFD API controller (including the // while we reconfigure the NFD API controller (including the
// listers) below // listers) below
m.nodeUpdaterPool.stop() m.updaterPool.stop()
// restart NFD API controller // restart NFD API controller
if m.nfdController != nil { if m.nfdController != nil {
@ -370,8 +388,8 @@ func (m *nfdMaster) Run() error {
return nil return nil
} }
} }
// Restart the nodeUpdaterPool // Restart the updaterPool
m.nodeUpdaterPool.start(m.config.NfdApiParallelism) m.updaterPool.start(m.config.NfdApiParallelism)
// Update all nodes when the configuration changes // Update all nodes when the configuration changes
if m.nfdController != nil && nfdfeatures.NFDFeatureGate.Enabled(nfdfeatures.NodeFeatureAPI) && m.args.EnableNodeFeatureApi { if m.nfdController != nil && nfdfeatures.NFDFeatureGate.Enabled(nfdfeatures.NodeFeatureAPI) && m.args.EnableNodeFeatureApi {
@ -474,6 +492,8 @@ func (m *nfdMaster) nfdAPIUpdateHandler() {
// disabled (i.e. NodeFeature API is enabled) // disabled (i.e. NodeFeature API is enabled)
updateAll := nfdfeatures.NFDFeatureGate.Enabled(nfdfeatures.NodeFeatureAPI) && m.args.EnableNodeFeatureApi updateAll := nfdfeatures.NFDFeatureGate.Enabled(nfdfeatures.NodeFeatureAPI) && m.args.EnableNodeFeatureApi
updateNodes := make(map[string]struct{}) updateNodes := make(map[string]struct{})
nodeFeatureGroup := make(map[string]struct{})
updateAllNodeFeatureGroups := false
rateLimit := time.After(time.Second) rateLimit := time.After(time.Second)
for { for {
select { select {
@ -481,7 +501,12 @@ func (m *nfdMaster) nfdAPIUpdateHandler() {
updateAll = true updateAll = true
case nodeName := <-m.nfdController.updateOneNodeChan: case nodeName := <-m.nfdController.updateOneNodeChan:
updateNodes[nodeName] = struct{}{} updateNodes[nodeName] = struct{}{}
case <-m.nfdController.updateAllNodeFeatureGroupsChan:
updateAllNodeFeatureGroups = true
case nodeFeatureGroupName := <-m.nfdController.updateNodeFeatureGroupChan:
nodeFeatureGroup[nodeFeatureGroupName] = struct{}{}
case <-rateLimit: case <-rateLimit:
// NodeFeature
errUpdateAll := false errUpdateAll := false
if updateAll { if updateAll {
if err := m.nfdAPIUpdateAllNodes(); err != nil { if err := m.nfdAPIUpdateAllNodes(); err != nil {
@ -490,12 +515,26 @@ func (m *nfdMaster) nfdAPIUpdateHandler() {
} }
} else { } else {
for nodeName := range updateNodes { for nodeName := range updateNodes {
m.nodeUpdaterPool.addNode(nodeName) m.updaterPool.addNode(nodeName)
}
}
// NodeFeatureGroup
errUpdateAllNFG := false
if updateAllNodeFeatureGroups {
if err := m.nfdAPIUpdateAllNodeFeatureGroups(); err != nil {
klog.ErrorS(err, "failed to update NodeFeatureGroups")
errUpdateAllNFG = true
}
} else {
for nodeFeatureGroupName := range nodeFeatureGroup {
m.updaterPool.addNodeFeatureGroup(nodeFeatureGroupName)
} }
} }
// Reset "work queue" and timer // Reset "work queue" and timer
updateAll = errUpdateAll updateAll = errUpdateAll
updateAllNodeFeatureGroups = errUpdateAllNFG
nodeFeatureGroup = map[string]struct{}{}
updateNodes = map[string]struct{}{} updateNodes = map[string]struct{}{}
rateLimit = time.After(time.Second) rateLimit = time.After(time.Second)
} }
@ -515,7 +554,7 @@ func (m *nfdMaster) Stop() {
m.nfdController.stop() m.nfdController.stop()
} }
m.nodeUpdaterPool.stop() m.updaterPool.stop()
close(m.stop) close(m.stop)
} }
@ -758,21 +797,30 @@ func (m *nfdMaster) nfdAPIUpdateAllNodes() error {
} }
for _, node := range nodes.Items { for _, node := range nodes.Items {
m.nodeUpdaterPool.addNode(node.Name) m.updaterPool.addNode(node.Name)
} }
return nil return nil
} }
func (m *nfdMaster) nfdAPIUpdateOneNode(cli k8sclient.Interface, node *corev1.Node) error { // getAndMergeNodeFeatures merges the NodeFeature objects of the given node into a single NodeFeatureSpec.
if m.nfdController == nil || m.nfdController.featureLister == nil { // The Name field of the returned NodeFeatureSpec contains the node name.
return nil func (m *nfdMaster) getAndMergeNodeFeatures(nodeName string) (*nfdv1alpha1.NodeFeature, error) {
nodeFeatures := &nfdv1alpha1.NodeFeature{
ObjectMeta: metav1.ObjectMeta{
Name: nodeName,
},
} }
sel := k8sLabels.SelectorFromSet(k8sLabels.Set{nfdv1alpha1.NodeFeatureObjNodeNameLabel: node.Name}) sel := k8sLabels.SelectorFromSet(k8sLabels.Set{nfdv1alpha1.NodeFeatureObjNodeNameLabel: nodeName})
objs, err := m.nfdController.featureLister.List(sel) objs, err := m.nfdController.featureLister.List(sel)
if err != nil { if err != nil {
return fmt.Errorf("failed to get NodeFeature resources for node %q: %w", node.Name, err) return &nfdv1alpha1.NodeFeature{}, fmt.Errorf("failed to get NodeFeature resources for node %q: %w", nodeName, err)
}
// Node without a running NFD-Worker
if len(objs) == 0 {
return &nfdv1alpha1.NodeFeature{}, nil
} }
// Sort our objects // Sort our objects
@ -792,16 +840,12 @@ func (m *nfdMaster) nfdAPIUpdateOneNode(cli k8sclient.Interface, node *corev1.No
return objs[i].Namespace < objs[j].Namespace return objs[i].Namespace < objs[j].Namespace
}) })
klog.V(1).InfoS("processing of node initiated by NodeFeature API", "nodeName", node.Name)
features := nfdv1alpha1.NewNodeFeatureSpec()
if len(objs) > 0 { if len(objs) > 0 {
// Merge in features // Merge in features
// //
// NOTE: changing the rule api to support handle multiple objects instead // NOTE: changing the rule api to support handle multiple objects instead
// of merging would probably perform better with lot less data to copy. // of merging would probably perform better with lot less data to copy.
features = objs[0].Spec.DeepCopy() features := objs[0].Spec.DeepCopy()
if !nfdfeatures.NFDFeatureGate.Enabled(nfdfeatures.DisableAutoPrefix) && m.config.AutoDefaultNs { if !nfdfeatures.NFDFeatureGate.Enabled(nfdfeatures.DisableAutoPrefix) && m.config.AutoDefaultNs {
features.Labels = addNsToMapKeys(features.Labels, nfdv1alpha1.FeatureLabelNs) features.Labels = addNsToMapKeys(features.Labels, nfdv1alpha1.FeatureLabelNs)
} }
@ -813,19 +857,123 @@ func (m *nfdMaster) nfdAPIUpdateOneNode(cli k8sclient.Interface, node *corev1.No
s.MergeInto(features) s.MergeInto(features)
} }
// Set the merged features to the NodeFeature object
nodeFeatures.Spec = *features
klog.V(4).InfoS("merged nodeFeatureSpecs", "newNodeFeatureSpec", utils.DelayedDumper(features)) klog.V(4).InfoS("merged nodeFeatureSpecs", "newNodeFeatureSpec", utils.DelayedDumper(features))
} }
return nodeFeatures, nil
}
func (m *nfdMaster) nfdAPIUpdateOneNode(cli k8sclient.Interface, node *corev1.Node) error {
if m.nfdController == nil || m.nfdController.featureLister == nil {
return nil
}
// Merge all NodeFeature objects into a single NodeFeatureSpec
nodeFeatures, err := m.getAndMergeNodeFeatures(node.Name)
if err != nil {
return fmt.Errorf("failed to merge NodeFeature objects for node %q: %w", node.Name, err)
}
// Update node labels et al. This may also mean removing all NFD-owned // Update node labels et al. This may also mean removing all NFD-owned
// labels (et al.), for example in the case no NodeFeature objects are // labels (et al.), for example in the case no NodeFeature objects are
// present. // present.
if err := m.refreshNodeFeatures(cli, node, features.Labels, &features.Features); err != nil { if err := m.refreshNodeFeatures(cli, node, nodeFeatures.Spec.Labels, &nodeFeatures.Spec.Features); err != nil {
return err return err
} }
return nil return nil
} }
func (m *nfdMaster) nfdAPIUpdateAllNodeFeatureGroups() error {
klog.V(1).InfoS("updating all NodeFeatureGroups")
nodeFeatureGroupsList, err := m.nfdController.featureGroupLister.List(labels.Everything())
if err != nil {
return fmt.Errorf("failed to get NodeFeatureGroup objects: %w", err)
}
if len(nodeFeatureGroupsList) > 0 {
for _, nodeFeatureGroup := range nodeFeatureGroupsList {
m.updaterPool.nfgQueue.Add(nodeFeatureGroup.Name)
}
} else {
klog.V(2).InfoS("no NodeFeatureGroup objects found")
}
return nil
}
func (m *nfdMaster) nfdAPIUpdateNodeFeatureGroup(nfdClient *nfdclientset.Clientset, nodeFeatureGroup *nfdv1alpha1.NodeFeatureGroup) error {
klog.V(2).InfoS("evaluating NodeFeatureGroup", "nodeFeatureGroup", klog.KObj(nodeFeatureGroup))
if m.nfdController == nil || m.nfdController.featureLister == nil {
return nil
}
// Get all Nodes
nodes, err := getNodes(m.k8sClient)
if err != nil {
return fmt.Errorf("failed to get nodes: %w", err)
}
nodeFeaturesList := make([]*nfdv1alpha1.NodeFeature, 0)
for _, node := range nodes.Items {
// Merge all NodeFeature objects into a single NodeFeatureSpec
nodeFeatures, err := m.getAndMergeNodeFeatures(node.Name)
if err != nil {
return fmt.Errorf("failed to merge NodeFeature objects for node %q: %w", node.Name, err)
}
if nodeFeatures.Name == "" {
// Nothing to do for this node
continue
}
nodeFeaturesList = append(nodeFeaturesList, nodeFeatures)
}
// Execute rules and create matching groups
nodePool := make([]nfdv1alpha1.FeatureGroupNode, 0)
nodeGroupValidator := make(map[string]bool)
for _, rule := range nodeFeatureGroup.Spec.Rules {
for _, feature := range nodeFeaturesList {
match, err := nodefeaturerule.ExecuteGroupRule(&rule, &feature.Spec.Features)
if err != nil {
klog.ErrorS(err, "failed to evaluate rule", "ruleName", rule.Name)
continue
}
if match {
klog.ErrorS(err, "failed to evaluate rule", "ruleName", rule.Name, "nodeName", feature.Name)
system := feature.Spec.Features.Attributes["system.name"]
nodeName := system.Elements["nodename"]
if _, ok := nodeGroupValidator[nodeName]; !ok {
nodePool = append(nodePool, nfdv1alpha1.FeatureGroupNode{
Name: nodeName,
})
nodeGroupValidator[nodeName] = true
}
}
}
}
// Update the NodeFeatureGroup object with the updated featureGroupRules
nodeFeatureGroupUpdated := nodeFeatureGroup.DeepCopy()
nodeFeatureGroupUpdated.Status.Nodes = nodePool
if !apiequality.Semantic.DeepEqual(nodeFeatureGroup, nodeFeatureGroupUpdated) {
klog.InfoS("updating NodeFeatureGroup object", "nodeFeatureGroup", klog.KObj(nodeFeatureGroup))
nodeFeatureGroupUpdated, err = nfdClient.NfdV1alpha1().NodeFeatureGroups(m.namespace).UpdateStatus(context.TODO(), nodeFeatureGroupUpdated, metav1.UpdateOptions{})
if err != nil {
return fmt.Errorf("failed to update NodeFeatureGroup object: %w", err)
}
klog.V(4).InfoS("NodeFeatureGroup object updated", "nodeFeatureGroup", utils.DelayedDumper(nodeFeatureGroupUpdated))
} else {
klog.V(1).InfoS("no changes in NodeFeatureGroup, object is up to date", "nodeFeatureGroup", klog.KObj(nodeFeatureGroup))
}
return nil
}
// filterExtendedResources filters extended resources and returns a map // filterExtendedResources filters extended resources and returns a map
// of valid extended resources. // of valid extended resources.
func (m *nfdMaster) filterExtendedResources(features *nfdv1alpha1.Features, extendedResources ExtendedResources) ExtendedResources { func (m *nfdMaster) filterExtendedResources(features *nfdv1alpha1.Features, extendedResources ExtendedResources) ExtendedResources {
@ -1433,6 +1581,10 @@ func getNode(cli k8sclient.Interface, nodeName string) (*corev1.Node, error) {
return cli.CoreV1().Nodes().Get(context.TODO(), nodeName, metav1.GetOptions{}) return cli.CoreV1().Nodes().Get(context.TODO(), nodeName, metav1.GetOptions{})
} }
func getNodeFeatureGroup(cli nfdclientset.Interface, namespace, name string) (*nfdv1alpha1.NodeFeatureGroup, error) {
return cli.NfdV1alpha1().NodeFeatureGroups(namespace).Get(context.TODO(), name, metav1.GetOptions{})
}
func getNodes(cli k8sclient.Interface) (*corev1.NodeList, error) { func getNodes(cli k8sclient.Interface) (*corev1.NodeList, error) {
return cli.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{}) return cli.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{})
} }

View file

@ -1,131 +0,0 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package nfdmaster
import (
"sync"
"time"
"golang.org/x/time/rate"
apierrors "k8s.io/apimachinery/pkg/api/errors"
k8sclient "k8s.io/client-go/kubernetes"
"k8s.io/client-go/util/workqueue"
"k8s.io/klog/v2"
)
type nodeUpdaterPool struct {
queue workqueue.RateLimitingInterface
sync.RWMutex
wg sync.WaitGroup
nfdMaster *nfdMaster
}
func newNodeUpdaterPool(nfdMaster *nfdMaster) *nodeUpdaterPool {
return &nodeUpdaterPool{
nfdMaster: nfdMaster,
wg: sync.WaitGroup{},
}
}
func (u *nodeUpdaterPool) processNodeUpdateRequest(cli k8sclient.Interface, queue workqueue.RateLimitingInterface) bool {
n, quit := queue.Get()
if quit {
return false
}
nodeName := n.(string)
defer queue.Done(nodeName)
nodeUpdateRequests.Inc()
// Check if node exists
if node, err := getNode(cli, nodeName); apierrors.IsNotFound(err) {
klog.InfoS("node not found, skip update", "nodeName", nodeName)
} else if err := u.nfdMaster.nfdAPIUpdateOneNode(cli, node); err != nil {
if n := queue.NumRequeues(nodeName); n < 15 {
klog.InfoS("retrying node update", "nodeName", nodeName, "lastError", err, "numRetries", n)
} else {
klog.ErrorS(err, "node update failed, queuing for retry ", "nodeName", nodeName, "numRetries", n)
// Count only long-failing attempts
nodeUpdateFailures.Inc()
}
queue.AddRateLimited(nodeName)
return true
}
queue.Forget(nodeName)
return true
}
func (u *nodeUpdaterPool) runNodeUpdater(queue workqueue.RateLimitingInterface) {
var cli k8sclient.Interface
if u.nfdMaster.kubeconfig != nil {
// For normal execution, initialize a separate api client for each updater
cli = k8sclient.NewForConfigOrDie(u.nfdMaster.kubeconfig)
} else {
// For tests, re-use the api client from nfd-master
cli = u.nfdMaster.k8sClient
}
for u.processNodeUpdateRequest(cli, queue) {
}
u.wg.Done()
}
func (u *nodeUpdaterPool) start(parallelism int) {
u.Lock()
defer u.Unlock()
if u.queue != nil && !u.queue.ShuttingDown() {
klog.InfoS("the NFD master node updater pool is already running.")
return
}
klog.InfoS("starting the NFD master node updater pool", "parallelism", parallelism)
// Create ratelimiter. Mimic workqueue.DefaultControllerRateLimiter() but
// with modified per-item (node) rate limiting parameters.
rl := workqueue.NewMaxOfRateLimiter(
workqueue.NewItemExponentialFailureRateLimiter(50*time.Millisecond, 100*time.Second),
&workqueue.BucketRateLimiter{Limiter: rate.NewLimiter(rate.Limit(10), 100)},
)
u.queue = workqueue.NewRateLimitingQueue(rl)
for i := 0; i < parallelism; i++ {
u.wg.Add(1)
go u.runNodeUpdater(u.queue)
}
}
func (u *nodeUpdaterPool) stop() {
u.Lock()
defer u.Unlock()
if u.queue == nil || u.queue.ShuttingDown() {
klog.InfoS("the NFD master node updater pool is not running.")
return
}
klog.InfoS("stopping the NFD master node updater pool")
u.queue.ShutDown()
u.wg.Wait()
}
func (u *nodeUpdaterPool) addNode(nodeName string) {
u.RLock()
defer u.RUnlock()
u.queue.Add(nodeName)
}

View file

@ -0,0 +1,194 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package nfdmaster
import (
"sync"
"time"
"golang.org/x/time/rate"
apierrors "k8s.io/apimachinery/pkg/api/errors"
k8sclient "k8s.io/client-go/kubernetes"
"k8s.io/client-go/util/workqueue"
"k8s.io/klog/v2"
nfdclientset "sigs.k8s.io/node-feature-discovery/api/generated/clientset/versioned"
nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/api/nfd/v1alpha1"
"sigs.k8s.io/node-feature-discovery/pkg/features"
)
type updaterPool struct {
queue workqueue.RateLimitingInterface
nfgQueue workqueue.RateLimitingInterface
sync.RWMutex
wg sync.WaitGroup
nfgWg sync.WaitGroup
nfdMaster *nfdMaster
}
func newUpdaterPool(nfdMaster *nfdMaster) *updaterPool {
return &updaterPool{
nfdMaster: nfdMaster,
wg: sync.WaitGroup{},
}
}
func (u *updaterPool) processNodeUpdateRequest(cli k8sclient.Interface, queue workqueue.RateLimitingInterface) bool {
n, quit := queue.Get()
if quit {
return false
}
nodeName := n.(string)
defer queue.Done(nodeName)
nodeUpdateRequests.Inc()
// Check if node exists
if node, err := getNode(cli, nodeName); apierrors.IsNotFound(err) {
klog.InfoS("node not found, skip update", "nodeName", nodeName)
} else if err := u.nfdMaster.nfdAPIUpdateOneNode(cli, node); err != nil {
if n := queue.NumRequeues(nodeName); n < 15 {
klog.InfoS("retrying node update", "nodeName", nodeName, "lastError", err, "numRetries", n)
} else {
klog.ErrorS(err, "node update failed, queuing for retry ", "nodeName", nodeName, "numRetries", n)
// Count only long-failing attempts
nodeUpdateFailures.Inc()
}
queue.AddRateLimited(nodeName)
return true
}
queue.Forget(nodeName)
return true
}
func (u *updaterPool) runNodeUpdater(queue workqueue.RateLimitingInterface) {
var cli k8sclient.Interface
if u.nfdMaster.kubeconfig != nil {
// For normal execution, initialize a separate api client for each updater
cli = k8sclient.NewForConfigOrDie(u.nfdMaster.kubeconfig)
} else {
// For tests, re-use the api client from nfd-master
cli = u.nfdMaster.k8sClient
}
for u.processNodeUpdateRequest(cli, queue) {
}
u.wg.Done()
}
func (u *updaterPool) processNodeFeatureGroupUpdateRequest(cli nfdclientset.Interface, ngfQueue workqueue.RateLimitingInterface) bool {
nfgName, quit := ngfQueue.Get()
if quit {
return false
}
defer ngfQueue.Done(nfgName)
nodeFeatureGroupUpdateRequests.Inc()
// Check if NodeFeatureGroup exists
var nfg *nfdv1alpha1.NodeFeatureGroup
var err error
if nfg, err = getNodeFeatureGroup(cli, u.nfdMaster.namespace, nfgName.(string)); apierrors.IsNotFound(err) {
klog.InfoS("NodeFeatureGroup not found, skip update", "NodeFeatureGroupName", nfgName)
} else if err := u.nfdMaster.nfdAPIUpdateNodeFeatureGroup(u.nfdMaster.nfdClient, nfg); err != nil {
if n := ngfQueue.NumRequeues(nfgName); n < 15 {
klog.InfoS("retrying NodeFeatureGroup update", "nodeFeatureGroup", klog.KObj(nfg), "lastError", err)
} else {
klog.ErrorS(err, "failed to update NodeFeatureGroup, queueing for retry", "nodeFeatureGroup", klog.KObj(nfg), "lastError", err, "numRetries", n)
}
ngfQueue.AddRateLimited(nfgName)
return true
}
ngfQueue.Forget(nfgName)
return true
}
func (u *updaterPool) runNodeFeatureGroupUpdater(ngfQueue workqueue.RateLimitingInterface) {
cli := nfdclientset.NewForConfigOrDie(u.nfdMaster.kubeconfig)
for u.processNodeFeatureGroupUpdateRequest(cli, ngfQueue) {
}
u.nfgWg.Done()
}
func (u *updaterPool) start(parallelism int) {
u.Lock()
defer u.Unlock()
if u.queue != nil && !u.queue.ShuttingDown() {
klog.InfoS("the NFD master updater pool is already running.")
return
}
if u.nfgQueue != nil && !u.nfgQueue.ShuttingDown() {
klog.InfoS("the NFD master node feature group updater pool is already running.")
return
}
klog.InfoS("starting the NFD master updater pool", "parallelism", parallelism)
// Create ratelimiter. Mimic workqueue.DefaultControllerRateLimiter() but
// with modified per-item (node) rate limiting parameters.
rl := workqueue.NewMaxOfRateLimiter(
workqueue.NewItemExponentialFailureRateLimiter(50*time.Millisecond, 100*time.Second),
&workqueue.BucketRateLimiter{Limiter: rate.NewLimiter(rate.Limit(10), 100)},
)
u.queue = workqueue.NewRateLimitingQueue(rl)
u.nfgQueue = workqueue.NewRateLimitingQueue(rl)
for i := 0; i < parallelism; i++ {
u.wg.Add(1)
go u.runNodeUpdater(u.queue)
if features.NFDFeatureGate.Enabled(features.NodeFeatureGroupAPI) {
u.nfgWg.Add(1)
go u.runNodeFeatureGroupUpdater(u.nfgQueue)
}
}
}
func (u *updaterPool) stop() {
u.Lock()
defer u.Unlock()
if u.queue == nil || u.queue.ShuttingDown() {
klog.InfoS("the NFD master updater pool is not running.")
return
}
if u.nfgQueue == nil || u.nfgQueue.ShuttingDown() {
klog.InfoS("the NFD master updater pool is not running.")
return
}
klog.InfoS("stopping the NFD master updater pool")
u.queue.ShutDown()
u.wg.Wait()
u.nfgQueue.ShutDown()
u.nfgWg.Wait()
}
func (u *updaterPool) addNode(nodeName string) {
u.RLock()
defer u.RUnlock()
u.queue.Add(nodeName)
}
func (u *updaterPool) addNodeFeatureGroup(nodeFeatureGroupName string) {
u.RLock()
defer u.RUnlock()
u.nfgQueue.Add(nodeFeatureGroupName)
}

View file

@ -26,44 +26,44 @@ import (
fakenfdclient "sigs.k8s.io/node-feature-discovery/api/generated/clientset/versioned/fake" fakenfdclient "sigs.k8s.io/node-feature-discovery/api/generated/clientset/versioned/fake"
) )
func newFakeNodeUpdaterPool(nfdMaster *nfdMaster) *nodeUpdaterPool { func newFakeupdaterPool(nfdMaster *nfdMaster) *updaterPool {
return &nodeUpdaterPool{ return &updaterPool{
nfdMaster: nfdMaster, nfdMaster: nfdMaster,
wg: sync.WaitGroup{}, wg: sync.WaitGroup{},
} }
} }
func TestNodeUpdaterStart(t *testing.T) { func TestUpdaterStart(t *testing.T) {
fakeMaster := newFakeMaster() fakeMaster := newFakeMaster()
nodeUpdaterPool := newFakeNodeUpdaterPool(fakeMaster) updaterPool := newFakeupdaterPool(fakeMaster)
Convey("When starting the node updater pool", t, func() { Convey("When starting the node updater pool", t, func() {
nodeUpdaterPool.start(10) updaterPool.start(10)
q := nodeUpdaterPool.queue q := updaterPool.queue
Convey("Node updater pool queue properties should change", func() { Convey("Node updater pool queue properties should change", func() {
So(q, ShouldNotBeNil) So(q, ShouldNotBeNil)
So(q.ShuttingDown(), ShouldBeFalse) So(q.ShuttingDown(), ShouldBeFalse)
}) })
nodeUpdaterPool.start(10) updaterPool.start(10)
Convey("Node updater pool queue should not change", func() { Convey("Node updater pool queue should not change", func() {
So(nodeUpdaterPool.queue, ShouldEqual, q) So(updaterPool.queue, ShouldEqual, q)
}) })
}) })
} }
func TestNodeUpdaterStop(t *testing.T) { func TestNodeUpdaterStop(t *testing.T) {
fakeMaster := newFakeMaster() fakeMaster := newFakeMaster()
nodeUpdaterPool := newFakeNodeUpdaterPool(fakeMaster) updaterPool := newFakeupdaterPool(fakeMaster)
nodeUpdaterPool.start(10) updaterPool.start(10)
Convey("When stoping the node updater pool", t, func() { Convey("When stoping the node updater pool", t, func() {
nodeUpdaterPool.stop() updaterPool.stop()
Convey("Node updater pool queue should be removed", func() { Convey("Node updater pool queue should be removed", func() {
// Wait for the wg.Done() // Wait for the wg.Done()
So(func() interface{} { So(func() interface{} {
return nodeUpdaterPool.queue.ShuttingDown() return updaterPool.queue.ShuttingDown()
}, withTimeout, 2*time.Second, ShouldBeTrue) }, withTimeout, 2*time.Second, ShouldBeTrue)
}) })
}) })
@ -72,15 +72,31 @@ func TestNodeUpdaterStop(t *testing.T) {
func TestRunNodeUpdater(t *testing.T) { func TestRunNodeUpdater(t *testing.T) {
fakeMaster := newFakeMaster(WithKubernetesClient(fakek8sclient.NewSimpleClientset())) fakeMaster := newFakeMaster(WithKubernetesClient(fakek8sclient.NewSimpleClientset()))
fakeMaster.nfdController = newFakeNfdAPIController(fakenfdclient.NewSimpleClientset()) fakeMaster.nfdController = newFakeNfdAPIController(fakenfdclient.NewSimpleClientset())
nodeUpdaterPool := newFakeNodeUpdaterPool(fakeMaster) updaterPool := newFakeupdaterPool(fakeMaster)
nodeUpdaterPool.start(10) updaterPool.start(10)
Convey("Queue has no element", t, func() { Convey("Queue has no element", t, func() {
So(nodeUpdaterPool.queue.Len(), ShouldEqual, 0) So(updaterPool.queue.Len(), ShouldEqual, 0)
}) })
nodeUpdaterPool.queue.Add(testNodeName) updaterPool.queue.Add(testNodeName)
Convey("Added element to the queue should be removed", t, func() { Convey("Added element to the queue should be removed", t, func() {
So(func() interface{} { return nodeUpdaterPool.queue.Len() }, So(func() interface{} { return updaterPool.queue.Len() },
withTimeout, 2*time.Second, ShouldEqual, 0)
})
}
func TestRunNodeFeatureGroupUpdater(t *testing.T) {
fakeMaster := newFakeMaster(WithKubernetesClient(fakek8sclient.NewSimpleClientset()))
fakeMaster.nfdController = newFakeNfdAPIController(fakenfdclient.NewSimpleClientset())
updaterPool := newFakeupdaterPool(fakeMaster)
updaterPool.start(10)
Convey("Queue has no element", t, func() {
So(updaterPool.nfgQueue.Len(), ShouldEqual, 0)
})
updaterPool.nfgQueue.Add(testNodeName)
Convey("Added element to the queue should be removed", t, func() {
So(func() interface{} { return updaterPool.queue.Len() },
withTimeout, 2*time.Second, ShouldEqual, 0) withTimeout, 2*time.Second, ShouldEqual, 0)
}) })
} }

View file

@ -53,6 +53,7 @@ func setupTest(args *master.Args) testContext {
os.Exit(1) os.Exit(1)
} }
_ = features.NFDMutableFeatureGate.OverrideDefault(features.NodeFeatureAPI, false) _ = features.NFDMutableFeatureGate.OverrideDefault(features.NodeFeatureAPI, false)
_ = features.NFDMutableFeatureGate.OverrideDefault(features.NodeFeatureGroupAPI, false)
m, err := master.NewNfdMaster( m, err := master.NewNfdMaster(
master.WithArgs(args), master.WithArgs(args),
master.WithKubernetesClient(fakeclient.NewSimpleClientset())) master.WithKubernetesClient(fakeclient.NewSimpleClientset()))

View file

@ -0,0 +1,11 @@
apiVersion: nfd.k8s-sigs.io/v1alpha1
kind: NodeFeatureGroup
metadata:
name: e2e-test-1
spec:
featureGroupRules:
- name: "e2e-attribute-test-1"
matchFeatures:
- feature: kernel.version
matchExpressions:
major: {op: Exists}

View file

@ -22,6 +22,7 @@ import (
"fmt" "fmt"
"maps" "maps"
"path/filepath" "path/filepath"
"reflect"
"strings" "strings"
"time" "time"
@ -181,6 +182,19 @@ func cleanupCRs(ctx context.Context, cli *nfdclient.Clientset, namespace string)
}()).NotTo(HaveOccurred()) }()).NotTo(HaveOccurred())
} }
} }
// Drop NodeFeatureGroup objects
nfgs, err := cli.NfdV1alpha1().NodeFeatureGroups(namespace).List(ctx, metav1.ListOptions{})
Expect(err).NotTo(HaveOccurred())
if len(nfgs.Items) != 0 {
By("Deleting NodeFeatureGroup objects from namespace " + namespace)
for _, nfg := range nfgs.Items {
err = cli.NfdV1alpha1().NodeFeatureGroups(namespace).Delete(ctx, nfg.Name, metav1.DeleteOptions{})
Expect(err).NotTo(HaveOccurred())
}
}
} }
// Actual test suite // Actual test suite
@ -873,6 +887,61 @@ core:
}) })
}) })
// Test NodeFeatureGroups
Context("and NodeFeatureGroups objects deployed", Label("nodefeaturegroup"), func() {
BeforeEach(func(ctx context.Context) {
// We need a NodeFeature from the node, can't be a fake one
if !useNodeFeatureApi {
Skip("NodeFeature API not enabled")
}
// enable the node feature group api
extraMasterPodSpecOpts = []testpod.SpecOption{
testpod.SpecWithContainerExtraArgs(
"--feature-gates=NodeFeatureGroupAPI=true",
),
}
})
It("custom NodeFeatureGroup should be updated", func(ctx context.Context) {
By("Creating nfd-worker daemonset")
podSpecOpts := createPodSpecOpts(
testpod.SpecWithContainerImage(dockerImage()),
)
workerDS := testds.NFDWorker(podSpecOpts...)
workerDS, err := f.ClientSet.AppsV1().DaemonSets(f.Namespace.Name).Create(ctx, workerDS, metav1.CreateOptions{})
Expect(err).NotTo(HaveOccurred())
By("Waiting for worker daemonset pods to be ready")
Expect(testpod.WaitForReady(ctx, f.ClientSet, f.Namespace.Name, workerDS.Spec.Template.Labels["name"], 2)).NotTo(HaveOccurred())
nodes, err := getNonControlPlaneNodes(ctx, f.ClientSet)
Expect(err).NotTo(HaveOccurred())
By("Creating NodeFeatureGroups #1")
Expect(testutils.CreateNodeFeatureGroupsFromFile(ctx, nfdClient, f.Namespace.Name, "nodefeaturegroup-1.yaml")).NotTo(HaveOccurred())
By("Verifying NodeFeatureGroups #1")
targetNodes := make([]nfdv1alpha1.FeatureGroupNode, 0)
for _, node := range nodes {
targetNodes = append(targetNodes, nfdv1alpha1.FeatureGroupNode{
Name: node.Name,
})
}
expectedGroup := nfdv1alpha1.NodeFeatureGroup{
Status: nfdv1alpha1.NodeFeatureGroupStatus{
Nodes: targetNodes,
},
}
Eventually(func() bool {
group, err := nfdClient.NfdV1alpha1().NodeFeatureGroups(f.Namespace.Name).Get(ctx, "e2e-test-1", metav1.GetOptions{})
if err != nil {
return false
}
return reflect.DeepEqual(group.Status, expectedGroup.Status)
}, 5*time.Minute, 5*time.Second).Should(BeTrue())
})
})
Context("and check whether master config passed successfully or not", func() { Context("and check whether master config passed successfully or not", func() {
BeforeEach(func(ctx context.Context) { BeforeEach(func(ctx context.Context) {
extraMasterPodSpecOpts = []testpod.SpecOption{ extraMasterPodSpecOpts = []testpod.SpecOption{

View file

@ -123,6 +123,21 @@ func CreateNodeFeatureRulesFromFile(ctx context.Context, cli nfdclientset.Interf
return nil return nil
} }
// CreateNodeFeatureGroupsFromFile creates a NodeFeatureGroup object from a given file located under test data directory.
func CreateNodeFeatureGroupsFromFile(ctx context.Context, cli nfdclientset.Interface, namespace, filename string) error {
objs, err := nodeFeatureGroupsFromFile(filepath.Join(packagePath, "..", "data", filename))
if err != nil {
return err
}
for _, obj := range objs {
if _, err = cli.NfdV1alpha1().NodeFeatureGroups(namespace).Create(ctx, obj, metav1.CreateOptions{}); err != nil {
return err
}
}
return nil
}
// UpdateNodeFeatureRulesFromFile updates existing NodeFeatureRule object from a given file located under test data directory. // UpdateNodeFeatureRulesFromFile updates existing NodeFeatureRule object from a given file located under test data directory.
func UpdateNodeFeatureRulesFromFile(ctx context.Context, cli nfdclientset.Interface, filename string) error { func UpdateNodeFeatureRulesFromFile(ctx context.Context, cli nfdclientset.Interface, filename string) error {
objs, err := nodeFeatureRulesFromFile(filepath.Join(packagePath, "..", "data", filename)) objs, err := nodeFeatureRulesFromFile(filepath.Join(packagePath, "..", "data", filename))
@ -238,6 +253,25 @@ func nodeFeatureRulesFromFile(path string) ([]*nfdv1alpha1.NodeFeatureRule, erro
return crs, nil return crs, nil
} }
func nodeFeatureGroupsFromFile(path string) ([]*nfdv1alpha1.NodeFeatureGroup, error) {
objs, err := apiObjsFromFile(path, nfdscheme.Codecs.UniversalDeserializer())
if err != nil {
return nil, err
}
crs := make([]*nfdv1alpha1.NodeFeatureGroup, len(objs))
for i, obj := range objs {
var ok bool
crs[i], ok = obj.(*nfdv1alpha1.NodeFeatureGroup)
if !ok {
return nil, fmt.Errorf("unexpected type %t when reading %q", obj, path)
}
}
return crs, nil
}
func init() { func init() {
_, thisFile, _, _ := runtime.Caller(0) _, thisFile, _, _ := runtime.Caller(0)
packagePath = filepath.Dir(thisFile) packagePath = filepath.Dir(thisFile)

View file

@ -186,6 +186,16 @@ func createClusterRoleMaster(ctx context.Context, cs clientset.Interface) (*rbac
Resources: []string{"nodefeatures", "nodefeaturerules"}, Resources: []string{"nodefeatures", "nodefeaturerules"},
Verbs: []string{"get", "list", "watch"}, Verbs: []string{"get", "list", "watch"},
}, },
{
APIGroups: []string{"nfd.k8s-sigs.io"},
Resources: []string{"nodefeaturegroups"},
Verbs: []string{"get", "list", "watch", "update"},
},
{
APIGroups: []string{"nfd.k8s-sigs.io"},
Resources: []string{"nodefeaturegroups/status"},
Verbs: []string{"patch", "update"},
},
}, },
} }
if *openShift { if *openShift {