1
0
Fork 0
mirror of https://github.com/kubernetes-sigs/node-feature-discovery.git synced 2025-03-28 02:37:11 +00:00

Merge pull request #228 from Ethyling/local-source-read-files

Allow to get labels by reading files in local source
This commit is contained in:
Kubernetes Prow Robot 2019-04-03 10:27:41 -07:00 committed by GitHub
commit effd6d436a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 165 additions and 47 deletions

View file

@ -122,7 +122,7 @@ the only label value published for features is the string `"true"`._
"feature.node.kubernetes.io/rdt-<feature-name>": "true",
"feature.node.kubernetes.io/storage-<feature-name>": "true",
"feature.node.kubernetes.io/system-<feature name>": "<feature value>",
"feature.node.kubernetes.io/<hook name>-<feature name>": "<feature value>"
"feature.node.kubernetes.io/<file name>-<feature name>": "<feature value>"
}
```
@ -195,36 +195,40 @@ See [configuration options](#configuration-options) for more information.
### Local (User-specific Features)
NFD has a special feature source named *local* which is designed for running
user-specific feature detector hooks. It provides a mechanism for users to
NFD has a special feature source named *local* which is designed for getting the
labels from user-specific feature detector. It provides a mechanism for users to
implement custom feature sources in a pluggable way, without modifying nfd
source code or Docker images. The local feature source can be used to advertise
new user-specific features, and, for overriding labels created by the other
feature sources.
The *local* feature source tries to execute files found under
`/etc/kubernetes/node-feature-discovery/source.d/` directory. The hooks must be
available inside the Docker image so Volumes and VolumeMounts must be used if
standard NFD images are used.
The *local* feature source gets its labels by two different ways:
* It tries to execute files found under `/etc/kubernetes/node-feature-discovery/source.d/`
directory. The hook files must be executable. When executed, the hooks are
supposed to print all discovered features in `stdout`, one per line.
* It reads files found under `/etc/kubernetes/node-feature-discovery/features.d/`
directory. The file content is expected to be similar to the hook output (described above).
The hook files must be executable. When executed, the hooks are supposed to
print all discovered features in `stdout`, one feature per line. Hooks can
advertise both binary and non-binary labels, using either `<name>` or
`<name>=<value>` output format.
These directories must be available inside the Docker image so Volumes and
VolumeMounts must be used if standard NFD images are used.
Unlike the other feature sources, the name of the hook, instead of the name of
In both cases, the labels can be binary or non binary, using either `<name>` or
`<name>=<value>` format.
Unlike the other feature sources, the name of the file, instead of the name of
the feature source (that would be `local` in this case), is used as a prefix in
the label name, normally. However, if the `<name>` printed by the hook starts
with a slash (`/`) it is used as the label name as is, without any additional
prefix. This makes it possible for the hooks to fully control the feature
label names, e.g. for overriding labels created by other feature sources.
the label name, normally. However, if the `<name>` of the label starts with a
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,
e.g. for overriding labels created by other feature sources.
The value of the label is either `true` (for binary labels) or `<value>`
(for non-binary labels).
`stderr` output of the hooks is propagated to NFD log so it can be used for
debugging and logging.
**An example:**<br/>
**A hook example:**<br/>
User has a shell script
`/etc/kubernetes/node-feature-discovery/source.d/my-source` which has the
following `stdout` output:
@ -242,6 +246,24 @@ feature.node.kubernetes.io/override_source-OVERRIDE_BOOL=true
feature.node.kubernetes.io/override_source-OVERRIDE_VALUE=123
```
**A file example:**<br/>
User has a file
`/etc/kubernetes/node-feature-discovery/features.d/my-source` which contains the
following lines:
```
MY_FEATURE_1
MY_FEATURE_2=myvalue
/override_source-OVERRIDE_BOOL
/override_source-OVERRIDE_VALUE=123
```
which, in turn, will translate into the following node labels:
```
feature.node.kubernetes.io/my-source-MY_FEATURE_1=true
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_VALUE=123
```
NFD tries to run any regular files found from the hooks directory. Any
additional data files your hook might need (e.g. a configuration file) should
be placed in a separate directory in order to avoid NFD unnecessarily trying to

View file

