Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: add expiry date for feature files #1285

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions docs/usage/customization-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,38 @@ Label namespace may be specified with `<namespace>/<name>[=<value>]`.

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
Expand Down
64 changes: 60 additions & 4 deletions source/local/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"os/exec"
"path/filepath"
"strings"
"time"

"k8s.io/klog/v2"

Expand All @@ -37,6 +38,13 @@
// 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/"
Expand All @@ -53,6 +61,11 @@
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()}
Expand Down Expand Up @@ -144,14 +157,56 @@
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
}

Expand Down Expand Up @@ -197,7 +252,7 @@
}

// Append features
fileFeatures := parseFeatures(lines)
fileFeatures := parseFeatures(lines, fileName)

Check warning on line 255 in source/local/local.go

View check run for this annotation

Codecov / codecov/patch

source/local/local.go#L255

Added line #L255 was not covered by tests
klog.V(4).InfoS("hook executed", "fileName", fileName, "features", utils.DelayedDumper(fileFeatures))
for k, v := range fileFeatures {
if old, ok := features[k]; ok {
Expand Down Expand Up @@ -273,7 +328,8 @@
}

// 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 {
Expand Down
49 changes: 49 additions & 0 deletions source/local/local_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ limitations under the License.
package local

import (
"fmt"
"os"
"path/filepath"
"testing"
"time"

"github.com/stretchr/testify/assert"
)
Expand All @@ -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")

AhmedGrati marked this conversation as resolved.
Show resolved Hide resolved
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,
},
AhmedGrati marked this conversation as resolved.
Show resolved Hide resolved
{
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)
})
}
}
2 changes: 2 additions & 0 deletions source/local/testdata/features.d/expired_feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# +expiry-time=2012-07-28T11:22:33Z
featureKeyExpired=featureValue
2 changes: 2 additions & 0 deletions source/local/testdata/features.d/feature_with_comments
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Notes: foo bar
featureKeyRandomComment=featureValue
11 changes: 11 additions & 0 deletions source/local/testdata/features.d/multiple_expiration_dates
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions source/local/testdata/features.d/unparsable_expiry_comment
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# +expiry-time=2080-07-28T11:22:33X
featureKeyUnparsable=featureValue
2 changes: 2 additions & 0 deletions source/local/testdata/features.d/valid_feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# +expiry-time=2080-07-28T11:22:33Z
featureKeyValid=featureValue