1
0
Fork 0
mirror of https://github.com/kubernetes-sigs/node-feature-discovery.git synced 2025-03-14 20:56:42 +00:00

Make it possible to run nfd as a DaemonSet (#105)

* Re-order imports in main.go alphabetically
* Refactor argument parsing
* Run nfd periodically in a loop by default
* Implement --sleep-interval command line option
* Add template for running nfd as a Kubernetes DaemonSet
This commit is contained in:
Markus Lehtonen 2018-04-11 19:33:06 +03:00 committed by Balaji Subramaniam
parent 0150183f60
commit 01e2110a5c
5 changed files with 187 additions and 78 deletions

View file

@ -33,7 +33,8 @@ This project uses GitHub [milestones](https://github.com/kubernetes-incubator/no
node-feature-discovery.
Usage:
node-feature-discovery [--no-publish --sources=<sources> --label-whitelist=<pattern>]
node-feature-discovery [--no-publish] [--sources=<sources>] [--label-whitelist=<pattern>]
[--oneshot | --sleep-interval=<seconds>]
node-feature-discovery -h | --help
node-feature-discovery --version
@ -46,6 +47,10 @@ node-feature-discovery.
cluster-local Kubernetes API server.
--label-whitelist=<pattern> Regular expression to filter label names to
publish to the Kubernetes API server. [Default: ]
--oneshot Label once and exit.
--sleep-interval=<seconds> Time to sleep between re-labeling. Non-positive
value implies no re-labeling (i.e. infinite
sleep). [Default: 60s]
```
## Feature discovery
@ -166,8 +171,20 @@ such as restricting discovered features with the --label-whitelist option._
### Usage
Feature discovery is done as a one-shot job. There is an example script in this
repo that demonstrates how to deploy the job to unlabeled nodes.
Feature discovery is preferably run as a Kubernetes DaemonSet. There is an
example spec that can be used as a template, or, as is when just trying out the
service:
```
kubectl create -f node-feature-discovery-daemonset.json.template
```
When run as a daemonset, nodes are re-labeled at an interval specified using
the `--sleep-interval` option. In the [template](https://github.com/kubernetes-incubator/node-feature-discovery/blob/master/node-feature-discovery-daemonset.json.template#L38) the default interval is set to 60s
which is also the default when no `--sleep-interval` is specified.
Feature discovery can alternatively be configured as a one-shot job. There is
an example script in this repo that demonstrates how to deploy the job to
unlabeled nodes.
```
./label-nodes.sh

98
main.go
View file

@ -6,6 +6,7 @@ import (
"os"
"regexp"
"strings"
"time"
"github.com/docopt/docopt-go"
"github.com/kubernetes-incubator/node-feature-discovery/source"
@ -15,9 +16,9 @@ import (
"github.com/kubernetes-incubator/node-feature-discovery/source/network"
"github.com/kubernetes-incubator/node-feature-discovery/source/panic_fake"
"github.com/kubernetes-incubator/node-feature-discovery/source/pstate"
"github.com/kubernetes-incubator/node-feature-discovery/source/storage"
"github.com/kubernetes-incubator/node-feature-discovery/source/rdt"
"github.com/kubernetes-incubator/node-feature-discovery/source/selinux"
"github.com/kubernetes-incubator/node-feature-discovery/source/storage"
api "k8s.io/api/core/v1"
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8sclient "k8s.io/client-go/kubernetes"
@ -71,6 +72,15 @@ type APIHelpers interface {
UpdateNode(*k8sclient.Clientset, *api.Node) error
}
// Command line arguments
type Args struct {
labelWhiteList string
noPublish bool
oneshot bool
sleepInterval time.Duration
sources []string
}
func main() {
// Assert that the version is known
if version == "" {
@ -78,32 +88,47 @@ func main() {
}
// Parse command-line arguments.
noPublish, sourcesArg, whiteListArg := argsParse(nil)
args := argsParse(nil)
// Configure the parameters for feature discovery.
sources, labelWhiteList, err := configureParameters(sourcesArg, whiteListArg)
enabledSources, labelWhiteList, err := configureParameters(args.sources, args.labelWhiteList)
if err != nil {
stderrLogger.Fatalf("error occurred while configuring parameters: %s", err.Error())
}
// Get the set of feature labels.
labels := createFeatureLabels(sources, labelWhiteList)
helper := APIHelpers(k8sHelpers{})
// Update the node with the feature labels.
err = updateNodeWithFeatureLabels(helper, noPublish, labels)
if err != nil {
stderrLogger.Fatalf("error occurred while updating node with feature labels: %s", err.Error())
for {
// Get the set of feature labels.
labels := createFeatureLabels(enabledSources, labelWhiteList)
// Update the node with the feature labels.
err = updateNodeWithFeatureLabels(helper, args.noPublish, labels)
if err != nil {
stderrLogger.Fatalf("error occurred while updating node with feature labels: %s", err.Error())
}
if args.oneshot {
break
}
if args.sleepInterval > 0 {
time.Sleep(args.sleepInterval)
} else {
// Sleep forever
select {}
}
}
}
// argsParse parses the command line arguments passed to the program.
// The argument argv is passed only for testing purposes.
func argsParse(argv []string) (noPublish bool, sourcesArg []string, whiteListArg string) {
func argsParse(argv []string) (args Args) {
usage := fmt.Sprintf(`%s.
Usage:
%s [--no-publish --sources=<sources> --label-whitelist=<pattern>]
%s [--no-publish] [--sources=<sources>] [--label-whitelist=<pattern>]
[--oneshot | --sleep-interval=<seconds>]
%s -h | --help
%s --version
@ -115,7 +140,11 @@ func argsParse(argv []string) (noPublish bool, sourcesArg []string, whiteListArg
--no-publish Do not publish discovered features to the
cluster-local Kubernetes API server.
--label-whitelist=<pattern> Regular expression to filter label names to
publish to the Kubernetes API server. [Default: ]`,
publish to the Kubernetes API server. [Default: ]
--oneshot Label once and exit.
--sleep-interval=<seconds> Time to sleep between re-labeling. Non-positive
value implies no re-labeling (i.e. infinite
sleep). [Default: 60s]`,
ProgramName,
ProgramName,
ProgramName,
@ -126,19 +155,32 @@ func argsParse(argv []string) (noPublish bool, sourcesArg []string, whiteListArg
fmt.Sprintf("%s %s", ProgramName, version), false)
// Parse argument values as usable types.
noPublish = arguments["--no-publish"].(bool)
sourcesArg = strings.Split(arguments["--sources"].(string), ",")
whiteListArg = arguments["--label-whitelist"].(string)
var err error
args.noPublish = arguments["--no-publish"].(bool)
args.sources = strings.Split(arguments["--sources"].(string), ",")
args.labelWhiteList = arguments["--label-whitelist"].(string)
args.oneshot = arguments["--oneshot"].(bool)
args.sleepInterval, err = time.ParseDuration(arguments["--sleep-interval"].(string))
return noPublish, sourcesArg, whiteListArg
// Check that sleep interval has a sane value
if err != nil {
stderrLogger.Fatalf("invalid --sleep-interval specified: %s", err.Error())
}
if args.sleepInterval > 0 && args.sleepInterval < time.Second {
stderrLogger.Printf("WARNING: too short sleep-intervall specified (%s), forcing to 1s", args.sleepInterval.String())
args.sleepInterval = time.Second
}
return args
}
// configureParameters returns all the variables required to perform feature
// discovery based on command line arguments.
func configureParameters(sourcesArg []string, whiteListArg string) (sources []source.FeatureSource, labelWhiteList *regexp.Regexp, err error) {
enabledSources := map[string]struct{}{}
for _, s := range sourcesArg {
enabledSources[strings.TrimSpace(s)] = struct{}{}
func configureParameters(sourcesWhiteList []string, labelWhiteListStr string) (enabledSources []source.FeatureSource, labelWhiteList *regexp.Regexp, err error) {
// A map for lookup
sourcesWhiteListMap := map[string]struct{}{}
for _, s := range sourcesWhiteList {
sourcesWhiteListMap[strings.TrimSpace(s)] = struct{}{}
}
// Configure feature sources.
@ -154,21 +196,21 @@ func configureParameters(sourcesArg []string, whiteListArg string) (sources []so
panic_fake.Source{},
}
sources = []source.FeatureSource{}
enabledSources = []source.FeatureSource{}
for _, s := range allSources {
if _, enabled := enabledSources[s.Name()]; enabled {
sources = append(sources, s)
if _, enabled := sourcesWhiteListMap[s.Name()]; enabled {
enabledSources = append(enabledSources, s)
}
}
// Compile whiteListArg regex
labelWhiteList, err = regexp.Compile(whiteListArg)
// Compile labelWhiteList regex
labelWhiteList, err = regexp.Compile(labelWhiteListStr)
if err != nil {
stderrLogger.Printf("error parsing whitelist regex (%s): %s", whiteListArg, err)
stderrLogger.Printf("error parsing whitelist regex (%s): %s", labelWhiteListStr, err)
return nil, nil, err
}
return sources, labelWhiteList, nil
return enabledSources, labelWhiteList, nil
}
// createFeatureLabels returns the set of feature labels from the enabled

View file

@ -4,6 +4,7 @@ import (
"fmt"
"regexp"
"testing"
"time"
"github.com/kubernetes-incubator/node-feature-discovery/source"
"github.com/kubernetes-incubator/node-feature-discovery/source/fake"
@ -122,48 +123,52 @@ func TestDiscoveryWithMockSources(t *testing.T) {
func TestArgsParse(t *testing.T) {
Convey("When parsing command line arguments", t, func() {
argv1 := []string{"--no-publish"}
argv2 := []string{"--sources=fake1,fake2,fake3"}
argv1 := []string{"--no-publish", "--oneshot"}
argv2 := []string{"--sources=fake1,fake2,fake3", "--sleep-interval=30s"}
argv3 := []string{"--label-whitelist=.*rdt.*"}
argv4 := []string{"--no-publish", "--sources=fake1,fake2,fake3"}
Convey("When --no-publish flag is passed", func() {
noPublish, sourcesArg, whiteListArg := argsParse(argv1)
Convey("When --no-publish and --oneshot flags are passed", func() {
args := argsParse(argv1)
Convey("noPublish is set and sourcesArg is set to the default value", func() {
So(noPublish, ShouldBeTrue)
So(sourcesArg, ShouldResemble, []string{"cpuid", "rdt", "pstate", "memory", "network", "storage", "selinux"})
So(len(whiteListArg), ShouldEqual, 0)
Convey("noPublish is set and args.sources is set to the default value", func() {
So(args.sleepInterval, ShouldEqual, 60*time.Second)
So(args.noPublish, ShouldBeTrue)
So(args.oneshot, ShouldBeTrue)
So(args.sources, ShouldResemble, []string{"cpuid", "rdt", "pstate", "memory", "network", "storage", "selinux"})
So(len(args.labelWhiteList), ShouldEqual, 0)
})
})
Convey("When --sources flag is passed and set to some values", func() {
noPublish, sourcesArg, whiteListArg := argsParse(argv2)
Convey("When --sources flag is passed and set to some values, --sleep-inteval is specified", func() {
args := argsParse(argv2)
Convey("sourcesArg is set to appropriate values", func() {
So(noPublish, ShouldBeFalse)
So(sourcesArg, ShouldResemble, []string{"fake1", "fake2", "fake3"})
So(len(whiteListArg), ShouldEqual, 0)
Convey("args.sources is set to appropriate values", func() {
So(args.sleepInterval, ShouldEqual, 30*time.Second)
So(args.noPublish, ShouldBeFalse)
So(args.oneshot, ShouldBeFalse)
So(args.sources, ShouldResemble, []string{"fake1", "fake2", "fake3"})
So(len(args.labelWhiteList), ShouldEqual, 0)
})
})
Convey("When --label-whitelist flag is passed and set to some value", func() {
noPublish, sourcesArg, whiteListArg := argsParse(argv3)
args := argsParse(argv3)
Convey("whiteListArg is set to appropriate value and sourcesArg is set to default value", func() {
So(noPublish, ShouldBeFalse)
So(sourcesArg, ShouldResemble, []string{"cpuid", "rdt", "pstate", "memory", "network", "storage","selinux"})
So(whiteListArg, ShouldResemble, ".*rdt.*")
Convey("args.labelWhiteList is set to appropriate value and args.sources is set to default value", func() {
So(args.noPublish, ShouldBeFalse)
So(args.sources, ShouldResemble, []string{"cpuid", "rdt", "pstate", "memory", "network", "storage", "selinux"})
So(args.labelWhiteList, ShouldResemble, ".*rdt.*")
})
})
Convey("When --no-publish and --sources flag are passed and --sources flag is set to some value", func() {
noPublish, sourcesArg, whiteListArg := argsParse(argv4)
args := argsParse(argv4)
Convey("--no-publish is set and sourcesArg is set to appropriate values", func() {
So(noPublish, ShouldBeTrue)
So(sourcesArg, ShouldResemble, []string{"fake1", "fake2", "fake3"})
So(len(whiteListArg), ShouldEqual, 0)
Convey("--no-publish is set and args.sources is set to appropriate values", func() {
So(args.noPublish, ShouldBeTrue)
So(args.sources, ShouldResemble, []string{"fake1", "fake2", "fake3"})
So(len(args.labelWhiteList), ShouldEqual, 0)
})
})
})
@ -172,60 +177,60 @@ func TestArgsParse(t *testing.T) {
func TestConfigureParameters(t *testing.T) {
Convey("When configuring parameters for node feature discovery", t, func() {
Convey("When no sourcesArg and whiteListArg are passed", func() {
sourcesArg := []string{}
whiteListArg := ""
Convey("When no sourcesWhiteList and labelWhiteListStr are passed", func() {
sourcesWhiteList := []string{}
labelWhiteListStr := ""
emptyRegexp, _ := regexp.Compile("")
sources, labelWhiteList, err := configureParameters(sourcesArg, whiteListArg)
enabledSources, labelWhiteList, err := configureParameters(sourcesWhiteList, labelWhiteListStr)
Convey("Error should not be produced", func() {
So(err, ShouldBeNil)
})
Convey("No sources or labelWhiteList are returned", func() {
So(len(sources), ShouldEqual, 0)
Convey("No sourcesWhiteList or labelWhiteList are returned", func() {
So(len(enabledSources), ShouldEqual, 0)
So(labelWhiteList, ShouldResemble, emptyRegexp)
})
})
Convey("When sourcesArg is passed", func() {
sourcesArg := []string{"fake"}
whiteListArg := ""
Convey("When sourcesWhiteList is passed", func() {
sourcesWhiteList := []string{"fake"}
labelWhiteListStr := ""
emptyRegexp, _ := regexp.Compile("")
sources, labelWhiteList, err := configureParameters(sourcesArg, whiteListArg)
enabledSources, labelWhiteList, err := configureParameters(sourcesWhiteList, labelWhiteListStr)
Convey("Error should not be produced", func() {
So(err, ShouldBeNil)
})
Convey("Proper sources are returned", func() {
So(len(sources), ShouldEqual, 1)
So(sources[0], ShouldHaveSameTypeAs, fake.Source{})
Convey("Proper sourcesWhiteList are returned", func() {
So(len(enabledSources), ShouldEqual, 1)
So(enabledSources[0], ShouldHaveSameTypeAs, fake.Source{})
So(labelWhiteList, ShouldResemble, emptyRegexp)
})
})
Convey("When invalid whiteListArg is passed", func() {
sourcesArg := []string{""}
whiteListArg := "*"
sources, labelWhiteList, err := configureParameters(sourcesArg, whiteListArg)
Convey("When invalid labelWhiteListStr is passed", func() {
sourcesWhiteList := []string{""}
labelWhiteListStr := "*"
enabledSources, labelWhiteList, err := configureParameters(sourcesWhiteList, labelWhiteListStr)
Convey("Error is produced", func() {
So(sources, ShouldBeNil)
So(enabledSources, ShouldBeNil)
So(labelWhiteList, ShouldBeNil)
So(err, ShouldNotBeNil)
})
})
Convey("When valid whiteListArg is passed", func() {
sourcesArg := []string{""}
whiteListArg := ".*rdt.*"
Convey("When valid labelWhiteListStr is passed", func() {
sourcesWhiteList := []string{""}
labelWhiteListStr := ".*rdt.*"
expectRegexp, err := regexp.Compile(".*rdt.*")
sources, labelWhiteList, err := configureParameters(sourcesArg, whiteListArg)
enabledSources, labelWhiteList, err := configureParameters(sourcesWhiteList, labelWhiteListStr)
Convey("Error should not be produced", func() {
So(err, ShouldBeNil)
})
Convey("Proper labelWhiteList is returned", func() {
So(len(sources), ShouldEqual, 0)
So(len(enabledSources), ShouldEqual, 0)
So(labelWhiteList, ShouldResemble, expectRegexp)
})
})

View file

@ -0,0 +1,44 @@
{
"apiVersion": "apps/v1",
"kind": "DaemonSet",
"metadata": {
"labels": {
"app": "node-feature-discovery"
},
"name": "node-feature-discovery"
},
"spec": {
"selector": {
"matchLabels": {
"app": "node-feature-discovery"
}
},
"template": {
"metadata": {
"labels": {
"app": "node-feature-discovery"
}
},
"spec": {
"hostNetwork": true,
"containers": [
{
"env": [
{
"name": "NODE_NAME",
"valueFrom": {
"fieldRef": {
"fieldPath": "spec.nodeName"
}
}
}
],
"image": "quay.io/kubernetes_incubator/node-feature-discovery:v0.1.0",
"name": "node-feature-discovery",
"args": ["--sleep-interval=60s"]
}
]
}
}
}
}

View file

@ -32,6 +32,7 @@
],
"image": "quay.io/kubernetes_incubator/node-feature-discovery:v0.1.0",
"name": "node-feature-discovery",
"args": ["--oneshot"],
"ports": [
{
"containerPort": 7156,