@ -31,8 +31,9 @@ import (
// Config
var (
hookDir = "/etc/kubernetes/node-feature-discovery/source.d/"
logger = log.New(os.Stderr, "", log.LstdFlags)
featureFilesDir = "/etc/kubernetes/node-feature-discovery/features.d/"
hookDir = "/etc/kubernetes/node-feature-discovery/source.d/"
logger = log.New(os.Stderr, "", log.LstdFlags)
)
// Implement FeatureSource interface
@ -41,6 +42,55 @@ type Source struct{}
func (s Source) Name() string { return "local" }
func (s Source) Discover() (source.Features, error) {
featuresFromHooks, err := getFeaturesFromHooks()
if err != nil {
log.Printf(err.Error())
}
featuresFromFiles, err := getFeaturesFromFiles()
if err != nil {
log.Printf(err.Error())
}
// Merge features from hooks and files
for k, v := range featuresFromHooks {
if old, ok := featuresFromFiles[k]; ok {
log.Printf("WARNING: overriding label '%s': value changed from '%s' to '%s'",
k, old, v)
}
featuresFromFiles[k] = v
}
return featuresFromFiles, nil
}
func parseFeatures(lines [][]byte, prefix string) source.Features {
features := source.Features{}
for _, line := range lines {
if len(line) > 0 {
lineSplit := strings.SplitN(string(line), "=", 2)
// Check if we need to add prefix
key := prefix + "-" + lineSplit[0]
if lineSplit[0][0] == '/' {
key = lineSplit[0][1:]
}
// Check if it's a boolean value
if len(lineSplit) == 1 {
features[key] = "true"
} else {
features[key] = lineSplit[1]
}
}
}
return features
}
// Run all hooks and get features
func getFeaturesFromHooks() (source.Features, error) {
features := source.Features{}
files, err := ioutil.ReadDir(hookDir)
@ -53,20 +103,20 @@ func (s Source) Discover() (source.Features, error) {
}
for _, file := range files {
hook := file.Name()
hookFeatures, err := runHook(hook)
fileName := file.Name()
lines, err := runHook(fileName)
if err != nil {
log.Printf("ERROR: source hook '%v' failed: %v", hook, err)
log.Printf("ERROR: source local failed running hook '%v': %v", fileName, err)
continue
}
for feature, value := range hookFeatures {
if feature[0] == '/' {
// Use feature name as the label as is if it is prefixed with a slash
features[feature[1:]] = value
} else {
// Normally, use hook name as label prefix
features[hook+"-"+feature] = value
// Append features
for k, v := range parseFeatures(lines, fileName) {
if old, ok := features[k]; ok {
log.Printf("WARNING: overriding label '%s' from another hook (%s): value changed from '%s' to '%s'",
k, fileName, old, v)
}
features[k] = v
}
}
@ -74,14 +124,14 @@ func (s Source) Discover() (source.Features, error) {
}
// Run one hook
func runHook(file string) (map[string]string, error) {
features := map[string]string{}
func runHook(file string) ([][]byte, error) {
var lines [][]byte
path := filepath.Join(hookDir, file)
filestat, err := os.Stat(path)
if err != nil {
log.Printf("ERROR: skipping %v, failed to get stat: %v", path, err)
return features, err
return lines, err
}
if filestat.Mode().IsRegular() {
@ -95,33 +145,79 @@ func runHook(file string) (map[string]string, error) {
err = cmd.Run()
// Forward stderr to our logger
lines := bytes.Split(stderr.Bytes(), []byte("\n"))
for i, line := range lines {
if i == len(lines)-1 && len(line) == 0 {
errLines := bytes.Split(stderr.Bytes(), []byte("\n"))
for i, line := range errLines {
if i == len(errLines)-1 && len(line) == 0 {
// Don't print the last empty string
break
}
log.Printf("%v: %s", file, line)
}
// Do not return any features if an error occurred
// Do not return any lines if an error occurred
if err != nil {
return features, err
return lines, err
}
lines = bytes.Split(stdout.Bytes(), []byte("\n"))
}
return lines, nil
}
// Read all files to get features
func getFeaturesFromFiles() (source.Features, error) {
features := source.Features{}
files, err := ioutil.ReadDir(featureFilesDir)
if err != nil {
if os.IsNotExist(err) {
log.Printf("ERROR: features directory %v does not exist", featureFilesDir)
return features, nil
}
return features, fmt.Errorf("Unable to access %v: %v", featureFilesDir, err)
}
for _, file := range files {
fileName := file.Name()
lines, err := getFileContent(fileName)
if err != nil {
log.Printf("ERROR: source local failed reading file '%v': %v", fileName, err)
continue
}
// Return features printed to stdout
lines = bytes.Split(stdout.Bytes(), []byte("\n"))
for _, line := range lines {
if len(line) > 0 {
lineSplit := strings.SplitN(string(line), "=", 2)
if len(lineSplit) == 1 {
features[lineSplit[0]] = "true"
} else {
features[lineSplit[0]] = lineSplit[1]
}
// Append features
for k, v := range parseFeatures(lines, fileName) {
if old, ok := features[k]; ok {
log.Printf("WARNING: overriding label '%s' from another features.d file (%s): value changed from '%s' to '%s'",
k, fileName, old, v)
}
features[k] = v
}
}
return features, nil
}
// Read one file
func getFileContent(fileName string) ([][]byte, error) {
var lines [][]byte
path := filepath.Join(featureFilesDir, fileName)
filestat, err := os.Stat(path)
if err != nil {
log.Printf("ERROR: skipping %v, failed to get stat: %v", path, err)
return lines, err
}
if filestat.Mode().IsRegular() {
fileContent, err := ioutil.ReadFile(path)
// Do not return any lines if an error occurred
if err != nil {
return lines, err
}
lines = bytes.Split(fileContent, []byte("\n"))
}
return lines, nil
}