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

Allow to change labels namespace

The aim here is to allow to override the default namespace
of NFD. The allowed namespaces are whitelisted.
See https://github.com/kubernetes-sigs/node-feature-discovery/issues/227

Signed-off-by: Jordan Jacobelli <jjacobelli@nvidia.com>
This commit is contained in:
Jordan Jacobelli 2019-04-05 15:31:40 -07:00
parent d09a7cb77c
commit 40918827f6
No known key found for this signature in database
GPG key ID: 8148DA8F04B91641
6 changed files with 123 additions and 47 deletions

View file

@ -53,27 +53,30 @@ nfd-master.
Usage: Usage:
nfd-master [--no-publish] [--label-whitelist=<pattern>] [--port=<port>] nfd-master [--no-publish] [--label-whitelist=<pattern>] [--port=<port>]
[--ca-file=<path>] [--cert-file=<path>] [--key-file=<path>] [--ca-file=<path>] [--cert-file=<path>] [--key-file=<path>]
[--verify-node-name] [--verify-node-name] [--extra-label-ns=<list>]
nfd-master -h | --help nfd-master -h | --help
nfd-master --version nfd-master --version
Options: Options:
-h --help Show this screen. -h --help Show this screen.
--version Output version and exit. --version Output version and exit.
--port=<port> Port on which to listen for connections. --port=<port> Port on which to listen for connections.
[Default: 8080] [Default: 8080]
--ca-file=<path> Root certificate for verifying connections --ca-file=<path> Root certificate for verifying connections
[Default: ] [Default: ]
--cert-file=<path> Certificate used for authenticating connections --cert-file=<path> Certificate used for authenticating connections
[Default: ] [Default: ]
--key-file=<path> Private key matching --cert-file --key-file=<path> Private key matching --cert-file
[Default: ] [Default: ]
--verify-node-name Verify worker node name against CN from the TLS --verify-node-name Verify worker node name against CN from the TLS
certificate. Only has effect when TLS authentication certificate. Only has effect when TLS authentication
has been enabled. has been enabled.
--no-publish Do not publish feature labels --no-publish Do not publish feature labels
--label-whitelist=<pattern> Regular expression to filter label names to --label-whitelist=<pattern> Regular expression to filter label names to
publish to the Kubernetes API server. [Default: ] publish to the Kubernetes API server. [Default: ]
--extra-label-ns=<list> Comma separated list of allowed extra label namespaces
[Default: ]
``` ```
### NFD-Worker ### NFD-Worker
@ -368,8 +371,13 @@ slash (`/`) it is used as the label name as is, without any additional prefix.
This makes it possible for the user to fully control the feature label names, This makes it possible for the user to fully control the feature label names,
e.g. for overriding labels created by other feature sources. e.g. for overriding labels created by other feature sources.
The value of the label is either `true` (for binary labels) or `<value>` You can also override the default namespace of your labels using this format:
(for non-binary labels). `<namespace>/<name>[=<value>]`. You must whitelist your namespace using the
`--extra-label-ns` option on the master. In this case, the name of the
file will not be added to the label name. For example, if you want to add the
label `my.namespace.org/my-label=value`, your hook output or file must contains
`my.namespace.org/my-label=value` and you must add
`--extra-label-ns=my.namespace.org` on the master command line.
`stderr` output of the hooks is propagated to NFD log so it can be used for `stderr` output of the hooks is propagated to NFD log so it can be used for
debugging and logging. debugging and logging.
@ -383,6 +391,7 @@ MY_FEATURE_1
MY_FEATURE_2=myvalue MY_FEATURE_2=myvalue
/override_source-OVERRIDE_BOOL /override_source-OVERRIDE_BOOL
/override_source-OVERRIDE_VALUE=123 /override_source-OVERRIDE_VALUE=123
override.namespace/value=456
``` ```
which, in turn, will translate into the following node labels: which, in turn, will translate into the following node labels:
``` ```
@ -390,6 +399,7 @@ feature.node.kubernetes.io/my-source-MY_FEATURE_1=true
feature.node.kubernetes.io/my-source-MY_FEATURE_2=myvalue feature.node.kubernetes.io/my-source-MY_FEATURE_2=myvalue
feature.node.kubernetes.io/override_source-OVERRIDE_BOOL=true feature.node.kubernetes.io/override_source-OVERRIDE_BOOL=true
feature.node.kubernetes.io/override_source-OVERRIDE_VALUE=123 feature.node.kubernetes.io/override_source-OVERRIDE_VALUE=123
override.namespace/value=456
``` ```
**A file example:**<br/> **A file example:**<br/>
@ -401,6 +411,7 @@ MY_FEATURE_1
MY_FEATURE_2=myvalue MY_FEATURE_2=myvalue
/override_source-OVERRIDE_BOOL /override_source-OVERRIDE_BOOL
/override_source-OVERRIDE_VALUE=123 /override_source-OVERRIDE_VALUE=123
override.namespace/value=456
``` ```
which, in turn, will translate into the following node labels: which, in turn, will translate into the following node labels:
``` ```
@ -408,6 +419,7 @@ feature.node.kubernetes.io/my-source-MY_FEATURE_1=true
feature.node.kubernetes.io/my-source-MY_FEATURE_2=myvalue feature.node.kubernetes.io/my-source-MY_FEATURE_2=myvalue
feature.node.kubernetes.io/override_source-OVERRIDE_BOOL=true feature.node.kubernetes.io/override_source-OVERRIDE_BOOL=true
feature.node.kubernetes.io/override_source-OVERRIDE_VALUE=123 feature.node.kubernetes.io/override_source-OVERRIDE_VALUE=123
override.namespace/value=456
``` ```
NFD tries to run any regular files found from the hooks directory. Any NFD tries to run any regular files found from the hooks directory. Any

