Repository: kubernetes-sigs/node-feature-discovery
Pull request #2010 (2025-01-08 13:04:24)

Bugfixes for image compatibility feature
Bugfixes:
* Always pull the latest compatibility artifact
* Handle missing compatibility matches to avoid panic
* Continue procesing the rule if the feature is missing


Patch:
diff --git a/pkg/apis/nfd/nodefeaturerule/rule.go b/pkg/apis/nfd/nodefeaturerule/rule.go
index f243e67e7..9289eaa12 100644
--- a/pkg/apis/nfd/nodefeaturerule/rule.go
+++ b/pkg/apis/nfd/nodefeaturerule/rule.go
@@ -269,7 +269,11 @@ func evaluateFeatureMatcher(m *nfdv1alpha1.FeatureMatcher, features *nfdv1alpha1
 		fI, okI := features.Instances[featureName]
 		if !okF && !okA && !okI {
 			klog.V(2).InfoS("feature not available", "featureName", featureName)
-			return false, nil, nil
+			if failFast {
+				return false, nil, nil
+			}
+			isMatch = false
+			continue
 		}
 
 		if term.MatchExpressions != nil {
diff --git a/pkg/client-nfd/compat/artifact-client/client.go b/pkg/client-nfd/compat/artifact-client/client.go
index ef37f1fc3..13b6f91ed 100644
--- a/pkg/client-nfd/compat/artifact-client/client.go
+++ b/pkg/client-nfd/compat/artifact-client/client.go
@@ -22,6 +22,8 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"slices"
+	"time"
 
 	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
 	oras "oras.land/oras-go/v2"
@@ -34,6 +36,10 @@ import (
 	compatv1alpha1 "sigs.k8s.io/node-feature-discovery/api/image-compatibility/v1alpha1"
 )
 
+const (
+	ArtifactCreationTimestampKey = "org.opencontainers.image.created"
+)
+
 // ArtifactClient interface contain set of functions to manipulate compatibility artfact.
 type ArtifactClient interface {
 	// FetchCompatibilitySpec downloads the compatibility specifcation associated with the image.
@@ -90,6 +96,14 @@ func (c *Client) FetchCompatibilitySpec(ctx context.Context) (*compatv1alpha1.Sp
 	} else if len(descs) < 1 {
 		return nil, fmt.Errorf("compatibility artifact not found")
 	}
+
+	// Sort the artifacts in desc order.
+	// If the artifact does not have creation timestamp it will be moved to the top of the slice.
+	slices.SortFunc(descs, func(i, j ocispec.Descriptor) int {
+		it, _ := time.Parse(time.RFC3339, i.Annotations[ArtifactCreationTimestampKey])
+		jt, _ := time.Parse(time.RFC3339, j.Annotations[ArtifactCreationTimestampKey])
+		return it.Compare(jt)
+	})
 	artifactDesc := descs[len(descs)-1]
 
 	_, content, err := oras.FetchBytes(ctx, repo.Manifests(), artifactDesc.Digest.String(), oras.DefaultFetchBytesOptions)
diff --git a/pkg/client-nfd/compat/node-validator/node-validator.go b/pkg/client-nfd/compat/node-validator/node-validator.go
index ef907c00f..a7c584070 100644
--- a/pkg/client-nfd/compat/node-validator/node-validator.go
+++ b/pkg/client-nfd/compat/node-validator/node-validator.go
@@ -97,6 +97,7 @@ func (nv *nodeValidator) Execute(ctx context.Context) ([]*CompatibilityStatus, e
 }
 
 func evaluateRuleStatus(rule *nfdv1alpha1.Rule, matchStatus *nodefeaturerule.MatchStatus) ProcessedRuleStatus {
+	var matchedFeatureTerms nfdv1alpha1.FeatureMatcher
 	out := ProcessedRuleStatus{Name: rule.Name, IsMatch: matchStatus.IsMatch}
 
 	evaluateFeatureMatcher := func(featureMatcher, matchedFeatureTerms nfdv1alpha1.FeatureMatcher) []MatchedExpression {
@@ -163,11 +164,17 @@ func evaluateRuleStatus(rule *nfdv1alpha1.Rule, matchStatus *nodefeaturerule.Mat
 	}
 
 	if matchFeatures := rule.MatchFeatures; matchFeatures != nil {
-		out.MatchedExpressions = evaluateFeatureMatcher(matchFeatures, matchStatus.MatchedFeaturesTerms)
+		if matchStatus.MatchFeatureStatus != nil {
+			matchedFeatureTerms = matchStatus.MatchFeatureStatus.MatchedFeaturesTerms
+		}
+		out.MatchedExpressions = evaluateFeatureMatcher(matchFeatures, matchedFeatureTerms)
 	}
 
 	for i, matchAnyElem := range rule.MatchAny {
-		matchedExpressions := evaluateFeatureMatcher(matchAnyElem.MatchFeatures, matchStatus.MatchAny[i].MatchedFeaturesTerms)
+		if matchStatus.MatchAny[i].MatchedFeaturesTerms != nil {
+			matchedFeatureTerms = matchStatus.MatchAny[i].MatchedFeaturesTerms
+		}
+		matchedExpressions := evaluateFeatureMatcher(matchAnyElem.MatchFeatures, matchedFeatureTerms)
 		out.MatchedAny = append(out.MatchedAny, MatchAnyElem{MatchedExpressions: matchedExpressions})
 	}
 


Test patch:
diff --git a/pkg/client-nfd/compat/node-validator/node-validator_test.go b/pkg/client-nfd/compat/node-validator/node-validator_test.go
index 6977578a9..c0cbdaf5c 100644
--- a/pkg/client-nfd/compat/node-validator/node-validator_test.go
+++ b/pkg/client-nfd/compat/node-validator/node-validator_test.go
@@ -114,6 +114,17 @@ func TestNodeValidator(t *testing.T) {
 								},
 							},
 						},
+						{
+							Name: "fake_5",
+							MatchFeatures: v1alpha1.FeatureMatcher{
+								{
+									Feature: "unknown.unknown",
+									MatchExpressions: &v1alpha1.MatchExpressionSet{
+										"name": &v1alpha1.MatchExpression{Op: v1alpha1.MatchIn, Value: v1alpha1.MatchValue{"instance_1"}},
+									},
+								},
+							},
+						},
 					},
 				},
 			},
@@ -219,6 +230,19 @@ func TestNodeValidator(t *testing.T) {
 							},
 						},
 					},
+					{
+						Name:    "fake_5",
+						IsMatch: false,
+						MatchedExpressions: []MatchedExpression{
+							{
+								Feature:     "unknown.unknown",
+								Name:        "name",
+								Expression:  &v1alpha1.MatchExpression{Op: v1alpha1.MatchIn, Value: v1alpha1.MatchValue{"instance_1"}},
+								MatcherType: MatchExpressionType,
+								IsMatch:     false,
+							},
+						},
+					},
 				},
 			},
 		}