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:
parent
0150183f60
commit
01e2110a5c
5 changed files with 187 additions and 78 deletions
23
README.md
23
README.md
|
@ -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
98
main.go
|
@ -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
|
||||
|
|
99
main_test.go
99
main_test.go
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
44
node-feature-discovery-daemonset.json.template
Normal file
44
node-feature-discovery-daemonset.json.template
Normal 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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -32,6 +32,7 @@
|
|||
],
|
||||
"image": "quay.io/kubernetes_incubator/node-feature-discovery:v0.1.0",
|
||||
"name": "node-feature-discovery",
|
||||
"args": ["--oneshot"],
|
||||
"ports": [
|
||||
{
|
||||
"containerPort": 7156,
|
||||
|
|
Loading…
Add table
Reference in a new issue