Skip to content

Commit c4eb8d0

Browse files
authored
Merge pull request #4 from compspec/add-unmarshall-spec
fix: unmarshall spec customization
2 parents 24e54af + eccb603 commit c4eb8d0

File tree

6 files changed

+182
-13
lines changed

6 files changed

+182
-13
lines changed

cmd/ocifit/main.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,11 @@ type JSONPatch struct {
5252

5353
// admit allows the request without modification
5454
func admit(ar *admissionv1.AdmissionReview) *admissionv1.AdmissionResponse {
55-
log.Printf("Allowing pod %s/%s without mutation", ar.Request.Namespace, ar.Request.Name)
55+
if ar.Request.Name != "" {
56+
log.Printf("Allowing pod %s/%s: %s", ar.Request.Namespace, ar.Request.Name)
57+
} else {
58+
log.Printf("Allowing pod in namespace %s without mutation", ar.Request.Namespace)
59+
}
5660
return &admissionv1.AdmissionResponse{
5761
Allowed: true,
5862
UID: ar.Request.UID,
@@ -61,7 +65,13 @@ func admit(ar *admissionv1.AdmissionReview) *admissionv1.AdmissionResponse {
6165

6266
// deny rejects the request with a message
6367
func deny(ar *admissionv1.AdmissionReview, message string) *admissionv1.AdmissionResponse {
64-
log.Printf("Denying pod %s/%s: %s", ar.Request.Namespace, ar.Request.Name, message)
68+
69+
// Most pods don't have a name yet
70+
if ar.Request.Name != "" {
71+
log.Printf("Denying pod %s/%s: %s", ar.Request.Namespace, ar.Request.Name, message)
72+
} else {
73+
log.Printf("Denying pod in namespace %s: %s", ar.Request.Namespace, message)
74+
}
6575
return &admissionv1.AdmissionResponse{
6676
Allowed: false,
6777
UID: ar.Request.UID,
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
{
2+
"version": "v1alpha1",
3+
"compatibilities": [
4+
{
5+
"tag": "ghcr.io/converged-computing/kernel-header-installer:ubuntu2204",
6+
"description": "Compatibility for eBPF installer intended for Ubuntu 22.04 LTS nodes with kernel 6.1 or newer.",
7+
"rules": [
8+
{
9+
"matchFeatures": [
10+
{
11+
"matchExpressions": [
12+
{
13+
"op": "In",
14+
"key": "feature.node.kubernetes.io/system-os_release.ID",
15+
"value": [
16+
"ubuntu"
17+
]
18+
},
19+
{
20+
"op": "In",
21+
"key": "feature.node.kubernetes.io/system-os_release.VERSION_ID",
22+
"value": [
23+
"22.04"
24+
]
25+
},
26+
{
27+
"op": "In",
28+
"key": "feature.node.kubernetes.io/kernel-version.major",
29+
"value": [
30+
"6"
31+
]
32+
},
33+
{
34+
"op": "Gte",
35+
"key": "feature.node.kubernetes.io/kernel-version.minor",
36+
"value": "1"
37+
}
38+
]
39+
}
40+
]
41+
}
42+
]
43+
},
44+
{
45+
"tag": "ghcr.io/converged-computing/kernel-header-installer:fedora43",
46+
"description": "Compatibility for eBPF installer intended for Amazon Linux 2023 nodes with kernel 6.1 or newer.",
47+
"rules": [
48+
{
49+
"matchFeatures": [
50+
{
51+
"matchExpressions": [
52+
{
53+
"op": "In",
54+
"key": "feature.node.kubernetes.io/system-os_release.ID",
55+
"value": [
56+
"amzn"
57+
]
58+
},
59+
{
60+
"op": "In",
61+
"key": "feature.node.kubernetes.io/system-os_release.VERSION_ID",
62+
"value": [
63+
"2023"
64+
]
65+
},
66+
{
67+
"op": "In",
68+
"key": "feature.node.kubernetes.io/kernel-version.major",
69+
"value": [
70+
"6"
71+
]
72+
},
73+
{
74+
"op": "Gte",
75+
"key": "feature.node.kubernetes.io/kernel-version.minor",
76+
"value": "1"
77+
}
78+
]
79+
}
80+
]
81+
}
82+
]
83+
}
84+
]
85+
}

pkg/types/types.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ const (
2525
MatchOpInRegexp MatchOp = "InRegexp"
2626
MatchOpExists MatchOp = "Exists"
2727
MatchOpDoesNotExist MatchOp = "DoesNotExist"
28-
MatchOpGt MatchOp = "Gt" // Greater than
29-
MatchOpLt MatchOp = "Lt" // Less than
28+
MatchOpGt MatchOp = "Gt" // Greater than
29+
MatchOpLt MatchOp = "Lt" // Less than
30+
MatchOpGte MatchOp = "Gte" // Greater than or equal to
31+
MatchOpLte MatchOp = "Lte" // Less than or equal to
3032
)
3133

3234
// MatchExpression specifies a requirement for matching features.

pkg/types/unmarshal.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package types
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
)
7+
8+
// UnmarshalJSON implements a custom unmarshaler for MatchExpression to handle value
9+
func (m *MatchExpression) UnmarshalJSON(data []byte) error {
10+
// 1. Define an alias type to avoid an infinite recursion loop.
11+
// If we called json.Unmarshal on MatchExpression inside this method,
12+
// it would call this method again (stack overflow).
13+
type alias MatchExpression
14+
15+
// 2. Create a temporary struct to unmarshal into. value needs to be raw
16+
temp := &struct {
17+
Value json.RawMessage `json:"value"`
18+
*alias
19+
}{
20+
// 3. Point the alias to the current MatchExpression instance ('m')
21+
// so that Op and Key are populated directly into it.
22+
alias: (*alias)(m),
23+
}
24+
25+
// 4. Unmarshal the data into our temporary struct.
26+
if err := json.Unmarshal(data, &temp); err != nil {
27+
return fmt.Errorf("failed to unmarshal match expression: %w", err)
28+
}
29+
30+
// 5. Now, inspect the raw JSON for the 'value' field.
31+
if len(temp.Value) > 0 {
32+
// If the first character is '[', it's a JSON array.
33+
if temp.Value[0] == '[' {
34+
// Unmarshal it as a normal slice of strings.
35+
if err := json.Unmarshal(temp.Value, &m.Value); err != nil {
36+
return fmt.Errorf("failed to unmarshal 'value' as array: %w", err)
37+
}
38+
} else {
39+
// Otherwise, assume it's a single JSON string.
40+
var strValue string
41+
if err := json.Unmarshal(temp.Value, &strValue); err != nil {
42+
return fmt.Errorf("failed to unmarshal 'value' as string: %w", err)
43+
}
44+
// **This is the key step:** We normalize the single string
45+
// into a slice containing just that string.
46+
m.Value = []string{strValue}
47+
}
48+
}
49+
50+
return nil
51+
}

pkg/validator/rule.go

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package validator
22

33
import (
4-
"fmt"
54
"log"
65
"regexp"
76
"strconv"
@@ -11,7 +10,6 @@ import (
1110

1211
func evaluateRule(expression types.MatchExpression, nodeLabels map[string]string) bool {
1312
// Get the value of the label from the node
14-
fmt.Printf("Checking expression %s against node labels %s", expression, nodeLabels)
1513
nodeVal, ok := nodeLabels[expression.Key]
1614

1715
switch expression.Op {
@@ -66,7 +64,7 @@ func evaluateRule(expression types.MatchExpression, nodeLabels map[string]string
6664
if err != nil {
6765
// A malformed regex in the spec is a spec error, not a node error.
6866
// Log it and treat it as a non-match for this pattern.
69-
log.Printf("WARN: Invalid regexp in compatibility spec: '%s'. Error: %v", pattern, err)
67+
log.Printf("WARN: Invalid regexp in compatibility spec: '%s'. Error: %v\n", pattern, err)
7068
continue
7169
}
7270
// Found a regex match.
@@ -77,7 +75,7 @@ func evaluateRule(expression types.MatchExpression, nodeLabels map[string]string
7775
// No patterns matched.
7876
return false
7977

80-
case types.MatchOpGt, types.MatchOpLt:
78+
case types.MatchOpGt, types.MatchOpLt, types.MatchOpGte, types.MatchOpLte:
8179
// Rule matches if the key exists AND its integer value is > or < the spec's value.
8280
if !ok {
8381

@@ -93,22 +91,28 @@ func evaluateRule(expression types.MatchExpression, nodeLabels map[string]string
9391
// Convert node value to an integer.
9492
nodeInt, err := strconv.Atoi(nodeVal)
9593
if err != nil {
96-
log.Printf("WARN: Could not convert node label value '%s' to int for Gt/Lt comparison. Key: %s", nodeVal, expression.Key)
94+
log.Printf("WARN: Could not convert node label value '%s' to int for Gt/Lt/Gte/Lte comparison. Key: %s", nodeVal, expression.Key)
9795
return false
9896
}
9997

10098
// Convert spec value to an integer.
10199
specInt, err := strconv.Atoi(specValStr)
102100
if err != nil {
103-
log.Printf("WARN: Could not convert spec value '%s' to int for Gt/Lt comparison. Key: %s", specValStr, expression.Key)
101+
log.Printf("WARN: Could not convert spec value '%s' to int for Gt/Lt/Gte/Lte comparison. Key: %s", specValStr, expression.Key)
104102
return false
105103
}
106104

107105
// Perform the correct comparison.
106+
if expression.Op == types.MatchOpGte {
107+
return nodeInt >= specInt
108+
}
108109
if expression.Op == types.MatchOpGt {
109110
return nodeInt > specInt
110111
}
111-
// If not Gt, it must be Lt.
112+
if expression.Op == types.MatchOpLte {
113+
return nodeInt <= specInt
114+
}
115+
// Last comparison, Lte
112116
return nodeInt < specInt
113117

114118
default:

pkg/validator/validator.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,19 @@ import (
77
"ghcr.io/compspec/ocifit-k8s/pkg/types"
88
)
99

10+
// Make it pretty :)
11+
var (
12+
Reset = "\033[0m"
13+
Red = "\033[31m"
14+
Green = "\033[32m"
15+
Yellow = "\033[33m"
16+
Blue = "\033[34m"
17+
Magenta = "\033[35m"
18+
Cyan = "\033[36m"
19+
Gray = "\033[37m"
20+
White = "\033[97m"
21+
)
22+
1023
// evaluateCompatibilitySpec finds the best matching compatibility set from the spec
1124
// by evaluating its rules against the node's labels.
1225
func EvaluateCompatibilitySpec(spec *types.CompatibilitySpec, nodeLabels map[string]string) (string, error) {
@@ -16,6 +29,7 @@ func EvaluateCompatibilitySpec(spec *types.CompatibilitySpec, nodeLabels map[str
1629
// Loop through each top-level Compatibility set in the spec.
1730
for i, comp := range spec.Compatibilities {
1831
allRulesMatch := true
32+
log.Printf("Assessing compatibility of %s", comp.Tag)
1933

2034
// Each Compatibility set can have multiple GroupRules
2135
// All GroupRules must match for the set to be considered a match (AND logic).
@@ -33,16 +47,19 @@ func EvaluateCompatibilitySpec(spec *types.CompatibilitySpec, nodeLabels map[str
3347
for _, expression := range featureMatcher.MatchExpressions {
3448
if !evaluateRule(expression, nodeLabels) {
3549
// As soon as one expression fails, the entire Compatibility set is invalid.
50+
log.Printf(Red+" FAILED %s"+Reset, expression)
3651
allRulesMatch = false
3752
break
53+
} else {
54+
log.Printf(Green+" PASSED %s"+Reset, expression)
3855
}
3956
}
4057
}
4158
}
4259

4360
// If after checking all the rules for this set, allRulesMatch is still true...
4461
if allRulesMatch {
45-
log.Printf("Found a matching compatibility set: '%s' (weight %d)", comp.Tag, comp.Weight)
62+
log.Printf("Found a matching compatibility set: '%s' (weight %d)\n", comp.Tag, comp.Weight)
4663

4764
// Check if this match is better (has a higher weight) than any previous match we found.
4865
if comp.Weight > maxWeight {
@@ -59,6 +76,6 @@ func EvaluateCompatibilitySpec(spec *types.CompatibilitySpec, nodeLabels map[str
5976
return "", fmt.Errorf("no matching compatibility rule found for the given node labels")
6077
}
6178

62-
log.Printf("Selected best match: '%s' with highest weight %d", bestMatch.Tag, bestMatch.Weight)
79+
log.Printf("Selected best match: '%s' with highest weight %d\n", bestMatch.Tag, bestMatch.Weight)
6380
return bestMatch.Tag, nil
6481
}

0 commit comments

Comments
 (0)