View file

@ -21,6 +21,7 @@ import (
"log" "log"
"regexp" "regexp"
"strconv" "strconv"
"strings"
"github.com/docopt/docopt-go" "github.com/docopt/docopt-go"
master "sigs.k8s.io/node-feature-discovery/pkg/nfd-master" master "sigs.k8s.io/node-feature-discovery/pkg/nfd-master"
@ -64,27 +65,29 @@ func argsParse(argv []string) (master.Args, error) {
Usage: Usage:
%s [--no-publish] [--label-whitelist=<pattern>] [--port=<port>] %s [--no-publish] [--label-whitelist=<pattern>] [--port=<port>]
[--ca-file=<path>] [--cert-file=<path>] [--key-file=<path>] [--ca-file=<path>] [--cert-file=<path>] [--key-file=<path>]
[--verify-node-name] [--verify-node-name] [--extra-label-ns=<list>]
%s -h | --help %s -h | --help
%s --version %s --version
Options: Options:
-h --help Show this screen. -h --help Show this screen.
--version Output version and exit. --version Output version and exit.
--port=<port> Port on which to listen for connections. --port=<port> Port on which to listen for connections.
[Default: 8080] [Default: 8080]
--ca-file=<path> Root certificate for verifying connections --ca-file=<path> Root certificate for verifying connections
[Default: ] [Default: ]
--cert-file=<path> Certificate used for authenticating connections --cert-file=<path> Certificate used for authenticating connections
[Default: ] [Default: ]
--key-file=<path> Private key matching --cert-file --key-file=<path> Private key matching --cert-file
[Default: ] [Default: ]
--verify-node-name Verify worker node name against CN from the TLS --verify-node-name Verify worker node name against CN from the TLS
certificate. Only has effect when TLS authentication certificate. Only has effect when TLS authentication
has been enabled. has been enabled.
--no-publish Do not publish feature labels --no-publish Do not publish feature labels
--label-whitelist=<pattern> Regular expression to filter label names to --label-whitelist=<pattern> Regular expression to filter label names to
publish to the Kubernetes API server. [Default: ]`, publish to the Kubernetes API server. [Default: ]
--extra-label-ns=<list> Comma separated list of allowed extra label namespaces
[Default: ]`,
ProgramName, ProgramName,
ProgramName, ProgramName,
ProgramName, ProgramName,
@ -109,6 +112,7 @@ func argsParse(argv []string) (master.Args, error) {
return args, fmt.Errorf("error parsing whitelist regex (%s): %s", arguments["--label-whitelist"], err) return args, fmt.Errorf("error parsing whitelist regex (%s): %s", arguments["--label-whitelist"], err)
} }
args.VerifyNodeName = arguments["--verify-node-name"].(bool) args.VerifyNodeName = arguments["--verify-node-name"].(bool)
args.ExtraLabelNs = strings.Split(arguments["--extra-label-ns"].(string), ",")
return args, nil return args, nil
} }

View file

@ -237,6 +237,29 @@ func TestSetLabels(t *testing.T) {
}) })
}) })
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)
mockLabels := map[string]string{"feature-1": "val-1",
"valid.ns/feature-2": "val-2",
"invalid.ns/feature-3": "val-3"}
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"}
So(len(mockNode.Annotations), ShouldEqual, len(a))
So(mockNode.Annotations, ShouldResemble, a)
})
})
mockErr := errors.New("mock-error") mockErr := errors.New("mock-error")
Convey("When node update fails", func() { Convey("When node update fails", func() {
mockHelper.On("GetClient").Return(mockClient, mockErr) mockHelper.On("GetClient").Return(mockClient, mockErr)

View file

@ -64,6 +64,7 @@ type Annotations map[string]string
type Args struct { type Args struct {
CaFile string CaFile string
CertFile string CertFile string
ExtraLabelNs []string
KeyFile string KeyFile string
LabelWhiteList *regexp.Regexp LabelWhiteList *regexp.Regexp
NoPublish bool NoPublish bool
@ -202,13 +203,30 @@ func updateMasterNode(helper apihelper.APIHelpers) error {
return nil return nil
} }
// Filter labels if whitelist has been defined // Filter labels by namespace and name whitelist
func filterFeatureLabels(labels Labels, labelWhiteList *regexp.Regexp) Labels { func filterFeatureLabels(labels Labels, extraLabelNs []string, labelWhiteList *regexp.Regexp) Labels {
for name := range labels { for label := range labels {
split := strings.SplitN(label, "/", 2)
name := split[0]
// 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)
}
}
}
// Skip if label doesn't match labelWhiteList // Skip if label doesn't match labelWhiteList
if !labelWhiteList.MatchString(name) { if !labelWhiteList.MatchString(name) {
stderrLogger.Printf("%s does not match the whitelist (%s) and will not be published.", name, labelWhiteList.String()) stderrLogger.Printf("%s does not match the whitelist (%s) and will not be published.", name, labelWhiteList.String())
delete(labels, name) delete(labels, label)
} }
} }
return labels return labels
@ -247,7 +265,7 @@ func (s *labelerServer) SetLabels(c context.Context, r *pb.SetLabelsRequest) (*p
} }
stdoutLogger.Printf("REQUEST Node: %s NFD-version: %s Labels: %s", r.NodeName, r.NfdVersion, r.Labels) stdoutLogger.Printf("REQUEST Node: %s NFD-version: %s Labels: %s", r.NodeName, r.NfdVersion, r.Labels)
labels := filterFeatureLabels(r.Labels, s.args.LabelWhiteList) labels := filterFeatureLabels(r.Labels, s.args.ExtraLabelNs, s.args.LabelWhiteList)
if !s.args.NoPublish { if !s.args.NoPublish {
// Advertise NFD worker version and label names as annotations // Advertise NFD worker version and label names as annotations
@ -320,14 +338,22 @@ func removeLabelsWithPrefix(n *api.Node, search string) {
// Removes NFD labels from a Node object // Removes NFD labels from a Node object
func removeLabels(n *api.Node, labelNames []string) { func removeLabels(n *api.Node, labelNames []string) {
for _, l := range labelNames { for _, l := range labelNames {
delete(n.Labels, labelNs+l) if strings.Contains(l, "/") {
delete(n.Labels, l)
} else {
delete(n.Labels, labelNs+l)
}
} }
} }
// Add NFD labels to a Node object. // Add NFD labels to a Node object.
func addLabels(n *api.Node, labels map[string]string) { func addLabels(n *api.Node, labels map[string]string) {
for k, v := range labels { for k, v := range labels {
n.Labels[labelNs+k] = v if strings.Contains(k, "/") {
n.Labels[k] = v
} else {
n.Labels[labelNs+k] = v
}
} }
} }

View file

@ -324,7 +324,12 @@ func getFeatureLabels(source source.FeatureSource) (labels Labels, err error) {
label := prefix + k label := prefix + k
// Validate label name. Use dummy namespace 'ns' because there is no // Validate label name. Use dummy namespace 'ns' because there is no
// function to validate just the name part // function to validate just the name part
errs := validation.IsQualifiedName("ns/" + label) labelName := "ns/" + label
// Do not use dummy namespace if there is already a namespace
if strings.Contains(label, "/") {
labelName = label
}
errs := validation.IsQualifiedName(labelName)
if len(errs) > 0 { if len(errs) > 0 {
stderrLogger.Printf("Ignoring invalid feature name '%s': %s", label, errs) stderrLogger.Printf("Ignoring invalid feature name '%s': %s", label, errs)
continue continue

View file

@ -71,9 +71,15 @@ func parseFeatures(lines [][]byte, prefix string) source.Features {
lineSplit := strings.SplitN(string(line), "=", 2) lineSplit := strings.SplitN(string(line), "=", 2)
// Check if we need to add prefix // Check if we need to add prefix
key := prefix + "-" + lineSplit[0] var key string
if lineSplit[0][0] == '/' { if strings.Contains(lineSplit[0], "/") {
key = lineSplit[0][1:] if lineSplit[0][0] == '/' {
key = lineSplit[0][1:]
} else {
key = lineSplit[0]
}
} else {
key = prefix + "-" + lineSplit[0]
} }
// Check if it's a boolean value // Check if it's a boolean value