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:
commit
effd6d436a
2 changed files with 165 additions and 47 deletions
56
README.md
56
README.md
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue