1
0
Fork 0
mirror of https://github.com/kubernetes-sigs/node-feature-discovery.git synced 2025-03-05 08:17:04 +00:00

Merge pull request #336 from marquiz/devel/122

nfd-master: patch node object instead of rewriting it
This commit is contained in:
Kubernetes Prow Robot 2020-11-24 04:51:00 -08:00 committed by GitHub
commit a0f59ef22f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 431 additions and 256 deletions

View file

@ -122,7 +122,10 @@ func argsParse(argv []string) (master.Args, error) {
return args, fmt.Errorf("error parsing whitelist regex (%s): %s", arguments["--label-whitelist"], err)
}
args.VerifyNodeName = arguments["--verify-node-name"].(bool)
args.ExtraLabelNs = strings.Split(arguments["--extra-label-ns"].(string), ",")
args.ExtraLabelNs = map[string]struct{}{}
for _, n := range strings.Split(arguments["--extra-label-ns"].(string), ",") {
args.ExtraLabelNs[n] = struct{}{}
}
args.ResourceLabels = strings.Split(arguments["--resource-labels"].(string), ",")
args.Prune = arguments["--prune"].(bool)
args.Kubeconfig = arguments["--kubeconfig"].(string)

1
go.mod
View file

@ -8,6 +8,7 @@ require (
github.com/klauspost/cpuid v1.2.3
github.com/onsi/ginkgo v1.11.0
github.com/onsi/gomega v1.7.0
github.com/smartystreets/assertions v1.2.0
github.com/smartystreets/goconvey v1.6.4
github.com/stretchr/testify v1.4.0
github.com/vektra/errors v0.0.0-20140903201135-c64d83aba85a

2
go.sum
View file

@ -572,6 +572,8 @@ github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E=

View file

@ -35,6 +35,9 @@ type APIHelpers interface {
// UpdateNode updates the node via the API server using a client.
UpdateNode(*k8sclient.Clientset, *api.Node) error
// PatchStatus updates the node status via the API server using a client.
PatchStatus(*k8sclient.Clientset, string, interface{}) error
// PatchNode updates the node object via the API server using a client.
PatchNode(*k8sclient.Clientset, string, []JsonPatch) error
// PatchNodeStatus updates the node status via the API server using a client.
PatchNodeStatus(*k8sclient.Clientset, string, []JsonPatch) error
}

View file

@ -0,0 +1,34 @@
/*
Copyright 2020 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 apihelper
import (
"path/filepath"
"strings"
)
// JsonPatch is a json marshaling helper used for patching API objects
type JsonPatch struct {
Op string `json:"op"`
Path string `json:"path"`
Value string `json:"value,omitempty"`
}
// NewJsonPatch returns a new JsonPatch object
func NewJsonPatch(verb string, path string, key string, value string) JsonPatch {
return JsonPatch{verb, filepath.Join(path, strings.ReplaceAll(key, "/", "~1")), value}
}

View file

@ -78,12 +78,25 @@ func (h K8sHelpers) UpdateNode(c *k8sclient.Clientset, n *api.Node) error {
return nil
}
func (h K8sHelpers) PatchStatus(c *k8sclient.Clientset, nodeName string, marshalable interface{}) error {
// Send the updated node to the apiserver.
patch, err := json.Marshal(marshalable)
if err == nil {
_, err = c.CoreV1().Nodes().Patch(context.TODO(), nodeName, types.JSONPatchType, patch, meta_v1.PatchOptions{}, "status")
func (h K8sHelpers) PatchNode(c *k8sclient.Clientset, nodeName string, patches []JsonPatch) error {
if len(patches) > 0 {
data, err := json.Marshal(patches)
if err == nil {
_, err = c.CoreV1().Nodes().Patch(context.TODO(), nodeName, types.JSONPatchType, data, meta_v1.PatchOptions{})
}
return err
}
return err
return nil
}
func (h K8sHelpers) PatchNodeStatus(c *k8sclient.Clientset, nodeName string, patches []JsonPatch) error {
if len(patches) > 0 {
data, err := json.Marshal(patches)
if err == nil {
_, err = c.CoreV1().Nodes().Patch(context.TODO(), nodeName, types.JSONPatchType, data, meta_v1.PatchOptions{}, "status")
}
return err
}
return nil
}

View file

@ -85,12 +85,26 @@ func (_m *MockAPIHelpers) GetNodes(_a0 *kubernetes.Clientset) (*v1.NodeList, err
return r0, r1
}
// PatchStatus provides a mock function with given fields: _a0, _a1, _a2
func (_m *MockAPIHelpers) PatchStatus(_a0 *kubernetes.Clientset, _a1 string, _a2 interface{}) error {
// PatchNode provides a mock function with given fields: _a0, _a1, _a2
func (_m *MockAPIHelpers) PatchNode(_a0 *kubernetes.Clientset, _a1 string, _a2 []JsonPatch) error {
ret := _m.Called(_a0, _a1, _a2)
var r0 error
if rf, ok := ret.Get(0).(func(*kubernetes.Clientset, string, interface{}) error); ok {
if rf, ok := ret.Get(0).(func(*kubernetes.Clientset, string, []JsonPatch) error); ok {
r0 = rf(_a0, _a1, _a2)
} else {
r0 = ret.Error(0)
}
return r0
}
// PatchNodeStatus provides a mock function with given fields: _a0, _a1, _a2
func (_m *MockAPIHelpers) PatchNodeStatus(_a0 *kubernetes.Clientset, _a1 string, _a2 []JsonPatch) error {
ret := _m.Called(_a0, _a1, _a2)
var r0 error
if rf, ok := ret.Get(0).(func(*kubernetes.Clientset, string, []JsonPatch) error); ok {
r0 = rf(_a0, _a1, _a2)
} else {
r0 = ret.Error(0)

View file

@ -22,6 +22,7 @@ import (
"strings"
"testing"
"github.com/smartystreets/assertions"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/mock"
"github.com/vektra/errors"
@ -33,6 +34,7 @@ import (
"sigs.k8s.io/node-feature-discovery/pkg/apihelper"
"sigs.k8s.io/node-feature-discovery/pkg/labeler"
"sigs.k8s.io/node-feature-discovery/pkg/version"
"sigs.k8s.io/yaml"
)
const (
@ -54,43 +56,58 @@ func newMockNode() *api.Node {
func TestUpdateNodeFeatures(t *testing.T) {
Convey("When I update the node using fake client", t, func() {
fakeFeatureLabels := map[string]string{"source-feature.1": "1", "source-feature.2": "2", "source-feature.3": "val3"}
fakeAnnotations := map[string]string{"version": version.Get()}
fakeExtResources := ExtendedResources{"source-feature.1": "", "source-feature.2": ""}
fakeFeatureLabels := map[string]string{LabelNs + "/source-feature.1": "1", LabelNs + "/source-feature.2": "2", LabelNs + "/source-feature.3": "val3"}
fakeAnnotations := map[string]string{"my-annotation": "my-val"}
fakeExtResources := ExtendedResources{LabelNs + "/source-feature.1": "1", LabelNs + "/source-feature.2": "2"}
fakeFeatureLabelNames := make([]string, 0, len(fakeFeatureLabels))
for k := range fakeFeatureLabels {
fakeFeatureLabelNames = append(fakeFeatureLabelNames, k)
fakeFeatureLabelNames = append(fakeFeatureLabelNames, strings.TrimPrefix(k, LabelNs+"/"))
}
sort.Strings(fakeFeatureLabelNames)
fakeAnnotations["feature-labels"] = strings.Join(fakeFeatureLabelNames, ",")
fakeExtResourceNames := make([]string, 0, len(fakeExtResources))
for k := range fakeExtResources {
fakeExtResourceNames = append(fakeExtResourceNames, strings.TrimPrefix(k, LabelNs+"/"))
}
sort.Strings(fakeExtResourceNames)
mockAPIHelper := new(apihelper.MockAPIHelpers)
mockClient := &k8sclient.Clientset{}
// Mock node with old features
mockNode := newMockNode()
mockNode.Labels[LabelNs+"old-feature"] = "old-value"
mockNode.Annotations[AnnotationNs+"feature-labels"] = "old-feature"
mockNode.Labels[LabelNs+"/old-feature"] = "old-value"
mockNode.Annotations[AnnotationNs+"/feature-labels"] = "old-feature"
Convey("When I successfully update the node with feature labels", func() {
// Create a list of expected node metadata patches
metadataPatches := []apihelper.JsonPatch{
apihelper.NewJsonPatch("replace", "/metadata/annotations", AnnotationNs+"/feature-labels", strings.Join(fakeFeatureLabelNames, ",")),
apihelper.NewJsonPatch("add", "/metadata/annotations", AnnotationNs+"/extended-resources", strings.Join(fakeExtResourceNames, ",")),
apihelper.NewJsonPatch("remove", "/metadata/labels", LabelNs+"/old-feature", ""),
}
for k, v := range fakeFeatureLabels {
metadataPatches = append(metadataPatches, apihelper.NewJsonPatch("add", "/metadata/labels", k, v))
}
for k, v := range fakeAnnotations {
metadataPatches = append(metadataPatches, apihelper.NewJsonPatch("add", "/metadata/annotations", k, v))
}
// Create a list of expected node status patches
statusPatches := []apihelper.JsonPatch{}
for k, v := range fakeExtResources {
statusPatches = append(statusPatches, apihelper.NewJsonPatch("add", "/status/capacity", k, v))
}
mockAPIHelper.On("GetClient").Return(mockClient, nil)
mockAPIHelper.On("GetNode", mockClient, mockNodeName).Return(mockNode, nil).Once()
mockAPIHelper.On("UpdateNode", mockClient, mockNode).Return(nil).Once()
mockAPIHelper.On("PatchStatus", mockClient, mockNodeName, mock.Anything).Return(nil).Twice()
mockAPIHelper.On("PatchNode", mockClient, mockNodeName, mock.MatchedBy(jsonPatchMatcher(metadataPatches))).Return(nil)
mockAPIHelper.On("PatchNodeStatus", mockClient, mockNodeName, mock.MatchedBy(jsonPatchMatcher(statusPatches))).Return(nil)
err := updateNodeFeatures(mockAPIHelper, mockNodeName, fakeFeatureLabels, fakeAnnotations, fakeExtResources)
Convey("Error is nil", func() {
So(err, ShouldBeNil)
})
Convey("Node object should have updated with labels and annotations", func() {
So(len(mockNode.Labels), ShouldEqual, len(fakeFeatureLabels))
for k, v := range fakeFeatureLabels {
So(mockNode.Labels[LabelNs+k], ShouldEqual, v)
}
So(len(mockNode.Annotations), ShouldEqual, len(fakeAnnotations))
for k, v := range fakeAnnotations {
So(mockNode.Annotations[AnnotationNs+k], ShouldEqual, v)
}
})
})
Convey("When I fail to update the node with feature labels", func() {
@ -128,7 +145,7 @@ func TestUpdateNodeFeatures(t *testing.T) {
expectedError := errors.New("fake error")
mockAPIHelper.On("GetClient").Return(mockClient, nil)
mockAPIHelper.On("GetNode", mockClient, mockNodeName).Return(mockNode, nil).Once()
mockAPIHelper.On("UpdateNode", mockClient, mockNode).Return(expectedError).Once()
mockAPIHelper.On("PatchNode", mockClient, mockNodeName, mock.Anything).Return(expectedError).Once()
err := updateNodeFeatures(mockAPIHelper, mockNodeName, fakeFeatureLabels, fakeAnnotations, fakeExtResources)
Convey("Error is produced", func() {
@ -145,9 +162,11 @@ func TestUpdateMasterNode(t *testing.T) {
mockClient := &k8sclient.Clientset{}
mockNode := newMockNode()
Convey("When update operation succeeds", func() {
expectedPatches := []apihelper.JsonPatch{
apihelper.NewJsonPatch("add", "/metadata/annotations", AnnotationNs+"/master.version", version.Get())}
mockHelper.On("GetClient").Return(mockClient, nil)
mockHelper.On("GetNode", mockClient, mockNodeName).Return(mockNode, nil)
mockHelper.On("UpdateNode", mockClient, mockNode).Return(nil)
mockHelper.On("PatchNode", mockClient, mockNodeName, mock.MatchedBy(jsonPatchMatcher(expectedPatches))).Return(nil)
err := updateMasterNode(mockHelper)
Convey("No error should be returned", func() {
So(err, ShouldBeNil)
@ -175,7 +194,7 @@ func TestUpdateMasterNode(t *testing.T) {
Convey("When updating node object fails", func() {
mockHelper.On("GetClient").Return(mockClient, nil)
mockHelper.On("GetNode", mockClient, mockNodeName).Return(mockNode, nil)
mockHelper.On("UpdateNode", mockClient, mockNode).Return(mockErr)
mockHelper.On("PatchNode", mockClient, mockNodeName, mock.Anything).Return(mockErr)
err := updateMasterNode(mockHelper)
Convey("An error should be returned", func() {
So(err, ShouldEqual, mockErr)
@ -189,31 +208,39 @@ func TestAddingExtResources(t *testing.T) {
Convey("When there are no matching labels", func() {
mockNode := newMockNode()
mockResourceLabels := ExtendedResources{}
resourceOps := getExtendedResourceOps(mockNode, mockResourceLabels)
So(len(resourceOps), ShouldEqual, 0)
patches := createExtendedResourcePatches(mockNode, mockResourceLabels)
So(len(patches), ShouldEqual, 0)
})
Convey("When there are matching labels", func() {
mockNode := newMockNode()
mockResourceLabels := ExtendedResources{"feature-1": "1", "feature-2": "2"}
resourceOps := getExtendedResourceOps(mockNode, mockResourceLabels)
So(len(resourceOps), ShouldBeGreaterThan, 0)
expectedPatches := []apihelper.JsonPatch{
apihelper.NewJsonPatch("add", "/status/capacity", "feature-1", "1"),
apihelper.NewJsonPatch("add", "/status/capacity", "feature-2", "2"),
}
patches := createExtendedResourcePatches(mockNode, mockResourceLabels)
So(sortJsonPatches(patches), ShouldResemble, sortJsonPatches(expectedPatches))
})
Convey("When the resource already exists", func() {
mockNode := newMockNode()
mockNode.Status.Capacity[api.ResourceName(LabelNs+"feature-1")] = *resource.NewQuantity(1, resource.BinarySI)
mockResourceLabels := ExtendedResources{"feature-1": "1"}
resourceOps := getExtendedResourceOps(mockNode, mockResourceLabels)
So(len(resourceOps), ShouldEqual, 0)
mockNode.Status.Capacity[api.ResourceName(LabelNs+"/feature-1")] = *resource.NewQuantity(1, resource.BinarySI)
mockResourceLabels := ExtendedResources{LabelNs + "/feature-1": "1"}
patches := createExtendedResourcePatches(mockNode, mockResourceLabels)
So(len(patches), ShouldEqual, 0)
})
Convey("When the resource already exists but its capacity has changed", func() {
mockNode := newMockNode()
mockNode.Status.Capacity[api.ResourceName(LabelNs+"feature-1")] = *resource.NewQuantity(2, resource.BinarySI)
mockNode.Status.Capacity[api.ResourceName("feature-1")] = *resource.NewQuantity(2, resource.BinarySI)
mockResourceLabels := ExtendedResources{"feature-1": "1"}
resourceOps := getExtendedResourceOps(mockNode, mockResourceLabels)
So(len(resourceOps), ShouldBeGreaterThan, 0)
expectedPatches := []apihelper.JsonPatch{
apihelper.NewJsonPatch("replace", "/status/capacity", "feature-1", "1"),
apihelper.NewJsonPatch("replace", "/status/allocatable", "feature-1", "1"),
}
patches := createExtendedResourcePatches(mockNode, mockResourceLabels)
So(sortJsonPatches(patches), ShouldResemble, sortJsonPatches(expectedPatches))
})
})
}
@ -222,30 +249,30 @@ func TestRemovingExtResources(t *testing.T) {
Convey("When removing extended resources", t, func() {
Convey("When none are removed", func() {
mockNode := newMockNode()
mockResourceLabels := ExtendedResources{"feature-1": "1", "feature-2": "2"}
mockNode.Annotations[AnnotationNs+"extended-resources"] = "feature-1,feature-2"
mockNode.Status.Capacity[api.ResourceName(LabelNs+"feature-1")] = *resource.NewQuantity(1, resource.BinarySI)
mockNode.Status.Capacity[api.ResourceName(LabelNs+"feature-2")] = *resource.NewQuantity(2, resource.BinarySI)
resourceOps := getExtendedResourceOps(mockNode, mockResourceLabels)
So(len(resourceOps), ShouldEqual, 0)
mockResourceLabels := ExtendedResources{LabelNs + "/feature-1": "1", LabelNs + "/feature-2": "2"}
mockNode.Annotations[AnnotationNs+"/extended-resources"] = "feature-1,feature-2"
mockNode.Status.Capacity[api.ResourceName(LabelNs+"/feature-1")] = *resource.NewQuantity(1, resource.BinarySI)
mockNode.Status.Capacity[api.ResourceName(LabelNs+"/feature-2")] = *resource.NewQuantity(2, resource.BinarySI)
patches := createExtendedResourcePatches(mockNode, mockResourceLabels)
So(len(patches), ShouldEqual, 0)
})
Convey("When the related label is gone", func() {
mockNode := newMockNode()
mockResourceLabels := ExtendedResources{"feature-4": "", "feature-2": "2"}
mockNode.Annotations[AnnotationNs+"extended-resources"] = "feature-4,feature-2"
mockNode.Status.Capacity[api.ResourceName(LabelNs+"feature-4")] = *resource.NewQuantity(4, resource.BinarySI)
mockNode.Status.Capacity[api.ResourceName(LabelNs+"feature-2")] = *resource.NewQuantity(2, resource.BinarySI)
resourceOps := getExtendedResourceOps(mockNode, mockResourceLabels)
So(len(resourceOps), ShouldBeGreaterThan, 0)
mockResourceLabels := ExtendedResources{LabelNs + "/feature-4": "", LabelNs + "/feature-2": "2"}
mockNode.Annotations[AnnotationNs+"/extended-resources"] = "feature-4,feature-2"
mockNode.Status.Capacity[api.ResourceName(LabelNs+"/feature-4")] = *resource.NewQuantity(4, resource.BinarySI)
mockNode.Status.Capacity[api.ResourceName(LabelNs+"/feature-2")] = *resource.NewQuantity(2, resource.BinarySI)
patches := createExtendedResourcePatches(mockNode, mockResourceLabels)
So(len(patches), ShouldBeGreaterThan, 0)
})
Convey("When the extended resource is no longer wanted", func() {
mockNode := newMockNode()
mockNode.Status.Capacity[api.ResourceName(LabelNs+"feature-1")] = *resource.NewQuantity(1, resource.BinarySI)
mockNode.Status.Capacity[api.ResourceName(LabelNs+"feature-2")] = *resource.NewQuantity(2, resource.BinarySI)
mockResourceLabels := ExtendedResources{"feature-2": "2"}
mockNode.Annotations[AnnotationNs+"extended-resources"] = "feature-1,feature-2"
resourceOps := getExtendedResourceOps(mockNode, mockResourceLabels)
So(len(resourceOps), ShouldBeGreaterThan, 0)
mockNode.Status.Capacity[api.ResourceName(LabelNs+"/feature-1")] = *resource.NewQuantity(1, resource.BinarySI)
mockNode.Status.Capacity[api.ResourceName(LabelNs+"/feature-2")] = *resource.NewQuantity(2, resource.BinarySI)
mockResourceLabels := ExtendedResources{LabelNs + "/feature-2": "2"}
mockNode.Annotations[AnnotationNs+"/extended-resources"] = "feature-1,feature-2"
patches := createExtendedResourcePatches(mockNode, mockResourceLabels)
So(len(patches), ShouldBeGreaterThan, 0)
})
})
}
@ -259,7 +286,8 @@ func TestSetLabels(t *testing.T) {
mockNode := newMockNode()
mockServer := labelerServer{args: Args{LabelWhiteList: regexp.MustCompile("")}, apiHelper: mockHelper}
mockCtx := context.Background()
mockLabels := map[string]string{"feature-1": "val-1", "feature-2": "val-2", "feature-3": "val-3"}
// In the gRPC request the label names may omit the default ns
mockLabels := map[string]string{"feature-1": "1", "feature-2": "val-2", "feature-3": "3"}
mockReq := &labeler.SetLabelsRequest{NodeName: workerName, NfdVersion: workerVer, Labels: mockLabels}
mockLabelNames := make([]string, 0, len(mockLabels))
@ -267,69 +295,94 @@ func TestSetLabels(t *testing.T) {
mockLabelNames = append(mockLabelNames, k)
}
sort.Strings(mockLabelNames)
expectedAnnotations := map[string]string{"worker.version": workerVer}
expectedAnnotations["feature-labels"] = strings.Join(mockLabelNames, ",")
expectedAnnotations["extended-resources"] = ""
expectedStatusPatches := []apihelper.JsonPatch{}
Convey("When node update succeeds", func() {
expectedPatches := []apihelper.JsonPatch{
apihelper.NewJsonPatch("add", "/metadata/annotations", workerVersionAnnotation, workerVer),
apihelper.NewJsonPatch("add", "/metadata/annotations", featureLabelAnnotation, strings.Join(mockLabelNames, ",")),
apihelper.NewJsonPatch("add", "/metadata/annotations", extendedResourceAnnotation, ""),
}
for k, v := range mockLabels {
expectedPatches = append(expectedPatches, apihelper.NewJsonPatch("add", "/metadata/labels", LabelNs+"/"+k, v))
}
mockHelper.On("GetClient").Return(mockClient, nil)
mockHelper.On("GetNode", mockClient, workerName).Return(mockNode, nil)
mockHelper.On("UpdateNode", mockClient, mockNode).Return(nil)
mockHelper.On("PatchNode", mockClient, mockNodeName, mock.MatchedBy(jsonPatchMatcher(expectedPatches))).Return(nil)
mockHelper.On("PatchNodeStatus", mockClient, mockNodeName, mock.MatchedBy(jsonPatchMatcher(expectedStatusPatches))).Return(nil)
_, err := mockServer.SetLabels(mockCtx, mockReq)
Convey("No error should be returned", func() {
So(err, ShouldBeNil)
})
Convey("Node object should have updated with labels and annotations", func() {
So(len(mockNode.Labels), ShouldEqual, len(mockLabels))
for k, v := range mockLabels {
So(mockNode.Labels[LabelNs+k], ShouldEqual, v)
}
So(len(mockNode.Annotations), ShouldEqual, len(expectedAnnotations))
for k, v := range expectedAnnotations {
So(mockNode.Annotations[AnnotationNs+k], ShouldEqual, v)
}
})
})
Convey("When --label-whitelist is specified", func() {
expectedPatches := []apihelper.JsonPatch{
apihelper.NewJsonPatch("add", "/metadata/annotations", workerVersionAnnotation, workerVer),
apihelper.NewJsonPatch("add", "/metadata/annotations", featureLabelAnnotation, "feature-2"),
apihelper.NewJsonPatch("add", "/metadata/annotations", extendedResourceAnnotation, ""),
apihelper.NewJsonPatch("add", "/metadata/labels", LabelNs+"/feature-2", mockLabels["feature-2"]),
}
mockServer.args.LabelWhiteList = regexp.MustCompile("^f.*2$")
mockHelper.On("GetClient").Return(mockClient, nil)
mockHelper.On("GetNode", mockClient, workerName).Return(mockNode, nil)
mockHelper.On("UpdateNode", mockClient, mockNode).Return(nil)
mockHelper.On("PatchNode", mockClient, mockNodeName, mock.MatchedBy(jsonPatchMatcher(expectedPatches))).Return(nil)
mockHelper.On("PatchNodeStatus", mockClient, mockNodeName, mock.MatchedBy(jsonPatchMatcher(expectedStatusPatches))).Return(nil)
_, err := mockServer.SetLabels(mockCtx, mockReq)
Convey("Error is nil", func() {
So(err, ShouldBeNil)
})
Convey("Node object should only have whitelisted labels", func() {
So(len(mockNode.Labels), ShouldEqual, 1)
So(mockNode.Labels, ShouldResemble, map[string]string{LabelNs + "feature-2": "val-2"})
a := map[string]string{AnnotationNs + "worker.version": workerVer, AnnotationNs + "feature-labels": "feature-2", AnnotationNs + "extended-resources": ""}
So(len(mockNode.Annotations), ShouldEqual, len(a))
So(mockNode.Annotations, ShouldResemble, a)
})
})
Convey("When --extra-label-ns is specified", func() {
mockServer.args.ExtraLabelNs = []string{"valid.ns"}
mockHelper.On("GetClient").Return(mockClient, nil)
mockHelper.On("GetNode", mockClient, workerName).Return(mockNode, nil)
mockHelper.On("UpdateNode", mockClient, mockNode).Return(nil)
// In the gRPC request the label names may omit the default ns
mockLabels := map[string]string{"feature-1": "val-1",
"valid.ns/feature-2": "val-2",
"invalid.ns/feature-3": "val-3"}
expectedPatches := []apihelper.JsonPatch{
apihelper.NewJsonPatch("add", "/metadata/annotations", workerVersionAnnotation, workerVer),
apihelper.NewJsonPatch("add", "/metadata/annotations", featureLabelAnnotation, "feature-1,valid.ns/feature-2"),
apihelper.NewJsonPatch("add", "/metadata/annotations", extendedResourceAnnotation, ""),
apihelper.NewJsonPatch("add", "/metadata/labels", LabelNs+"/feature-1", mockLabels["feature-1"]),
apihelper.NewJsonPatch("add", "/metadata/labels", "valid.ns/feature-2", mockLabels["valid.ns/feature-2"]),
}
mockServer.args.ExtraLabelNs = map[string]struct{}{"valid.ns": struct{}{}}
mockHelper.On("GetClient").Return(mockClient, nil)
mockHelper.On("GetNode", mockClient, workerName).Return(mockNode, nil)
mockHelper.On("PatchNode", mockClient, mockNodeName, mock.MatchedBy(jsonPatchMatcher(expectedPatches))).Return(nil)
mockHelper.On("PatchNodeStatus", mockClient, mockNodeName, mock.MatchedBy(jsonPatchMatcher(expectedStatusPatches))).Return(nil)
mockReq := &labeler.SetLabelsRequest{NodeName: workerName, NfdVersion: workerVer, Labels: mockLabels}
_, err := mockServer.SetLabels(mockCtx, mockReq)
Convey("Error is nil", func() {
So(err, ShouldBeNil)
})
Convey("Node object should only have allowed label namespaces", func() {
So(len(mockNode.Labels), ShouldEqual, 2)
So(mockNode.Labels, ShouldResemble, map[string]string{LabelNs + "feature-1": "val-1", "valid.ns/feature-2": "val-2"})
a := map[string]string{AnnotationNs + "worker.version": workerVer, AnnotationNs + "feature-labels": "feature-1,valid.ns/feature-2", AnnotationNs + "extended-resources": ""}
So(len(mockNode.Annotations), ShouldEqual, len(a))
So(mockNode.Annotations, ShouldResemble, a)
})
Convey("When --resource-labels is specified", func() {
expectedPatches := []apihelper.JsonPatch{
apihelper.NewJsonPatch("add", "/metadata/annotations", workerVersionAnnotation, workerVer),
apihelper.NewJsonPatch("add", "/metadata/annotations", featureLabelAnnotation, "feature-2"),
apihelper.NewJsonPatch("add", "/metadata/annotations", extendedResourceAnnotation, "feature-1,feature-3"),
apihelper.NewJsonPatch("add", "/metadata/labels", LabelNs+"/feature-2", mockLabels["feature-2"]),
}
expectedStatusPatches := []apihelper.JsonPatch{
apihelper.NewJsonPatch("add", "/status/capacity", LabelNs+"/feature-1", mockLabels["feature-1"]),
apihelper.NewJsonPatch("add", "/status/capacity", LabelNs+"/feature-3", mockLabels["feature-3"]),
}
mockServer.args.ResourceLabels = []string{"feature-3", "feature-1"}
mockHelper.On("GetClient").Return(mockClient, nil)
mockHelper.On("GetNode", mockClient, workerName).Return(mockNode, nil)
mockHelper.On("PatchNode", mockClient, mockNodeName, mock.MatchedBy(jsonPatchMatcher(expectedPatches))).Return(nil)
mockHelper.On("PatchNodeStatus", mockClient, mockNodeName, mock.MatchedBy(jsonPatchMatcher(expectedStatusPatches))).Return(nil)
_, err := mockServer.SetLabels(mockCtx, mockReq)
Convey("Error is nil", func() {
So(err, ShouldBeNil)
})
})
@ -352,28 +405,46 @@ func TestSetLabels(t *testing.T) {
})
}
func TestAddLabels(t *testing.T) {
Convey("When adding labels", t, func() {
labels := map[string]string{}
n := &api.Node{
ObjectMeta: meta_v1.ObjectMeta{
Labels: map[string]string{},
},
}
func TestCreatePatches(t *testing.T) {
Convey("When creating JSON patches", t, func() {
existingItems := map[string]string{"key-1": "val-1", "key-2": "val-2", "key-3": "val-3"}
jsonPath := "/root"
Convey("If no labels are passed", func() {
addLabels(n, labels)
Convey("None should be added", func() {
So(len(n.Labels), ShouldEqual, 0)
})
Convey("When when there are neither itmes to remoe nor to add or update", func() {
p := createPatches([]string{"foo", "bar"}, existingItems, map[string]string{}, jsonPath)
So(len(p), ShouldEqual, 0)
})
Convey("They should be added to the node.Labels", func() {
test1 := "test1"
labels[test1] = "true"
addLabels(n, labels)
So(n.Labels, ShouldContainKey, LabelNs+test1)
Convey("When when there are itmes to remoe but none to add or update", func() {
p := createPatches([]string{"key-2", "key-3", "foo"}, existingItems, map[string]string{}, jsonPath)
expected := []apihelper.JsonPatch{
apihelper.NewJsonPatch("remove", jsonPath, "key-2", ""),
apihelper.NewJsonPatch("remove", jsonPath, "key-3", ""),
}
So(sortJsonPatches(p), ShouldResemble, sortJsonPatches(expected))
})
Convey("When when there are no itmes to remove but new items to add", func() {
newItems := map[string]string{"new-key": "new-val", "key-1": "new-1"}
p := createPatches([]string{"key-1"}, existingItems, newItems, jsonPath)
expected := []apihelper.JsonPatch{
apihelper.NewJsonPatch("add", jsonPath, "new-key", newItems["new-key"]),
apihelper.NewJsonPatch("replace", jsonPath, "key-1", newItems["key-1"]),
}
So(sortJsonPatches(p), ShouldResemble, sortJsonPatches(expected))
})
Convey("When when there are items to remove add and update", func() {
newItems := map[string]string{"new-key": "new-val", "key-2": "new-2", "key-4": "val-4"}
p := createPatches([]string{"key-1", "key-2", "key-3", "foo"}, existingItems, newItems, jsonPath)
expected := []apihelper.JsonPatch{
apihelper.NewJsonPatch("add", jsonPath, "new-key", newItems["new-key"]),
apihelper.NewJsonPatch("add", jsonPath, "key-4", newItems["key-4"]),
apihelper.NewJsonPatch("replace", jsonPath, "key-2", newItems["key-2"]),
apihelper.NewJsonPatch("remove", jsonPath, "key-1", ""),
apihelper.NewJsonPatch("remove", jsonPath, "key-3", ""),
}
So(sortJsonPatches(p), ShouldResemble, sortJsonPatches(expected))
})
})
}
@ -391,16 +462,16 @@ func TestRemoveLabelsWithPrefix(t *testing.T) {
}
Convey("a unique label should be removed", func() {
removeLabelsWithPrefix(n, "single")
So(len(n.Labels), ShouldEqual, 2)
So(n.Labels, ShouldNotContainKey, "single")
p := removeLabelsWithPrefix(n, "single")
So(p, ShouldResemble, []apihelper.JsonPatch{apihelper.NewJsonPatch("remove", "/metadata/labels", "single-label", "")})
})
Convey("a non-unique search string should remove all matching keys", func() {
removeLabelsWithPrefix(n, "multiple")
So(len(n.Labels), ShouldEqual, 1)
So(n.Labels, ShouldNotContainKey, "multiple_A")
So(n.Labels, ShouldNotContainKey, "multiple_B")
p := removeLabelsWithPrefix(n, "multiple")
So(sortJsonPatches(p), ShouldResemble, sortJsonPatches([]apihelper.JsonPatch{
apihelper.NewJsonPatch("remove", "/metadata/labels", "multiple_A", ""),
apihelper.NewJsonPatch("remove", "/metadata/labels", "multiple_B", ""),
}))
})
Convey("a search string with no matches should not alter labels", func() {
@ -412,3 +483,25 @@ func TestRemoveLabelsWithPrefix(t *testing.T) {
})
})
}
func jsonPatchMatcher(expected []apihelper.JsonPatch) func([]apihelper.JsonPatch) bool {
return func(actual []apihelper.JsonPatch) bool {
// We don't care about modifying the original slices
ok, msg := assertions.So(sortJsonPatches(actual), ShouldResemble, sortJsonPatches(expected))
if !ok {
// We parse the cryptic string message for better readability
var f assertions.FailureView
if err := yaml.Unmarshal([]byte(msg), &f); err == nil {
Printf("%s\n", f.Message)
} else {
Printf("%s\n", msg)
}
}
return ok
}
}
func sortJsonPatches(p []apihelper.JsonPatch) []apihelper.JsonPatch {
sort.Slice(p, func(i, j int) bool { return p[i].Path < p[j].Path })
return p
}

View file

@ -24,6 +24,7 @@ import (
"log"
"net"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
@ -42,10 +43,16 @@ import (
const (
// Namespace for feature labels
LabelNs = "feature.node.kubernetes.io/"
LabelNs = "feature.node.kubernetes.io"
// Namespace for all NFD-related annotations
AnnotationNs = "nfd.node.kubernetes.io/"
AnnotationNs = "nfd.node.kubernetes.io"
// NFD Annotations
extendedResourceAnnotation = AnnotationNs + "/extended-resources"
featureLabelAnnotation = AnnotationNs + "/feature-labels"
masterVersionAnnotation = AnnotationNs + "/master.version"
workerVersionAnnotation = AnnotationNs + "/worker.version"
)
// package loggers
@ -68,7 +75,7 @@ type Annotations map[string]string
type Args struct {
CaFile string
CertFile string
ExtraLabelNs []string
ExtraLabelNs map[string]struct{}
KeyFile string
Kubeconfig string
LabelWhiteList *regexp.Regexp
@ -92,21 +99,6 @@ type nfdMaster struct {
apihelper apihelper.APIHelpers
}
// statusOp is a json marshaling helper used for patching node status
type statusOp struct {
Op string `json:"op"`
Path string `json:"path"`
Value string `json:"value,omitempty"`
}
func createStatusOp(verb string, resource string, path string, value string) statusOp {
if !strings.Contains(resource, "/") {
resource = LabelNs + resource
}
res := strings.ReplaceAll(resource, "/", "~1")
return statusOp{verb, "/status/" + path + "/" + res, value}
}
// Create new NfdMaster server instance.
func NewNfdMaster(args Args) (NfdMaster, error) {
nfd := &nfdMaster{args: args, ready: make(chan bool, 1)}
@ -260,60 +252,62 @@ func updateMasterNode(helper apihelper.APIHelpers) error {
}
// Advertise NFD version as an annotation
addAnnotations(node, Annotations{"master.version": version.Get()})
err = helper.UpdateNode(cli, node)
p := createPatches(nil, node.Annotations, Annotations{masterVersionAnnotation: version.Get()}, "/metadata/annotations")
err = helper.PatchNode(cli, node.Name, p)
if err != nil {
stderrLogger.Printf("can't update node: %s", err.Error())
stderrLogger.Printf("failed to patch node annotations: %v", err)
return err
}
return nil
}
// Filter labels by namespace and name whitelist
func filterFeatureLabels(labels Labels, extraLabelNs []string, labelWhiteList *regexp.Regexp, extendedResourceNames []string) (Labels, ExtendedResources) {
for label := range labels {
split := strings.SplitN(label, "/", 2)
name := split[0]
// Filter labels by namespace and name whitelist, and, turn selected labels
// into extended resources. This function also handles proper namespacing of
// labels and ERs, i.e. adds the possibly missing default namespace for labels
// arriving through the gRPC API.
func filterFeatureLabels(labels Labels, extraLabelNs map[string]struct{}, labelWhiteList *regexp.Regexp, extendedResourceNames []string) (Labels, ExtendedResources) {
outLabels := Labels{}
// Check namespaced labels, filter out if ns is not whitelisted
if len(split) == 2 {
ns := split[0]
name = split[1]
for i, extraNs := range extraLabelNs {
if ns == extraNs {
break
} else if i == len(extraLabelNs)-1 {
stderrLogger.Printf("Namespace '%s' is not allowed. Ignoring label '%s'\n", ns, label)
delete(labels, label)
}
for label, value := range labels {
// Add possibly missing default ns
label := addNs(label, LabelNs)
ns, name := splitNs(label)
// Check label namespace, filter out if ns is not whitelisted
if ns != LabelNs {
if _, ok := extraLabelNs[ns]; !ok {
stderrLogger.Printf("Namespace '%s' is not allowed. Ignoring label '%s'\n", ns, label)
continue
}
}
// Skip if label doesn't match labelWhiteList
if !labelWhiteList.MatchString(name) {
stderrLogger.Printf("%s does not match the whitelist (%s) and will not be published.", name, labelWhiteList.String())
delete(labels, label)
stderrLogger.Printf("%s (%s) does not match the whitelist (%s) and will not be published.", name, label, labelWhiteList.String())
continue
}
outLabels[label] = value
}
// Remove labels which are intended to be extended resources
extendedResources := ExtendedResources{}
for _, extendedResourceName := range extendedResourceNames {
// remove possibly given default LabelNs to keep annotations shorter
extendedResourceName = strings.TrimPrefix(extendedResourceName, LabelNs)
if _, ok := labels[extendedResourceName]; ok {
if _, err := strconv.Atoi(labels[extendedResourceName]); err != nil {
stderrLogger.Printf("bad label value encountered for extended resource: %s", err.Error())
// Add possibly missing default ns
extendedResourceName = addNs(extendedResourceName, LabelNs)
if value, ok := outLabels[extendedResourceName]; ok {
if _, err := strconv.Atoi(value); err != nil {
stderrLogger.Printf("bad label value (%s: %s) encountered for extended resource: %s", extendedResourceName, value, err.Error())
continue // non-numeric label can't be used
}
extendedResources[extendedResourceName] = labels[extendedResourceName]
delete(labels, extendedResourceName)
extendedResources[extendedResourceName] = value
delete(outLabels, extendedResourceName)
}
}
return labels, extendedResources
return outLabels, extendedResources
}
// Implement LabelerServer
@ -352,23 +346,8 @@ func (s *labelerServer) SetLabels(c context.Context, r *pb.SetLabelsRequest) (*p
labels, extendedResources := filterFeatureLabels(r.Labels, s.args.ExtraLabelNs, s.args.LabelWhiteList, s.args.ResourceLabels)
if !s.args.NoPublish {
// Advertise NFD worker version, label names and extended resources as annotations
labelKeys := make([]string, 0, len(labels))
for k := range labels {
labelKeys = append(labelKeys, k)
}
sort.Strings(labelKeys)
extendedResourceKeys := make([]string, 0, len(extendedResources))
for key := range extendedResources {
extendedResourceKeys = append(extendedResourceKeys, key)
}
sort.Strings(extendedResourceKeys)
annotations := Annotations{"worker.version": r.NfdVersion,
"feature-labels": strings.Join(labelKeys, ","),
"extended-resources": strings.Join(extendedResourceKeys, ","),
}
// Advertise NFD worker version as an annotation
annotations := Annotations{workerVersionAnnotation: r.NfdVersion}
err := updateNodeFeatures(s.apiHelper, r.NodeName, labels, annotations, extendedResources)
if err != nil {
@ -394,78 +373,106 @@ func updateNodeFeatures(helper apihelper.APIHelpers, nodeName string, labels Lab
return err
}
// Resolve publishable extended resources before node is modified
statusOps := getExtendedResourceOps(node, extendedResources)
// Remove old labels
if l, ok := node.Annotations[AnnotationNs+"feature-labels"]; ok {
oldLabels := strings.Split(l, ",")
removeLabels(node, oldLabels)
// Store names of labels in an annotation
labelKeys := make([]string, 0, len(labels))
for key := range labels {
// Drop the ns part for labels in the default ns
labelKeys = append(labelKeys, strings.TrimPrefix(key, LabelNs+"/"))
}
sort.Strings(labelKeys)
annotations[featureLabelAnnotation] = strings.Join(labelKeys, ",")
// Store names of extended resources in an annotation
extendedResourceKeys := make([]string, 0, len(extendedResources))
for key := range extendedResources {
// Drop the ns part if in the default ns
extendedResourceKeys = append(extendedResourceKeys, strings.TrimPrefix(key, LabelNs+"/"))
}
sort.Strings(extendedResourceKeys)
annotations[extendedResourceAnnotation] = strings.Join(extendedResourceKeys, ",")
// Create JSON patches for changes in labels and annotations
oldLabels := stringToNsNames(node.Annotations[featureLabelAnnotation], LabelNs)
patches := createPatches(oldLabels, node.Labels, labels, "/metadata/labels")
patches = append(patches, createPatches(nil, node.Annotations, annotations, "/metadata/annotations")...)
// Also, remove all labels with the old prefix, and the old version label
removeLabelsWithPrefix(node, "node.alpha.kubernetes-incubator.io/nfd")
removeLabelsWithPrefix(node, "node.alpha.kubernetes-incubator.io/node-feature-discovery")
patches = append(patches, removeLabelsWithPrefix(node, "node.alpha.kubernetes-incubator.io/nfd")...)
patches = append(patches, removeLabelsWithPrefix(node, "node.alpha.kubernetes-incubator.io/node-feature-discovery")...)
// Add labels to the node object.
addLabels(node, labels)
// Add annotations
addAnnotations(node, annotations)
// Send the updated node to the apiserver.
err = helper.UpdateNode(cli, node)
// Patch the node object in the apiserver
err = helper.PatchNode(cli, node.Name, patches)
if err != nil {
stderrLogger.Printf("can't update node: %s", err.Error())
stderrLogger.Printf("error while patching node object: %s", err.Error())
return err
}
// patch node status with extended resource changes
if len(statusOps) > 0 {
err = helper.PatchStatus(cli, node.Name, statusOps)
if err != nil {
stderrLogger.Printf("error while patching extended resources: %s", err.Error())
return err
}
patches = createExtendedResourcePatches(node, extendedResources)
err = helper.PatchNodeStatus(cli, node.Name, patches)
if err != nil {
stderrLogger.Printf("error while patching extended resources: %s", err.Error())
return err
}
return err
}
// Remove any labels having the given prefix
func removeLabelsWithPrefix(n *api.Node, search string) {
func removeLabelsWithPrefix(n *api.Node, search string) []apihelper.JsonPatch {
var p []apihelper.JsonPatch
for k := range n.Labels {
if strings.HasPrefix(k, search) {
delete(n.Labels, k)
p = append(p, apihelper.NewJsonPatch("remove", "/metadata/labels", k, ""))
}
}
return p
}
// Removes NFD labels from a Node object
func removeLabels(n *api.Node, labelNames []string) {
for _, l := range labelNames {
if strings.Contains(l, "/") {
delete(n.Labels, l)
// createPatches is a generic helper that returns json patch operations to perform
func createPatches(removeKeys []string, oldItems map[string]string, newItems map[string]string, jsonPath string) []apihelper.JsonPatch {
patches := []apihelper.JsonPatch{}
// Determine items to remove
for _, key := range removeKeys {
if _, ok := oldItems[key]; ok {
if _, ok := newItems[key]; !ok {
patches = append(patches, apihelper.NewJsonPatch("remove", jsonPath, key, ""))
}
}
}
// Determine items to add or replace
for key, newVal := range newItems {
if oldVal, ok := oldItems[key]; ok {
if newVal != oldVal {
patches = append(patches, apihelper.NewJsonPatch("replace", jsonPath, key, newVal))
}
} else {
delete(n.Labels, LabelNs+l)
patches = append(patches, apihelper.NewJsonPatch("add", jsonPath, key, newVal))
}
}
return patches
}
// getExtendedResourceOps returns a slice of operations to perform on the node status
func getExtendedResourceOps(n *api.Node, extendedResources ExtendedResources) []statusOp {
var statusOps []statusOp
// createExtendedResourcePatches returns a slice of operations to perform on
// the node status
func createExtendedResourcePatches(n *api.Node, extendedResources ExtendedResources) []apihelper.JsonPatch {
patches := []apihelper.JsonPatch{}
oldResources := strings.Split(n.Annotations[AnnotationNs+"extended-resources"], ",")
// Form a list of namespaced resource names managed by us
oldResources := stringToNsNames(n.Annotations[extendedResourceAnnotation], LabelNs)
// figure out which resources to remove
for _, resource := range oldResources {
if _, ok := n.Status.Capacity[api.ResourceName(addNs(resource, LabelNs))]; ok {
if _, ok := n.Status.Capacity[api.ResourceName(resource)]; ok {
// check if the ext resource is still needed
_, extResNeeded := extendedResources[resource]
if !extResNeeded {
statusOps = append(statusOps, createStatusOp("remove", resource, "capacity", ""))
statusOps = append(statusOps, createStatusOp("remove", resource, "allocatable", ""))
if _, extResNeeded := extendedResources[resource]; !extResNeeded {
patches = append(patches, apihelper.NewJsonPatch("remove", "/status/capacity", resource, ""))
patches = append(patches, apihelper.NewJsonPatch("remove", "/status/allocatable", resource, ""))
}
}
}
@ -473,37 +480,19 @@ func getExtendedResourceOps(n *api.Node, extendedResources ExtendedResources) []
// figure out which resources to replace and which to add
for resource, value := range extendedResources {
// check if the extended resource already exists with the same capacity in the node
if quantity, ok := n.Status.Capacity[api.ResourceName(addNs(resource, LabelNs))]; ok {
if quantity, ok := n.Status.Capacity[api.ResourceName(resource)]; ok {
val, _ := quantity.AsInt64()
if strconv.FormatInt(val, 10) != value {
statusOps = append(statusOps, createStatusOp("replace", resource, "capacity", value))
statusOps = append(statusOps, createStatusOp("replace", resource, "allocatable", value))
patches = append(patches, apihelper.NewJsonPatch("replace", "/status/capacity", resource, value))
patches = append(patches, apihelper.NewJsonPatch("replace", "/status/allocatable", resource, value))
}
} else {
statusOps = append(statusOps, createStatusOp("add", resource, "capacity", value))
patches = append(patches, apihelper.NewJsonPatch("add", "/status/capacity", resource, value))
// "allocatable" gets added implicitly after adding to capacity
}
}
return statusOps
}
// Add NFD labels to a Node object.
func addLabels(n *api.Node, labels map[string]string) {
for k, v := range labels {
if strings.Contains(k, "/") {
n.Labels[k] = v
} else {
n.Labels[LabelNs+k] = v
}
}
}
// Add Annotations to a Node object
func addAnnotations(n *api.Node, annotations map[string]string) {
for k, v := range annotations {
n.Annotations[AnnotationNs+k] = v
}
return patches
}
// addNs adds a namespace if one isn't already found from src string
@ -511,5 +500,28 @@ func addNs(src string, nsToAdd string) string {
if strings.Contains(src, "/") {
return src
}
return nsToAdd + src
return filepath.Join(nsToAdd, src)
}
// splitNs splits a name into its namespace and name parts
func splitNs(fullname string) (string, string) {
split := strings.SplitN(fullname, "/", 2)
if len(split) == 2 {
return split[0], split[1]
}
return "", fullname
}
// stringToNsNames is a helper for converting a string of comma-separated names
// into a slice of fully namespaced names
func stringToNsNames(cslist, ns string) []string {
var names []string
if cslist != "" {
names = strings.Split(cslist, ",")
for i, name := range names {
// Expect that names may omit the ns part
names[i] = addNs(name, ns)
}
}
return names
}

View file

@ -458,9 +458,9 @@ var _ = framework.KubeDescribe("[NFD] Node Feature Discovery", func() {
ginkgo.It("it should decorate the node with the fake feature labels", func() {
fakeFeatureLabels := map[string]string{
master.LabelNs + "fake-fakefeature1": "true",
master.LabelNs + "fake-fakefeature2": "true",
master.LabelNs + "fake-fakefeature3": "true",
master.LabelNs + "/fake-fakefeature1": "true",
master.LabelNs + "/fake-fakefeature2": "true",
master.LabelNs + "/fake-fakefeature3": "true",
}
// Remove pre-existing stale annotations and labels