mirror of
https://github.com/kubernetes-sigs/node-feature-discovery.git
synced 2024-12-14 11:57:51 +00:00
Merge pull request #1813 from marquiz/devel/gc-metalister
nfd-gc: only fetch object metadata
This commit is contained in:
commit
57f1b79856
2 changed files with 82 additions and 78 deletions
|
@ -21,23 +21,28 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
topologyclientset "github.com/k8stopologyawareschedwg/noderesourcetopology-api/pkg/generated/clientset/versioned"
|
topologyv1alpha2 "github.com/k8stopologyawareschedwg/noderesourcetopology-api/pkg/apis/topology/v1alpha2"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/apimachinery/pkg/api/errors"
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
"k8s.io/client-go/informers"
|
metadataclient "k8s.io/client-go/metadata"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/metadata/metadatainformer"
|
||||||
"k8s.io/client-go/tools/cache"
|
"k8s.io/client-go/tools/cache"
|
||||||
"k8s.io/klog/v2"
|
"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"
|
nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/api/nfd/v1alpha1"
|
||||||
"sigs.k8s.io/node-feature-discovery/pkg/utils"
|
"sigs.k8s.io/node-feature-discovery/pkg/utils"
|
||||||
"sigs.k8s.io/node-feature-discovery/pkg/version"
|
"sigs.k8s.io/node-feature-discovery/pkg/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
gvrNF = nfdv1alpha1.SchemeGroupVersion.WithResource("nodefeatures")
|
||||||
|
gvrNRT = topologyv1alpha2.SchemeGroupVersion.WithResource("noderesourcetopologies")
|
||||||
|
gvrNode = corev1.SchemeGroupVersion.WithResource("nodes")
|
||||||
|
)
|
||||||
|
|
||||||
// Args are the command line arguments
|
// Args are the command line arguments
|
||||||
type Args struct {
|
type Args struct {
|
||||||
GCPeriod time.Duration
|
GCPeriod time.Duration
|
||||||
|
@ -51,11 +56,10 @@ type NfdGarbageCollector interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
type nfdGarbageCollector struct {
|
type nfdGarbageCollector struct {
|
||||||
args *Args
|
args *Args
|
||||||
stopChan chan struct{}
|
stopChan chan struct{}
|
||||||
nfdClient nfdclientset.Interface
|
client metadataclient.Interface
|
||||||
topoClient topologyclientset.Interface
|
factory metadatainformer.SharedInformerFactory
|
||||||
factory informers.SharedInformerFactory
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(args *Args) (NfdGarbageCollector, error) {
|
func New(args *Args) (NfdGarbageCollector, error) {
|
||||||
|
@ -64,20 +68,19 @@ func New(args *Args) (NfdGarbageCollector, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
clientset := kubernetes.NewForConfigOrDie(kubeconfig)
|
cli := metadataclient.NewForConfigOrDie(kubeconfig)
|
||||||
|
|
||||||
return &nfdGarbageCollector{
|
return &nfdGarbageCollector{
|
||||||
args: args,
|
args: args,
|
||||||
stopChan: make(chan struct{}),
|
stopChan: make(chan struct{}),
|
||||||
topoClient: topologyclientset.NewForConfigOrDie(kubeconfig),
|
client: cli,
|
||||||
nfdClient: nfdclientset.NewForConfigOrDie(kubeconfig),
|
factory: metadatainformer.NewSharedInformerFactory(cli, 0),
|
||||||
factory: informers.NewSharedInformerFactory(clientset, 5*time.Minute),
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *nfdGarbageCollector) deleteNodeFeature(namespace, name string) {
|
func (n *nfdGarbageCollector) deleteNodeFeature(namespace, name string) {
|
||||||
kind := "NodeFeature"
|
kind := "NodeFeature"
|
||||||
if err := n.nfdClient.NfdV1alpha1().NodeFeatures(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{}); err != nil {
|
if err := n.client.Resource(gvrNF).Namespace(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{}); err != nil {
|
||||||
if errors.IsNotFound(err) {
|
if errors.IsNotFound(err) {
|
||||||
klog.V(2).InfoS("NodeFeature not found, omitting deletion", "nodefeature", klog.KRef(namespace, name))
|
klog.V(2).InfoS("NodeFeature not found, omitting deletion", "nodefeature", klog.KRef(namespace, name))
|
||||||
return
|
return
|
||||||
|
@ -93,7 +96,7 @@ func (n *nfdGarbageCollector) deleteNodeFeature(namespace, name string) {
|
||||||
|
|
||||||
func (n *nfdGarbageCollector) deleteNRT(nodeName string) {
|
func (n *nfdGarbageCollector) deleteNRT(nodeName string) {
|
||||||
kind := "NodeResourceTopology"
|
kind := "NodeResourceTopology"
|
||||||
if err := n.topoClient.TopologyV1alpha2().NodeResourceTopologies().Delete(context.TODO(), nodeName, metav1.DeleteOptions{}); err != nil {
|
if err := n.client.Resource(gvrNRT).Delete(context.TODO(), nodeName, metav1.DeleteOptions{}); err != nil {
|
||||||
if errors.IsNotFound(err) {
|
if errors.IsNotFound(err) {
|
||||||
klog.V(2).InfoS("NodeResourceTopology not found, omitting deletion", "nodeName", nodeName)
|
klog.V(2).InfoS("NodeResourceTopology not found, omitting deletion", "nodeName", nodeName)
|
||||||
return
|
return
|
||||||
|
@ -115,17 +118,18 @@ func (n *nfdGarbageCollector) deleteNodeHandler(object interface{}) {
|
||||||
obj = deletedFinalStateUnknown.Obj
|
obj = deletedFinalStateUnknown.Obj
|
||||||
}
|
}
|
||||||
|
|
||||||
node, ok := obj.(*corev1.Node)
|
meta, ok := obj.(*metav1.PartialObjectMetadata)
|
||||||
if !ok {
|
if !ok {
|
||||||
klog.InfoS("cannot convert object to v1.Node", "object", object)
|
klog.InfoS("cannot convert object to metav1.ObjectMeta", "object", object)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
nodeName := meta.ObjectMeta.GetName()
|
||||||
|
|
||||||
n.deleteNRT(node.GetName())
|
n.deleteNRT(nodeName)
|
||||||
|
|
||||||
// Delete all NodeFeature objects (from all namespaces) targeting the deleted node
|
// Delete all NodeFeature objects (from all namespaces) targeting the deleted node
|
||||||
nfListOptions := metav1.ListOptions{LabelSelector: nfdv1alpha1.NodeFeatureObjNodeNameLabel + "=" + node.GetName()}
|
nfListOptions := metav1.ListOptions{LabelSelector: nfdv1alpha1.NodeFeatureObjNodeNameLabel + "=" + nodeName}
|
||||||
if nfs, err := n.nfdClient.NfdV1alpha1().NodeFeatures("").List(context.TODO(), nfListOptions); err != nil {
|
if nfs, err := n.client.Resource(gvrNF).List(context.TODO(), nfListOptions); err != nil {
|
||||||
klog.ErrorS(err, "failed to list NodeFeature objects")
|
klog.ErrorS(err, "failed to list NodeFeature objects")
|
||||||
} else {
|
} else {
|
||||||
for _, nf := range nfs.Items {
|
for _, nf := range nfs.Items {
|
||||||
|
@ -137,24 +141,25 @@ func (n *nfdGarbageCollector) deleteNodeHandler(object interface{}) {
|
||||||
// garbageCollect removes all stale API objects
|
// garbageCollect removes all stale API objects
|
||||||
func (n *nfdGarbageCollector) garbageCollect() {
|
func (n *nfdGarbageCollector) garbageCollect() {
|
||||||
klog.InfoS("performing garbage collection")
|
klog.InfoS("performing garbage collection")
|
||||||
nodes, err := n.factory.Core().V1().Nodes().Lister().List(labels.Everything())
|
objs, err := n.factory.ForResource(gvrNode).Lister().List(labels.Everything())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
klog.ErrorS(err, "failed to list Node objects")
|
klog.ErrorS(err, "failed to list Node objects")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
nodeNames := sets.NewString()
|
nodeNames := sets.NewString()
|
||||||
for _, node := range nodes {
|
for _, obj := range objs {
|
||||||
nodeNames.Insert(node.Name)
|
meta := obj.(*metav1.PartialObjectMetadata).ObjectMeta
|
||||||
|
nodeNames.Insert(meta.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle NodeFeature objects
|
// Handle NodeFeature objects
|
||||||
nfs, err := n.nfdClient.NfdV1alpha1().NodeFeatures("").List(context.TODO(), metav1.ListOptions{})
|
objMetas, err := n.client.Resource(gvrNF).List(context.TODO(), metav1.ListOptions{})
|
||||||
if errors.IsNotFound(err) {
|
if errors.IsNotFound(err) {
|
||||||
klog.V(2).InfoS("NodeFeature CRD does not exist")
|
klog.V(2).InfoS("NodeFeature CRD does not exist")
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
klog.ErrorS(err, "failed to list NodeFeature objects")
|
klog.ErrorS(err, "failed to list NodeFeature objects")
|
||||||
} else {
|
} else {
|
||||||
for _, nf := range nfs.Items {
|
for _, nf := range objMetas.Items {
|
||||||
nodeName, ok := nf.GetLabels()[nfdv1alpha1.NodeFeatureObjNodeNameLabel]
|
nodeName, ok := nf.GetLabels()[nfdv1alpha1.NodeFeatureObjNodeNameLabel]
|
||||||
if !ok {
|
if !ok {
|
||||||
klog.InfoS("node name label missing from NodeFeature object", "nodefeature", klog.KObj(&nf))
|
klog.InfoS("node name label missing from NodeFeature object", "nodefeature", klog.KObj(&nf))
|
||||||
|
@ -166,13 +171,13 @@ func (n *nfdGarbageCollector) garbageCollect() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle NodeResourceTopology objects
|
// Handle NodeResourceTopology objects
|
||||||
nrts, err := n.topoClient.TopologyV1alpha2().NodeResourceTopologies().List(context.TODO(), metav1.ListOptions{})
|
objMetas, err = n.client.Resource(gvrNRT).List(context.TODO(), metav1.ListOptions{})
|
||||||
if errors.IsNotFound(err) {
|
if errors.IsNotFound(err) {
|
||||||
klog.V(2).InfoS("NodeResourceTopology CRD does not exist")
|
klog.V(2).InfoS("NodeResourceTopology CRD does not exist")
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
klog.ErrorS(err, "failed to list NodeResourceTopology objects")
|
klog.ErrorS(err, "failed to list NodeResourceTopology objects")
|
||||||
} else {
|
} else {
|
||||||
for _, nrt := range nrts.Items {
|
for _, nrt := range objMetas.Items {
|
||||||
if !nodeNames.Has(nrt.Name) {
|
if !nodeNames.Has(nrt.Name) {
|
||||||
n.deleteNRT(nrt.Name)
|
n.deleteNRT(nrt.Name)
|
||||||
}
|
}
|
||||||
|
@ -199,7 +204,7 @@ func (n *nfdGarbageCollector) periodicGC(gcPeriod time.Duration) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *nfdGarbageCollector) startNodeInformer() error {
|
func (n *nfdGarbageCollector) startNodeInformer() error {
|
||||||
nodeInformer := n.factory.Core().V1().Nodes().Informer()
|
nodeInformer := n.factory.ForResource(gvrNode).Informer()
|
||||||
|
|
||||||
if _, err := nodeInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
|
if _, err := nodeInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||||
DeleteFunc: n.deleteNodeHandler,
|
DeleteFunc: n.deleteNodeHandler,
|
||||||
|
|
|
@ -21,17 +21,16 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/k8stopologyawareschedwg/noderesourcetopology-api/pkg/apis/topology/v1alpha2"
|
topologyv1alpha2 "github.com/k8stopologyawareschedwg/noderesourcetopology-api/pkg/apis/topology/v1alpha2"
|
||||||
topologyclientset "github.com/k8stopologyawareschedwg/noderesourcetopology-api/pkg/generated/clientset/versioned"
|
|
||||||
faketopologyv1alpha2 "github.com/k8stopologyawareschedwg/noderesourcetopology-api/pkg/generated/clientset/versioned/fake"
|
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
"k8s.io/client-go/informers"
|
metadataclient "k8s.io/client-go/metadata"
|
||||||
k8sclientset "k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/metadata/fake"
|
||||||
fakek8sclientset "k8s.io/client-go/kubernetes/fake"
|
fakemetadataclient "k8s.io/client-go/metadata/fake"
|
||||||
fakenfdclientset "sigs.k8s.io/node-feature-discovery/api/generated/clientset/versioned/fake"
|
"k8s.io/client-go/metadata/metadatainformer"
|
||||||
|
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
)
|
)
|
||||||
|
@ -43,7 +42,7 @@ func TestNRTGC(t *testing.T) {
|
||||||
errChan := make(chan error)
|
errChan := make(chan error)
|
||||||
go func() { errChan <- gc.Run() }()
|
go func() { errChan <- gc.Run() }()
|
||||||
|
|
||||||
So(waitForNRT(gc.topoClient), ShouldBeTrue)
|
So(waitForNRT(gc.client), ShouldBeTrue)
|
||||||
|
|
||||||
gc.Stop()
|
gc.Stop()
|
||||||
So(<-errChan, ShouldBeNil)
|
So(<-errChan, ShouldBeNil)
|
||||||
|
@ -54,7 +53,7 @@ func TestNRTGC(t *testing.T) {
|
||||||
errChan := make(chan error)
|
errChan := make(chan error)
|
||||||
go func() { errChan <- gc.Run() }()
|
go func() { errChan <- gc.Run() }()
|
||||||
|
|
||||||
So(waitForNRT(gc.topoClient, "node1"), ShouldBeTrue)
|
So(waitForNRT(gc.client, "node1"), ShouldBeTrue)
|
||||||
|
|
||||||
gc.Stop()
|
gc.Stop()
|
||||||
So(<-errChan, ShouldBeNil)
|
So(<-errChan, ShouldBeNil)
|
||||||
|
@ -65,85 +64,85 @@ func TestNRTGC(t *testing.T) {
|
||||||
errChan := make(chan error)
|
errChan := make(chan error)
|
||||||
go func() { errChan <- gc.Run() }()
|
go func() { errChan <- gc.Run() }()
|
||||||
|
|
||||||
err := gc.k8sClient.CoreV1().Nodes().Delete(context.TODO(), "node1", metav1.DeleteOptions{})
|
gvr := corev1.SchemeGroupVersion.WithResource("nodes")
|
||||||
|
err := gc.client.Resource(gvr).Delete(context.TODO(), "node1", metav1.DeleteOptions{})
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
So(waitForNRT(gc.topoClient, "node2"), ShouldBeTrue)
|
So(waitForNRT(gc.client, "node2"), ShouldBeTrue)
|
||||||
})
|
})
|
||||||
Convey("periodic GC should remove obsolete NRT", t, func() {
|
Convey("periodic GC should remove obsolete NRT", t, func() {
|
||||||
gc := newMockGC([]string{"node1", "node2"}, []string{"node1", "node2"})
|
gc := newMockGC([]string{"node1", "node2"}, []string{"node1", "node2"})
|
||||||
// Override period to run fast
|
// Override period to run fast
|
||||||
gc.args.GCPeriod = 100 * time.Millisecond
|
gc.args.GCPeriod = 100 * time.Millisecond
|
||||||
|
|
||||||
nrt := v1alpha2.NodeResourceTopology{
|
nrt := createPartialObjectMetadata("topology.node.k8s.io/v1alpha2", "NodeResourceTopology", "", "not-existing")
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Name: "not-existing",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
errChan := make(chan error)
|
errChan := make(chan error)
|
||||||
go func() { errChan <- gc.Run() }()
|
go func() { errChan <- gc.Run() }()
|
||||||
|
|
||||||
_, err := gc.topoClient.TopologyV1alpha2().NodeResourceTopologies().Create(context.TODO(), &nrt, metav1.CreateOptions{})
|
gvr := topologyv1alpha2.SchemeGroupVersion.WithResource("noderesourcetopologies")
|
||||||
|
_, err := gc.client.Resource(gvr).(fake.MetadataClient).CreateFake(nrt, metav1.CreateOptions{})
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
So(waitForNRT(gc.topoClient, "node1", "node2"), ShouldBeTrue)
|
So(waitForNRT(gc.client, "node1", "node2"), ShouldBeTrue)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMockGC(nodes, nrts []string) *mockGC {
|
func newMockGC(nodes, nrts []string) *mockGC {
|
||||||
k8sClient := fakek8sclientset.NewSimpleClientset(createFakeNodes(nodes...)...)
|
// Create fake objects
|
||||||
|
objs := []runtime.Object{}
|
||||||
|
for _, name := range nodes {
|
||||||
|
objs = append(objs, createPartialObjectMetadata("v1", "Node", "", name))
|
||||||
|
}
|
||||||
|
for _, name := range nrts {
|
||||||
|
objs = append(objs, createPartialObjectMetadata("topology.node.k8s.io/v1alpha2", "NodeResourceTopology", "", name))
|
||||||
|
}
|
||||||
|
|
||||||
|
scheme := fake.NewTestScheme()
|
||||||
|
_ = metav1.AddMetaToScheme(scheme)
|
||||||
|
cli := fakemetadataclient.NewSimpleMetadataClient(scheme, objs...)
|
||||||
return &mockGC{
|
return &mockGC{
|
||||||
nfdGarbageCollector: nfdGarbageCollector{
|
nfdGarbageCollector: nfdGarbageCollector{
|
||||||
factory: informers.NewSharedInformerFactory(k8sClient, 5*time.Minute),
|
factory: metadatainformer.NewSharedInformerFactory(cli, 0),
|
||||||
nfdClient: fakenfdclientset.NewSimpleClientset(),
|
client: cli,
|
||||||
topoClient: faketopologyv1alpha2.NewSimpleClientset(createFakeNRTs(nrts...)...),
|
stopChan: make(chan struct{}),
|
||||||
stopChan: make(chan struct{}),
|
|
||||||
args: &Args{
|
args: &Args{
|
||||||
GCPeriod: 10 * time.Minute,
|
GCPeriod: 10 * time.Minute,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
k8sClient: k8sClient,
|
client: cli,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createFakeNodes(names ...string) []runtime.Object {
|
func createPartialObjectMetadata(apiVersion, kind, namespace, name string) *metav1.PartialObjectMetadata {
|
||||||
nodes := make([]runtime.Object, len(names))
|
return &metav1.PartialObjectMetadata{
|
||||||
for i, n := range names {
|
TypeMeta: metav1.TypeMeta{
|
||||||
nodes[i] = &corev1.Node{
|
APIVersion: apiVersion,
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
Kind: kind,
|
||||||
Name: n,
|
},
|
||||||
}}
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: namespace,
|
||||||
|
Name: name,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
return nodes
|
|
||||||
}
|
|
||||||
|
|
||||||
func createFakeNRTs(names ...string) []runtime.Object {
|
|
||||||
nrts := make([]runtime.Object, len(names))
|
|
||||||
for i, n := range names {
|
|
||||||
nrts[i] = &v1alpha2.NodeResourceTopology{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Name: n,
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
return nrts
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockGC struct {
|
type mockGC struct {
|
||||||
nfdGarbageCollector
|
nfdGarbageCollector
|
||||||
|
|
||||||
k8sClient k8sclientset.Interface
|
client metadataclient.Interface
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitForNRT(cli topologyclientset.Interface, names ...string) bool {
|
func waitForNRT(cli metadataclient.Interface, names ...string) bool {
|
||||||
nameSet := sets.NewString(names...)
|
nameSet := sets.NewString(names...)
|
||||||
|
gvr := topologyv1alpha2.SchemeGroupVersion.WithResource("noderesourcetopologies")
|
||||||
for i := 0; i < 2; i++ {
|
for i := 0; i < 2; i++ {
|
||||||
nrts, err := cli.TopologyV1alpha2().NodeResourceTopologies().List(context.TODO(), metav1.ListOptions{})
|
rsp, err := cli.Resource(gvr).List(context.TODO(), metav1.ListOptions{})
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
nrtNames := sets.NewString()
|
nrtNames := sets.NewString()
|
||||||
for _, nrt := range nrts.Items {
|
for _, meta := range rsp.Items {
|
||||||
nrtNames.Insert(nrt.Name)
|
nrtNames.Insert(meta.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
if nrtNames.Equal(nameSet) {
|
if nrtNames.Equal(nameSet) {
|
||||||
|
|
Loading…
Reference in a new issue