mirror of
https://github.com/kubernetes-sigs/node-feature-discovery.git
synced 2025-03-05 08:17:04 +00:00
Support feature detector hooks
Implement a new feature source named 'local' whose only purpose is to run feature source hooks found under /etc/kubernetes/node-feature-discovery/source.d/ It tries to execute all files found under the directory, in alphabetical order. This feature source provides users a mechanism to implement custom feature sources in a pluggable way, without modifying nfd source code or Docker images. The hooks are supposed to print all discovered features in stdout, one feature per line. The output in stdout is used in the node label as is. Full node label name will have the following format: feature.node.kubernetes.io/<hook name>-<feature name> Stderr from the hooks is propagated to nfd log.
This commit is contained in:
parent
0259f0652a
commit
a84b5c9d82
4 changed files with 174 additions and 6 deletions
47
README.md
47
README.md
|
@ -54,7 +54,7 @@ node-feature-discovery.
|
|||
will override settings read from the config file.
|
||||
[Default: ]
|
||||
--sources=<sources> Comma separated list of feature sources.
|
||||
[Default: cpuid,iommu,kernel,memory,network,pci,pstate,rdt,selinux,storage]
|
||||
[Default: cpuid,iommu,kernel,local,memory,network,pci,pstate,rdt,selinux,storage]
|
||||
--no-publish Do not publish discovered features to the
|
||||
cluster-local Kubernetes API server.
|
||||
--label-whitelist=<pattern> Regular expression to filter label names to
|
||||
|
@ -80,6 +80,7 @@ The current set of feature sources are the following:
|
|||
- [CPUID][cpuid] for x86/Arm64 CPU details
|
||||
- IOMMU
|
||||
- Kernel
|
||||
- Local (user-specific features)
|
||||
- Memory
|
||||
- Network
|
||||
- Pstate ([Intel P-State driver][intel-pstate])
|
||||
|
@ -122,7 +123,8 @@ the only label value published for features is the string `"true"`._
|
|||
"feature.node.kubernetes.io/nfd-pstate-<feature-name>": "true",
|
||||
"feature.node.kubernetes.io/nfd-rdt-<feature-name>": "true",
|
||||
"feature.node.kubernetes.io/nfd-selinux-<feature-name>": "true",
|
||||
"feature.node.kubernetes.io/nfd-storage-<feature-name>": "true"
|
||||
"feature.node.kubernetes.io/nfd-storage-<feature-name>": "true",
|
||||
"feature.node.kubernetes.io/<hook name>-<feature name>": "true"
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -176,6 +178,47 @@ such as restricting discovered features with the --label-whitelist option._
|
|||
| <br> | minor | Second component of the kernel version (e.g. '5')
|
||||
| <br> | revision | Third component of the kernel version (e.g. '6')
|
||||
|
||||
### 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
|
||||
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 hook files must be executable. When executed, the hooks are supposed to
|
||||
print all discovered features in `stdout`, one feature per line. The output in
|
||||
stdout is used in the node label as is. Unlike other feature sources, the
|
||||
source name (i.e. `local`) is not used as a prefix in the label. The full name
|
||||
of node label name will conform to the following convention:
|
||||
`feature.node.kubernetes.io/<hook name>-<feature name>`.
|
||||
`stderr` output of the hooks is propagated to NFD log so it can be used for
|
||||
debugging and logging.
|
||||
|
||||
**An example:**
|
||||
User has a shell script
|
||||
`/etc/kubernetes/node-feature-discovery/source.d/my-source` which has the
|
||||
following `stdout` output:
|
||||
```
|
||||
MY_FEATURE_1
|
||||
MY_FEATURE_2
|
||||
```
|
||||
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=true
|
||||
```
|
||||
|
||||
**NOTE!** NFD will blindly run any executables placed/mounted in the hooks
|
||||
directory. It is the user's responsibility to review the hooks for e.g.
|
||||
possible security implications.
|
||||
|
||||
### Memory Features
|
||||
|
||||
| Feature name | Description |
|
||||
|
|
14
main.go
14
main.go
|
@ -17,6 +17,7 @@ import (
|
|||
"github.com/kubernetes-incubator/node-feature-discovery/source/fake"
|
||||
"github.com/kubernetes-incubator/node-feature-discovery/source/iommu"
|
||||
"github.com/kubernetes-incubator/node-feature-discovery/source/kernel"
|
||||
"github.com/kubernetes-incubator/node-feature-discovery/source/local"
|
||||
"github.com/kubernetes-incubator/node-feature-discovery/source/memory"
|
||||
"github.com/kubernetes-incubator/node-feature-discovery/source/network"
|
||||
"github.com/kubernetes-incubator/node-feature-discovery/source/panic_fake"
|
||||
|
@ -179,7 +180,7 @@ func argsParse(argv []string) (args Args) {
|
|||
will override settings read from the config file.
|
||||
[Default: ]
|
||||
--sources=<sources> Comma separated list of feature sources.
|
||||
[Default: cpuid,iommu,kernel,memory,network,pci,pstate,rdt,selinux,storage]
|
||||
[Default: cpuid,iommu,kernel,local,memory,network,pci,pstate,rdt,selinux,storage]
|
||||
--no-publish Do not publish discovered features to the
|
||||
cluster-local Kubernetes API server.
|
||||
--label-whitelist=<pattern> Regular expression to filter label names to
|
||||
|
@ -266,6 +267,9 @@ func configureParameters(sourcesWhiteList []string, labelWhiteListStr string) (e
|
|||
rdt.Source{},
|
||||
selinux.Source{},
|
||||
storage.Source{},
|
||||
// local needs to be the last source so that it is able to override
|
||||
// labels from other sources
|
||||
local.Source{},
|
||||
}
|
||||
|
||||
enabledSources = []source.FeatureSource{}
|
||||
|
@ -351,7 +355,13 @@ func getFeatureLabels(source source.FeatureSource) (labels Labels, err error) {
|
|||
return nil, err
|
||||
}
|
||||
for k := range features {
|
||||
labels[fmt.Sprintf("%s-%s", source.Name(), k)] = fmt.Sprintf("%v", features[k])
|
||||
prefix := source.Name() + "-"
|
||||
switch source.(type) {
|
||||
case local.Source:
|
||||
// Do not prefix labels from the hooks
|
||||
prefix = ""
|
||||
}
|
||||
labels[fmt.Sprintf("%s%s", prefix, k)] = fmt.Sprintf("%v", features[k])
|
||||
}
|
||||
return labels, nil
|
||||
}
|
||||
|
|
|
@ -152,7 +152,7 @@ func TestArgsParse(t *testing.T) {
|
|||
So(args.sleepInterval, ShouldEqual, 60*time.Second)
|
||||
So(args.noPublish, ShouldBeTrue)
|
||||
So(args.oneshot, ShouldBeTrue)
|
||||
So(args.sources, ShouldResemble, []string{"cpuid", "iommu", "kernel", "memory", "network", "pci", "pstate", "rdt", "selinux", "storage"})
|
||||
So(args.sources, ShouldResemble, []string{"cpuid", "iommu", "kernel", "local", "memory", "network", "pci", "pstate", "rdt", "selinux", "storage"})
|
||||
So(len(args.labelWhiteList), ShouldEqual, 0)
|
||||
})
|
||||
})
|
||||
|
@ -174,7 +174,7 @@ func TestArgsParse(t *testing.T) {
|
|||
|
||||
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", "iommu", "kernel", "memory", "network", "pci", "pstate", "rdt", "selinux", "storage"})
|
||||
So(args.sources, ShouldResemble, []string{"cpuid", "iommu", "kernel", "local", "memory", "network", "pci", "pstate", "rdt", "selinux", "storage"})
|
||||
So(args.labelWhiteList, ShouldResemble, ".*rdt.*")
|
||||
})
|
||||
})
|
||||
|
|
115
source/local/local.go
Normal file
115
source/local/local.go
Normal file
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/kubernetes-incubator/node-feature-discovery/source"
|
||||
)
|
||||
|
||||
// Config
|
||||
var (
|
||||
hookDir = "/etc/kubernetes/node-feature-discovery/source.d/"
|
||||
logger = log.New(os.Stderr, "", log.LstdFlags)
|
||||
)
|
||||
|
||||
// Implement FeatureSource interface
|
||||
type Source struct{}
|
||||
|
||||
func (s Source) Name() string { return "local" }
|
||||
|
||||
func (s Source) Discover() (source.Features, error) {
|
||||
features := source.Features{}
|
||||
|
||||
files, err := ioutil.ReadDir(hookDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
log.Printf("ERROR: hook directory %v does not exist", hookDir)
|
||||
return features, nil
|
||||
}
|
||||
return features, fmt.Errorf("Unable to access %v: %v", hookDir, err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
hook := file.Name()
|
||||
hookFeatures, err := runHook(hook)
|
||||
if err != nil {
|
||||
log.Printf("ERROR: source hook '%v' failed: %v", hook, err)
|
||||
continue
|
||||
}
|
||||
for _, feature := range hookFeatures {
|
||||
features[hook+"-"+feature] = true
|
||||
}
|
||||
}
|
||||
|
||||
return features, nil
|
||||
}
|
||||
|
||||
// Run one hook
|
||||
func runHook(file string) ([]string, error) {
|
||||
var features []string
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if filestat.Mode().IsRegular() {
|
||||
cmd := exec.Command(path)
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
// Run hook
|
||||
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 {
|
||||
// Don't print the last empty string
|
||||
break
|
||||
}
|
||||
log.Printf("%v: %s", file, line)
|
||||
}
|
||||
|
||||
// Do not return any features if an error occurred
|
||||
if err != nil {
|
||||
return features, err
|
||||
}
|
||||
|
||||
// Return features printed to stdout
|
||||
lines = bytes.Split(stdout.Bytes(), []byte("\n"))
|
||||
for _, line := range lines {
|
||||
if len(line) > 0 {
|
||||
features = append(features, string(line))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return features, nil
|
||||
}
|
Loading…
Add table
Reference in a new issue