mirror of
https://github.com/kubernetes-sigs/node-feature-discovery.git
synced 2024-12-14 11:57:51 +00:00
Create a Validate pkg
Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
This commit is contained in:
parent
bdfef6df18
commit
affb93ea50
7 changed files with 505 additions and 96 deletions
|
@ -59,8 +59,6 @@ namespaces, excluding `kubernetes.io` namespace and its sub-namespaces
|
||||||
`kubernetes.io` and its sub-namespaces are always denied.
|
`kubernetes.io` and its sub-namespaces are always denied.
|
||||||
This option can be used to exclude some vendors or application specific
|
This option can be used to exclude some vendors or application specific
|
||||||
namespaces.
|
namespaces.
|
||||||
Note that the namespaces `feature.node.kubernetes.io` and `profile.node.kubernetes.io`
|
|
||||||
and their sub-namespaces are always allowed and cannot be denied.
|
|
||||||
|
|
||||||
Default: *empty*
|
Default: *empty*
|
||||||
|
|
||||||
|
|
|
@ -475,9 +475,6 @@ The namespace part (i.e. prefix) of the labels is controlled by nfd:
|
||||||
[`-extra-label-ns`](../reference/master-commandline-reference.md#-extra-label-ns)
|
[`-extra-label-ns`](../reference/master-commandline-reference.md#-extra-label-ns)
|
||||||
command line flag of nfd-master.
|
command line flag of nfd-master.
|
||||||
e.g: `nfd-master -deny-label-ns="*" -extra-label-ns=example.com`
|
e.g: `nfd-master -deny-label-ns="*" -extra-label-ns=example.com`
|
||||||
- Built-in default namespaces `feature.node.kubernetes.io` and
|
|
||||||
`profile.node.kubernetes.io` (and their sub-namespaces) are always allowed
|
|
||||||
and cannot be denied.
|
|
||||||
|
|
||||||
## Feature rule format
|
## Feature rule format
|
||||||
|
|
||||||
|
|
166
pkg/apis/nfd/validate/validate.go
Normal file
166
pkg/apis/nfd/validate/validate.go
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
/*
|
||||||
|
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 validate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
k8sQuantity "k8s.io/apimachinery/pkg/api/resource"
|
||||||
|
k8svalidation "k8s.io/apimachinery/pkg/util/validation"
|
||||||
|
|
||||||
|
nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Default error message for invalid label/annotation keys
|
||||||
|
ErrNSNotAllowed = fmt.Errorf("namespace is not allowed")
|
||||||
|
// Default error message for invalid label/annotation keys
|
||||||
|
ErrUnprefixedKeysNotAllowed = fmt.Errorf("unprefixed keys are not allowed")
|
||||||
|
// Default error for invalid taint effect
|
||||||
|
ErrInvalidTaintEffect = fmt.Errorf("invalid taint effect")
|
||||||
|
// Default error for empty taint effect
|
||||||
|
ErrEmptyTaintEffect = fmt.Errorf("empty taint effect")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Label validates a label key and value and returns an error if the key or
|
||||||
|
// value is invalid.
|
||||||
|
func Label(key, value string) error {
|
||||||
|
//Validate label key and value
|
||||||
|
if err := k8svalidation.IsQualifiedName(key); len(err) > 0 {
|
||||||
|
return fmt.Errorf("invalid label key %q: %s", key, strings.Join(err, "; "))
|
||||||
|
}
|
||||||
|
// Check label namespace, filter out if ns is not whitelisted
|
||||||
|
ns, _ := splitNs(key)
|
||||||
|
// And is not empty
|
||||||
|
if ns == "" {
|
||||||
|
return ErrUnprefixedKeysNotAllowed
|
||||||
|
}
|
||||||
|
// And is not a denied namespace
|
||||||
|
if ns == "kubernetes.io" || strings.HasSuffix(ns, ".kubernetes.io") {
|
||||||
|
// And is not a default namespace
|
||||||
|
if ns != nfdv1alpha1.FeatureLabelNs && ns != nfdv1alpha1.ProfileLabelNs &&
|
||||||
|
!strings.HasSuffix(ns, nfdv1alpha1.FeatureLabelSubNsSuffix) && !strings.HasSuffix(ns, nfdv1alpha1.ProfileLabelSubNsSuffix) {
|
||||||
|
return ErrNSNotAllowed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate label value
|
||||||
|
if err := k8svalidation.IsValidLabelValue(value); len(err) > 0 {
|
||||||
|
return fmt.Errorf("invalid labelvalue %q: %s", value, strings.Join(err, "; "))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Annotation validates an annotation key and value and returns an error if the
|
||||||
|
// key or value is invalid.
|
||||||
|
func Annotation(key, value string) error {
|
||||||
|
// Validate the annotation key
|
||||||
|
if err := k8svalidation.IsQualifiedName(key); len(err) > 0 {
|
||||||
|
return fmt.Errorf("invalid annotation key %q: %s", key, strings.Join(err, "; "))
|
||||||
|
}
|
||||||
|
|
||||||
|
ns, _ := splitNs(key)
|
||||||
|
// And is not empty
|
||||||
|
if ns == "" {
|
||||||
|
return ErrUnprefixedKeysNotAllowed
|
||||||
|
}
|
||||||
|
// And is not a denied namespace
|
||||||
|
if ns == "kubernetes.io" || strings.HasSuffix(ns, ".kubernetes.io") {
|
||||||
|
// And is not a default namespace
|
||||||
|
if ns != nfdv1alpha1.FeatureAnnotationNs && !strings.HasSuffix(ns, nfdv1alpha1.FeatureAnnotationSubNsSuffix) {
|
||||||
|
return ErrNSNotAllowed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate annotation value
|
||||||
|
if errs := k8svalidation.IsValidLabelValue(value); len(errs) > 0 {
|
||||||
|
return fmt.Errorf("invalid annotation value %q: %s", value, strings.Join(errs, "; "))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Taint validates a taint key and value and returns an error if the key or
|
||||||
|
// value is invalid.
|
||||||
|
func Taint(taint *corev1.Taint) error {
|
||||||
|
ns, _ := splitNs(taint.Key)
|
||||||
|
// And is not empty
|
||||||
|
if ns == "" {
|
||||||
|
return ErrUnprefixedKeysNotAllowed
|
||||||
|
}
|
||||||
|
// And is not a denied namespace
|
||||||
|
if ns == "kubernetes.io" || strings.HasSuffix(ns, ".kubernetes.io") {
|
||||||
|
// And is not a default namespace
|
||||||
|
if ns != nfdv1alpha1.TaintNs && !strings.HasSuffix(ns, nfdv1alpha1.TaintSubNsSuffix) {
|
||||||
|
return ErrNSNotAllowed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate taint effect is not empty
|
||||||
|
if taint.Effect == "" {
|
||||||
|
return ErrEmptyTaintEffect
|
||||||
|
}
|
||||||
|
// Validate effect to be only one of NoSchedule, PreferNoSchedule or NoExecute
|
||||||
|
if taint.Effect != corev1.TaintEffectNoSchedule &&
|
||||||
|
taint.Effect != corev1.TaintEffectPreferNoSchedule &&
|
||||||
|
taint.Effect != corev1.TaintEffectNoExecute {
|
||||||
|
return ErrInvalidTaintEffect
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtendedResource validates an extended resource key and value and returns an
|
||||||
|
// error if the key or value is invalid.
|
||||||
|
func ExtendedResource(key, value string) error {
|
||||||
|
//Validate extendedResource name
|
||||||
|
if errs := k8svalidation.IsQualifiedName(key); len(errs) > 0 {
|
||||||
|
return fmt.Errorf("invalid name %q: %s", key, strings.Join(errs, "; "))
|
||||||
|
}
|
||||||
|
ns, _ := splitNs(key)
|
||||||
|
// And is not empty
|
||||||
|
if ns == "" {
|
||||||
|
return ErrUnprefixedKeysNotAllowed
|
||||||
|
}
|
||||||
|
// And is not a denied namespace
|
||||||
|
if ns == "kubernetes.io" || strings.HasSuffix(ns, ".kubernetes.io") {
|
||||||
|
// And is not a default namespace
|
||||||
|
if ns != nfdv1alpha1.ExtendedResourceNs && !strings.HasSuffix(ns, nfdv1alpha1.ExtendedResourceSubNsSuffix) {
|
||||||
|
return ErrNSNotAllowed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Static Value (Pre-Defined at the NodeFeatureRule)
|
||||||
|
_, err := k8sQuantity.ParseQuantity(value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid value %s (from %s): %w", value, value, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
236
pkg/apis/nfd/validate/validate_test.go
Normal file
236
pkg/apis/nfd/validate/validate_test.go
Normal file
|
@ -0,0 +1,236 @@
|
||||||
|
package validate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAnnotation(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
key string
|
||||||
|
value string
|
||||||
|
want error
|
||||||
|
fail bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid annotation",
|
||||||
|
key: "feature.node.kubernetes.io/feature",
|
||||||
|
value: "true",
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid annotation key",
|
||||||
|
key: "invalid-key",
|
||||||
|
value: "true",
|
||||||
|
want: ErrUnprefixedKeysNotAllowed,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid annotation value",
|
||||||
|
key: "feature.node.kubernetes.io/feature",
|
||||||
|
value: "invalid value",
|
||||||
|
want: fmt.Errorf("invalid value \"invalid value\": value must be a valid label value"),
|
||||||
|
fail: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Denied annotation key",
|
||||||
|
key: "kubernetes.io/denied",
|
||||||
|
value: "true",
|
||||||
|
want: ErrNSNotAllowed,
|
||||||
|
fail: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := Annotation(tt.key, tt.value)
|
||||||
|
if got != tt.want {
|
||||||
|
if tt.fail {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Errorf("Annotation() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTaint(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
taint *corev1.Taint
|
||||||
|
want error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid taint",
|
||||||
|
taint: &corev1.Taint{
|
||||||
|
Key: "feature.node.kubernetes.io/taint",
|
||||||
|
Value: "true",
|
||||||
|
Effect: corev1.TaintEffectNoSchedule,
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UNPREFIXED taint key",
|
||||||
|
taint: &corev1.Taint{
|
||||||
|
Key: "invalid-key",
|
||||||
|
Value: "true",
|
||||||
|
Effect: corev1.TaintEffectNoSchedule,
|
||||||
|
},
|
||||||
|
want: ErrUnprefixedKeysNotAllowed,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid taint key",
|
||||||
|
taint: &corev1.Taint{
|
||||||
|
Key: "invalid.kubernetes.io/invalid-key",
|
||||||
|
Value: "true",
|
||||||
|
Effect: corev1.TaintEffectNoSchedule,
|
||||||
|
},
|
||||||
|
want: ErrNSNotAllowed,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty taint effect",
|
||||||
|
taint: &corev1.Taint{
|
||||||
|
Key: "feature.node.kubernetes.io/taint",
|
||||||
|
Value: "true",
|
||||||
|
Effect: "",
|
||||||
|
},
|
||||||
|
want: ErrEmptyTaintEffect,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid taint effect",
|
||||||
|
taint: &corev1.Taint{
|
||||||
|
Key: "feature.node.kubernetes.io/taint",
|
||||||
|
Value: "true",
|
||||||
|
Effect: "invalid-effect",
|
||||||
|
},
|
||||||
|
want: ErrInvalidTaintEffect,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := Taint(tt.taint)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("Taint() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLabel(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
key string
|
||||||
|
value string
|
||||||
|
want error
|
||||||
|
fail bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid label",
|
||||||
|
key: "feature.node.kubernetes.io/label",
|
||||||
|
value: "true",
|
||||||
|
want: nil,
|
||||||
|
fail: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid vendor label",
|
||||||
|
key: "vendor.io/label",
|
||||||
|
value: "true",
|
||||||
|
want: nil,
|
||||||
|
fail: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Denied label with prefix",
|
||||||
|
key: "kubernetes.io/label",
|
||||||
|
value: "true",
|
||||||
|
want: ErrNSNotAllowed,
|
||||||
|
fail: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid label key",
|
||||||
|
key: "invalid-key",
|
||||||
|
value: "true",
|
||||||
|
want: ErrNSNotAllowed,
|
||||||
|
fail: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid label value",
|
||||||
|
key: "feature.node.kubernetes.io/label",
|
||||||
|
value: "invalid value",
|
||||||
|
want: fmt.Errorf("invalid value \"invalid value\": value must be a valid label value"),
|
||||||
|
fail: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid value label",
|
||||||
|
key: "feature.node.kubernetes.io/label",
|
||||||
|
value: "true",
|
||||||
|
want: nil,
|
||||||
|
fail: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := Label(tt.key, tt.value)
|
||||||
|
if err != tt.want {
|
||||||
|
if tt.fail {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Errorf("Label() = %v, want %v", err, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtendedResource(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
key string
|
||||||
|
value string
|
||||||
|
want error
|
||||||
|
fail bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid extended resource",
|
||||||
|
key: "feature.node.kubernetes.io/extended-resource",
|
||||||
|
value: "123",
|
||||||
|
want: nil,
|
||||||
|
fail: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid extended resource key",
|
||||||
|
key: "invalid-key",
|
||||||
|
value: "123",
|
||||||
|
want: ErrNSNotAllowed,
|
||||||
|
fail: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid extended resource value",
|
||||||
|
key: "feature.node.kubernetes.io/extended-resource",
|
||||||
|
value: "invalid value",
|
||||||
|
want: fmt.Errorf("invalid value \"invalid value\": value must be a valid label value"),
|
||||||
|
fail: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Denied extended resource key",
|
||||||
|
key: "kubernetes.io/extended-resource",
|
||||||
|
value: "123",
|
||||||
|
want: ErrNSNotAllowed,
|
||||||
|
fail: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := ExtendedResource(tt.key, tt.value)
|
||||||
|
if err != tt.want {
|
||||||
|
if tt.fail {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Errorf("ExtendedResource() = %v, want %v", err, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -382,6 +382,7 @@ func TestSetLabels(t *testing.T) {
|
||||||
expectedStatusPatches := []apihelper.JsonPatch{}
|
expectedStatusPatches := []apihelper.JsonPatch{}
|
||||||
|
|
||||||
Convey("When node update succeeds", func() {
|
Convey("When node update succeeds", func() {
|
||||||
|
mockMaster.config.ExtraLabelNs = map[string]struct{}{"example.io": {}}
|
||||||
expectedPatches := []apihelper.JsonPatch{
|
expectedPatches := []apihelper.JsonPatch{
|
||||||
apihelper.NewJsonPatch("add", "/metadata/annotations", nfdv1alpha1.FeatureLabelsAnnotation, strings.Join(mockLabelNames, ",")),
|
apihelper.NewJsonPatch("add", "/metadata/annotations", nfdv1alpha1.FeatureLabelsAnnotation, strings.Join(mockLabelNames, ",")),
|
||||||
}
|
}
|
||||||
|
@ -400,6 +401,7 @@ func TestSetLabels(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("When -label-whitelist is specified", func() {
|
Convey("When -label-whitelist is specified", func() {
|
||||||
|
mockMaster.config.ExtraLabelNs = map[string]struct{}{"example.io": {}}
|
||||||
expectedPatches := []apihelper.JsonPatch{
|
expectedPatches := []apihelper.JsonPatch{
|
||||||
apihelper.NewJsonPatch("add", "/metadata/annotations", nfdv1alpha1.FeatureLabelsAnnotation, "example.io/feature-2"),
|
apihelper.NewJsonPatch("add", "/metadata/annotations", nfdv1alpha1.FeatureLabelsAnnotation, "example.io/feature-2"),
|
||||||
apihelper.NewJsonPatch("add", "/metadata/labels", "example.io/feature-2", mockLabels["example.io/feature-2"]),
|
apihelper.NewJsonPatch("add", "/metadata/labels", "example.io/feature-2", mockLabels["example.io/feature-2"]),
|
||||||
|
@ -443,7 +445,6 @@ func TestSetLabels(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
mockMaster.deniedNs.normal = map[string]struct{}{"random.denied.ns": {}}
|
mockMaster.deniedNs.normal = map[string]struct{}{"random.denied.ns": {}}
|
||||||
mockMaster.deniedNs.wildcard = map[string]struct{}{"kubernetes.io": {}}
|
|
||||||
mockMaster.config.ExtraLabelNs = map[string]struct{}{"valid.ns": {}}
|
mockMaster.config.ExtraLabelNs = map[string]struct{}{"valid.ns": {}}
|
||||||
mockMaster.args.Instance = instance
|
mockMaster.args.Instance = instance
|
||||||
mockHelper.On("GetClient").Return(mockClient, nil)
|
mockHelper.On("GetClient").Return(mockClient, nil)
|
||||||
|
@ -459,10 +460,11 @@ func TestSetLabels(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("When -resource-labels is specified", func() {
|
Convey("When -resource-labels is specified", func() {
|
||||||
|
mockMaster.config.ExtraLabelNs = map[string]struct{}{"example.io": {}}
|
||||||
expectedPatches := []apihelper.JsonPatch{
|
expectedPatches := []apihelper.JsonPatch{
|
||||||
apihelper.NewJsonPatch("add", "/metadata/annotations", nfdv1alpha1.FeatureLabelsAnnotation, "example.io/feature-2"),
|
apihelper.NewJsonPatch("add", "/metadata/annotations", nfdv1alpha1.FeatureLabelsAnnotation, "example.io/feature-2"),
|
||||||
apihelper.NewJsonPatch("add", "/metadata/annotations", nfdv1alpha1.ExtendedResourceAnnotation, "feature-1,feature-3"),
|
|
||||||
apihelper.NewJsonPatch("add", "/metadata/labels", "example.io/feature-2", mockLabels["example.io/feature-2"]),
|
apihelper.NewJsonPatch("add", "/metadata/labels", "example.io/feature-2", mockLabels["example.io/feature-2"]),
|
||||||
|
apihelper.NewJsonPatch("add", "/metadata/annotations", nfdv1alpha1.ExtendedResourceAnnotation, "feature-1,feature-3"),
|
||||||
}
|
}
|
||||||
expectedStatusPatches := []apihelper.JsonPatch{
|
expectedStatusPatches := []apihelper.JsonPatch{
|
||||||
apihelper.NewJsonPatch("add", "/status/capacity", "feature.node.kubernetes.io/feature-1", mockLabels["feature.node.kubernetes.io/feature-1"]),
|
apihelper.NewJsonPatch("add", "/status/capacity", "feature.node.kubernetes.io/feature-1", mockLabels["feature.node.kubernetes.io/feature-1"]),
|
||||||
|
@ -502,6 +504,7 @@ func TestSetLabels(t *testing.T) {
|
||||||
func TestFilterLabels(t *testing.T) {
|
func TestFilterLabels(t *testing.T) {
|
||||||
mockHelper := &apihelper.MockAPIHelpers{}
|
mockHelper := &apihelper.MockAPIHelpers{}
|
||||||
mockMaster := newMockMaster(mockHelper)
|
mockMaster := newMockMaster(mockHelper)
|
||||||
|
mockMaster.config.ExtraLabelNs = map[string]struct{}{"example.io": {}}
|
||||||
mockMaster.deniedNs = deniedNs{
|
mockMaster.deniedNs = deniedNs{
|
||||||
normal: map[string]struct{}{"": struct{}{}, "kubernetes.io": struct{}{}, "denied.ns": struct{}{}},
|
normal: map[string]struct{}{"": struct{}{}, "kubernetes.io": struct{}{}, "denied.ns": struct{}{}},
|
||||||
wildcard: map[string]struct{}{".kubernetes.io": struct{}{}, ".denied.subns": struct{}{}},
|
wildcard: map[string]struct{}{".kubernetes.io": struct{}{}, ".denied.subns": struct{}{}},
|
||||||
|
@ -940,3 +943,62 @@ func removeLabelsWithPrefix(n *corev1.Node, search string) []apihelper.JsonPatch
|
||||||
|
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetDynamicValue(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
value string
|
||||||
|
features *nfdv1alpha1.Features
|
||||||
|
want string
|
||||||
|
fail bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid dynamic value",
|
||||||
|
value: "@test.feature.LSM",
|
||||||
|
features: &nfdv1alpha1.Features{
|
||||||
|
Attributes: map[string]nfdv1alpha1.AttributeFeatureSet{
|
||||||
|
"test.feature": nfdv1alpha1.AttributeFeatureSet{
|
||||||
|
Elements: map[string]string{
|
||||||
|
"LSM": "123",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: "123",
|
||||||
|
fail: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid feature name",
|
||||||
|
value: "@invalid",
|
||||||
|
features: &nfdv1alpha1.Features{},
|
||||||
|
want: "",
|
||||||
|
fail: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Element not found",
|
||||||
|
value: "@test.feature.LSM",
|
||||||
|
features: &nfdv1alpha1.Features{},
|
||||||
|
want: "",
|
||||||
|
fail: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid dynamic value",
|
||||||
|
value: "@test.feature.LSM",
|
||||||
|
features: &nfdv1alpha1.Features{},
|
||||||
|
want: "",
|
||||||
|
fail: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := getDynamicValue(tt.value, tt.features)
|
||||||
|
if err != nil && !tt.fail {
|
||||||
|
t.Errorf("getDynamicValue() = %v, want %v", err, tt.want)
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("getDynamicValue() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -38,10 +38,8 @@ 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"
|
||||||
k8sQuantity "k8s.io/apimachinery/pkg/api/resource"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
k8sLabels "k8s.io/apimachinery/pkg/labels"
|
k8sLabels "k8s.io/apimachinery/pkg/labels"
|
||||||
k8svalidation "k8s.io/apimachinery/pkg/util/validation"
|
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
restclient "k8s.io/client-go/rest"
|
restclient "k8s.io/client-go/rest"
|
||||||
"k8s.io/client-go/tools/leaderelection"
|
"k8s.io/client-go/tools/leaderelection"
|
||||||
|
@ -55,6 +53,7 @@ import (
|
||||||
|
|
||||||
"sigs.k8s.io/node-feature-discovery/pkg/apihelper"
|
"sigs.k8s.io/node-feature-discovery/pkg/apihelper"
|
||||||
nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
|
nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
|
||||||
|
"sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/validate"
|
||||||
pb "sigs.k8s.io/node-feature-discovery/pkg/labeler"
|
pb "sigs.k8s.io/node-feature-discovery/pkg/labeler"
|
||||||
"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"
|
||||||
|
@ -553,24 +552,27 @@ func (m *nfdMaster) filterFeatureLabels(labels Labels, features *nfdv1alpha1.Fea
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *nfdMaster) filterFeatureLabel(name, value string, features *nfdv1alpha1.Features) (string, error) {
|
func (m *nfdMaster) filterFeatureLabel(name, value string, features *nfdv1alpha1.Features) (string, error) {
|
||||||
//Validate label name
|
// Check if Value is dynamic
|
||||||
if errs := k8svalidation.IsQualifiedName(name); len(errs) > 0 {
|
var filteredValue string
|
||||||
return "", fmt.Errorf("invalid name %q: %s", name, strings.Join(errs, "; "))
|
if strings.HasPrefix(value, "@") {
|
||||||
|
dynamicValue, err := getDynamicValue(value, features)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
filteredValue = dynamicValue
|
||||||
|
} else {
|
||||||
|
filteredValue = value
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check label namespace, filter out if ns is not whitelisted
|
// Validate
|
||||||
ns, base := splitNs(name)
|
ns, base := splitNs(name)
|
||||||
if ns == "" {
|
err := validate.Label(name, filteredValue)
|
||||||
return "", fmt.Errorf("labels without namespace (prefix/) not allowed")
|
if err == validate.ErrNSNotAllowed || isNamespaceDenied(ns, m.deniedNs.wildcard, m.deniedNs.normal) {
|
||||||
}
|
if _, ok := m.config.ExtraLabelNs[ns]; !ok {
|
||||||
if ns != nfdv1alpha1.FeatureLabelNs && ns != nfdv1alpha1.ProfileLabelNs &&
|
return "", fmt.Errorf("namespace %q is not allowed", ns)
|
||||||
!strings.HasSuffix(ns, nfdv1alpha1.FeatureLabelSubNsSuffix) && !strings.HasSuffix(ns, nfdv1alpha1.ProfileLabelSubNsSuffix) {
|
|
||||||
// If the namespace is denied, and not present in the extraLabelNs, label will be ignored
|
|
||||||
if isNamespaceDenied(ns, m.deniedNs.wildcard, m.deniedNs.normal) {
|
|
||||||
if _, ok := m.config.ExtraLabelNs[ns]; !ok {
|
|
||||||
return "", fmt.Errorf("namespace %q is not allowed", ns)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip if label doesn't match labelWhiteList
|
// Skip if label doesn't match labelWhiteList
|
||||||
|
@ -578,24 +580,7 @@ func (m *nfdMaster) filterFeatureLabel(name, value string, features *nfdv1alpha1
|
||||||
return "", fmt.Errorf("%s (%s) does not match the whitelist (%s)", base, name, m.config.LabelWhiteList.Regexp.String())
|
return "", fmt.Errorf("%s (%s) does not match the whitelist (%s)", base, name, m.config.LabelWhiteList.Regexp.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
var filteredLabel string
|
return filteredValue, nil
|
||||||
|
|
||||||
// Dynamic Value
|
|
||||||
if strings.HasPrefix(value, "@") {
|
|
||||||
dynamicValue, err := getDynamicValue(value, features)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
filteredLabel = dynamicValue
|
|
||||||
} else {
|
|
||||||
filteredLabel = value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate the label value
|
|
||||||
if errs := k8svalidation.IsValidLabelValue(filteredLabel); len(errs) > 0 {
|
|
||||||
return "", fmt.Errorf("invalid value %q: %s", filteredLabel, strings.Join(errs, "; "))
|
|
||||||
}
|
|
||||||
return filteredLabel, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDynamicValue(value string, features *nfdv1alpha1.Features) (string, error) {
|
func getDynamicValue(value string, features *nfdv1alpha1.Features) (string, error) {
|
||||||
|
@ -621,7 +606,7 @@ func filterTaints(taints []corev1.Taint) []corev1.Taint {
|
||||||
outTaints := []corev1.Taint{}
|
outTaints := []corev1.Taint{}
|
||||||
|
|
||||||
for _, taint := range taints {
|
for _, taint := range taints {
|
||||||
if err := filterTaint(&taint); err != nil {
|
if err := validate.Taint(&taint); err != nil {
|
||||||
klog.ErrorS(err, "ignoring taint", "taint", taint)
|
klog.ErrorS(err, "ignoring taint", "taint", taint)
|
||||||
nodeTaintsRejected.Inc()
|
nodeTaintsRejected.Inc()
|
||||||
} else {
|
} else {
|
||||||
|
@ -631,19 +616,6 @@ func filterTaints(taints []corev1.Taint) []corev1.Taint {
|
||||||
return outTaints
|
return outTaints
|
||||||
}
|
}
|
||||||
|
|
||||||
func filterTaint(taint *corev1.Taint) error {
|
|
||||||
// Check prefix of the key, filter out disallowed ones
|
|
||||||
ns, _ := splitNs(taint.Key)
|
|
||||||
if ns == "" {
|
|
||||||
return fmt.Errorf("taint keys without namespace (prefix/) are not allowed")
|
|
||||||
}
|
|
||||||
if ns != nfdv1alpha1.TaintNs && !strings.HasSuffix(ns, nfdv1alpha1.TaintSubNsSuffix) &&
|
|
||||||
(ns == "kubernetes.io" || strings.HasSuffix(ns, ".kubernetes.io")) {
|
|
||||||
return fmt.Errorf("prefix %q is not allowed for taint key", ns)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func verifyNodeName(cert *x509.Certificate, nodeName string) error {
|
func verifyNodeName(cert *x509.Certificate, nodeName string) error {
|
||||||
if cert.Subject.CommonName == nodeName {
|
if cert.Subject.CommonName == nodeName {
|
||||||
return nil
|
return nil
|
||||||
|
@ -809,35 +781,25 @@ func (m *nfdMaster) filterExtendedResources(features *nfdv1alpha1.Features, exte
|
||||||
}
|
}
|
||||||
|
|
||||||
func filterExtendedResource(name, value string, features *nfdv1alpha1.Features) (string, error) {
|
func filterExtendedResource(name, value string, features *nfdv1alpha1.Features) (string, error) {
|
||||||
// Check if given NS is allowed
|
// Dynamic Value
|
||||||
ns, _ := splitNs(name)
|
var filteredValue string
|
||||||
if ns == "" {
|
if strings.HasPrefix(value, "@") {
|
||||||
return "", fmt.Errorf("extended resource without namespace (prefix/) not allowed")
|
dynamicValue, err := getDynamicValue(value, features)
|
||||||
}
|
if err != nil {
|
||||||
if ns != nfdv1alpha1.ExtendedResourceNs && !strings.HasSuffix(ns, nfdv1alpha1.ExtendedResourceSubNsSuffix) {
|
return "", err
|
||||||
if ns == "kubernetes.io" || strings.HasSuffix(ns, ".kubernetes.io") {
|
|
||||||
return "", fmt.Errorf("namespace %q is not allowed", ns)
|
|
||||||
}
|
}
|
||||||
|
filteredValue = dynamicValue
|
||||||
|
} else {
|
||||||
|
filteredValue = value
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dynamic Value
|
// Validate
|
||||||
if strings.HasPrefix(value, "@") {
|
err := validate.ExtendedResource(name, filteredValue)
|
||||||
if element, err := getDynamicValue(value, features); err != nil {
|
|
||||||
return "", err
|
|
||||||
} else {
|
|
||||||
q, err := k8sQuantity.ParseQuantity(element)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("invalid value %s (from %s): %w", element, value, err)
|
|
||||||
}
|
|
||||||
return q.String(), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Static Value (Pre-Defined at the NodeFeatureRule)
|
|
||||||
q, err := k8sQuantity.ParseQuantity(value)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("invalid value %s: %w", value, err)
|
return "", err
|
||||||
}
|
}
|
||||||
return q.String(), nil
|
|
||||||
|
return filteredValue, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *nfdMaster) refreshNodeFeatures(cli *kubernetes.Clientset, nodeName string, labels map[string]string, features *nfdv1alpha1.Features) error {
|
func (m *nfdMaster) refreshNodeFeatures(cli *kubernetes.Clientset, nodeName string, labels map[string]string, features *nfdv1alpha1.Features) error {
|
||||||
|
@ -1331,6 +1293,7 @@ func addNs(src string, nsToAdd string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// splitNs splits a name into its namespace and name parts
|
// splitNs splits a name into its namespace and name parts
|
||||||
|
// Ported to Validate
|
||||||
func splitNs(fullname string) (string, string) {
|
func splitNs(fullname string) (string, string) {
|
||||||
split := strings.SplitN(fullname, "/", 2)
|
split := strings.SplitN(fullname, "/", 2)
|
||||||
if len(split) == 2 {
|
if len(split) == 2 {
|
||||||
|
@ -1441,22 +1404,11 @@ func (m *nfdMaster) filterFeatureAnnotations(annotations map[string]string) map[
|
||||||
outAnnotations := make(map[string]string)
|
outAnnotations := make(map[string]string)
|
||||||
|
|
||||||
for annotation, value := range annotations {
|
for annotation, value := range annotations {
|
||||||
ns, _ := splitNs(annotation)
|
|
||||||
if ns == "" {
|
|
||||||
klog.ErrorS(fmt.Errorf("annotations without namespace (prefix/) not allowed"), fmt.Sprintf("Ignoring annotation %s", annotation))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Check annotation namespace, filter out if ns is not whitelisted
|
// Check annotation namespace, filter out if ns is not whitelisted
|
||||||
if ns != nfdv1alpha1.FeatureAnnotationNs && !strings.HasSuffix(ns, nfdv1alpha1.FeatureAnnotationSubNsSuffix) {
|
err := validate.Annotation(annotation, value)
|
||||||
// If the namespace is denied the annotation will be ignored
|
if err != nil {
|
||||||
if ns == "" {
|
klog.ErrorS(err, "ignoring annotation", "annotationKey", annotation, "annotationValue", value)
|
||||||
klog.ErrorS(fmt.Errorf("labels without namespace (prefix/) not allowed"), fmt.Sprintf("Ignoring annotation %s", annotation))
|
continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
if ns == "kubernetes.io" || strings.HasSuffix(ns, ".kubernetes.io") || ns == nfdv1alpha1.AnnotationNs {
|
|
||||||
klog.ErrorS(fmt.Errorf("namespace %q is not allowed", ns), fmt.Sprintf("Ignoring annotation %s", annotation))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
outAnnotations[annotation] = value
|
outAnnotations[annotation] = value
|
||||||
|
|
|
@ -30,7 +30,5 @@ while true; do
|
||||||
i=$(( $i + 1 ))
|
i=$(( $i + 1 ))
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|
||||||
# Configure environment and run tests
|
# Configure environment and run tests
|
||||||
make e2e-test
|
make e2e-test
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue