diff --git a/docs/usage/customization-guide.md b/docs/usage/customization-guide.md index 1ff4e64942..0bec68c599 100644 --- a/docs/usage/customization-guide.md +++ b/docs/usage/customization-guide.md @@ -317,6 +317,38 @@ Label namespace may be specified with `/[=]`. Comment lines (starting with `#`) are ignored. +Adding following line anywhere to feature file defines date when +its content expires / is ignored: + +```plaintext +# +expiry-time=2023-07-29T11:22:33Z +``` + +Also, the expiry-time value would stay the same during the processing of the +feature file until another expiry-time directive is encountered. +Considering the following file: + +```plaintext +# +expiry-time=2012-07-28T11:22:33Z +featureKey=featureValue + +# +expiry-time=2080-07-28T11:22:33Z +featureKey2=featureValue2 + +# +expiry-time=2070-07-28T11:22:33Z +featureKey3=featureValue3 + +# +expiry-time=2002-07-28T11:22:33Z +featureKey4=featureValue4 +``` + +After processing the above file, only `featureKey2` and `featureKey3` would be +included in the list of accepted features. + +> **NOTE:** The time format that we are supporting is RFC3339. Also, the `expiry-time` +> tag is only evaluated in each re-discovery period, and the expiration of +> node labels is not tracked. + ### Mounts The standard NFD deployments contain `hostPath` mounts for diff --git a/source/local/local.go b/source/local/local.go index 9d14595727..3f2529bccc 100644 --- a/source/local/local.go +++ b/source/local/local.go @@ -23,6 +23,7 @@ import ( "os/exec" "path/filepath" "strings" + "time" "k8s.io/klog/v2" @@ -37,6 +38,13 @@ const Name = "local" // LabelFeature of this feature source const LabelFeature = "label" +// ExpiryTimeKey is the key of this feature source indicating +// when features should be removed. +const ExpiryTimeKey = "expiry-time" + +// DirectivePrefix defines the prefix of directives that should be parsed +const DirectivePrefix = "# +" + // Config var ( featureFilesDir = "/etc/kubernetes/node-feature-discovery/features.d/" @@ -53,6 +61,11 @@ type Config struct { HooksEnabled bool `json:"hooksEnabled,omitempty"` } +// parsingOpts contains options used for directives parsing +type parsingOpts struct { + ExpiryTime time.Time +} + // Singleton source instance var ( src = localSource{config: newDefaultConfig()} @@ -144,14 +157,56 @@ func (s *localSource) GetFeatures() *nfdv1alpha1.Features { return s.features } -func parseFeatures(lines [][]byte) map[string]string { +func parseDirectives(line string, opts *parsingOpts) error { + if !strings.HasPrefix(line, DirectivePrefix) { + return nil + } + + directive := line[len(DirectivePrefix):] + split := strings.SplitN(directive, "=", 2) + key := split[0] + + if len(split) == 1 { + return fmt.Errorf("invalid directive format in %q, should be '# +key=value'", line) + } + value := split[1] + + switch key { + case ExpiryTimeKey: + expiryDate, err := time.Parse(time.RFC3339, strings.TrimSpace(value)) + if err != nil { + return fmt.Errorf("failed to parse expiry-date directive: %w", err) + } + opts.ExpiryTime = expiryDate + default: + return fmt.Errorf("unknown feature file directive %q", key) + } + + return nil +} + +func parseFeatures(lines [][]byte, fileName string) map[string]string { features := make(map[string]string) + now := time.Now() + parsingOpts := &parsingOpts{ + ExpiryTime: now, + } for _, l := range lines { line := strings.TrimSpace(string(l)) if len(line) > 0 { - // Skip comment lines if strings.HasPrefix(line, "#") { + // Parse directives + err := parseDirectives(line, parsingOpts) + if err != nil { + klog.ErrorS(err, "error while parsing directives", "fileName", fileName) + } + + continue + } + + // handle expiration + if parsingOpts.ExpiryTime.Before(now) { continue } @@ -197,7 +252,7 @@ func getFeaturesFromHooks() (map[string]string, error) { } // Append features - fileFeatures := parseFeatures(lines) + fileFeatures := parseFeatures(lines, fileName) klog.V(4).InfoS("hook executed", "fileName", fileName, "features", utils.DelayedDumper(fileFeatures)) for k, v := range fileFeatures { if old, ok := features[k]; ok { @@ -273,7 +328,8 @@ func getFeaturesFromFiles() (map[string]string, error) { } // Append features - fileFeatures := parseFeatures(lines) + fileFeatures := parseFeatures(lines, fileName) + klog.V(4).InfoS("feature file read", "fileName", fileName, "features", utils.DelayedDumper(fileFeatures)) for k, v := range fileFeatures { if old, ok := features[k]; ok { diff --git a/source/local/local_test.go b/source/local/local_test.go index e83493c63c..08d20b77cc 100644 --- a/source/local/local_test.go +++ b/source/local/local_test.go @@ -17,7 +17,11 @@ limitations under the License. package local import ( + "fmt" + "os" + "path/filepath" "testing" + "time" "github.com/stretchr/testify/assert" ) @@ -33,3 +37,48 @@ func TestLocalSource(t *testing.T) { assert.Empty(t, l) } + +func TestGetExpirationDate(t *testing.T) { + expectedFeaturesLen := 5 + pwd, _ := os.Getwd() + featureFilesDir = filepath.Join(pwd, "testdata/features.d") + + features, err := getFeaturesFromFiles() + fmt.Println(features) + assert.NoError(t, err) + assert.Equal(t, expectedFeaturesLen, len(features)) +} + +func TestParseDirectives(t *testing.T) { + testCases := []struct { + name string + directive string + wantErr bool + }{ + { + name: "valid directive", + directive: "# +expiry-time=2080-07-28T11:22:33Z", + wantErr: false, + }, + { + name: "invalid directive", + directive: "# +random-key=random-value", + wantErr: true, + }, + { + name: "invalid directive format", + directive: "# + Something", + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + parsingOpts := parsingOpts{ + ExpiryTime: time.Now(), + } + err := parseDirectives(tc.directive, &parsingOpts) + assert.Equal(t, err != nil, tc.wantErr) + }) + } +} diff --git a/source/local/testdata/features.d/expired_feature b/source/local/testdata/features.d/expired_feature new file mode 100644 index 0000000000..d92bc7d50b --- /dev/null +++ b/source/local/testdata/features.d/expired_feature @@ -0,0 +1,2 @@ +# +expiry-time=2012-07-28T11:22:33Z +featureKeyExpired=featureValue diff --git a/source/local/testdata/features.d/feature_with_comments b/source/local/testdata/features.d/feature_with_comments new file mode 100644 index 0000000000..03dac9adba --- /dev/null +++ b/source/local/testdata/features.d/feature_with_comments @@ -0,0 +1,2 @@ +# Notes: foo bar +featureKeyRandomComment=featureValue diff --git a/source/local/testdata/features.d/multiple_expiration_dates b/source/local/testdata/features.d/multiple_expiration_dates new file mode 100644 index 0000000000..e6fd0ba3c5 --- /dev/null +++ b/source/local/testdata/features.d/multiple_expiration_dates @@ -0,0 +1,11 @@ +# +expiry-time=2012-07-28T11:22:33Z +featureKey=featureValue + +# +expiry-time=2080-07-28T11:22:33Z +featureKey2=featureValue2 + +# +expiry-time=2070-07-28T11:22:33Z +featureKey3=featureValue3 + +# +expiry-time=2002-07-28T11:22:33Z +featureKey4=featureValue4 diff --git a/source/local/testdata/features.d/unparsable_expiry_comment b/source/local/testdata/features.d/unparsable_expiry_comment new file mode 100644 index 0000000000..75ec2f272f --- /dev/null +++ b/source/local/testdata/features.d/unparsable_expiry_comment @@ -0,0 +1,2 @@ +# +expiry-time=2080-07-28T11:22:33X +featureKeyUnparsable=featureValue diff --git a/source/local/testdata/features.d/valid_feature b/source/local/testdata/features.d/valid_feature new file mode 100644 index 0000000000..9c0fdd3072 --- /dev/null +++ b/source/local/testdata/features.d/valid_feature @@ -0,0 +1,2 @@ +# +expiry-time=2080-07-28T11:22:33Z +featureKeyValid=featureValue