Files
Ben Perry 377ba25c26
Scorecard supply-chain security / Scorecard analysis (push) Failing after 1m40s
Post / coverage (push) Failing after 35m43s
Post / images (amd64) (push) Failing after 8m36s
Post / images (arm64) (push) Failing after 8m8s
Post / image manifest (push) Has been skipped
Post / trigger clusteradm e2e (push) Has been skipped
Close stale issues and PRs / stale (push) Successful in 48s
Workload conditions (#910)
* Import OCM API changes for workload conditions

Signed-off-by: Ben Perry <bhperry94@gmail.com>

* Implement condition rule evaluator

Signed-off-by: Ben Perry <bhperry94@gmail.com>

* Evaluate manifest condition rules after apply

Signed-off-by: Ben Perry <bhperry94@gmail.com>

* note to self

Signed-off-by: Ben Perry <bhperry94@gmail.com>

* Cleanup

Signed-off-by: Ben Perry <bhperry94@gmail.com>

* Return config option if rules are set

Signed-off-by: Ben Perry <bhperry94@gmail.com>

* update api

Signed-off-by: Ben Perry <bhperry94@gmail.com>

* Always return an error to inform user about the state of their condition rule

Signed-off-by: Ben Perry <bhperry94@gmail.com>

* Condition rule errors should not result in retrying apply

Signed-off-by: Ben Perry <bhperry94@gmail.com>

* Test condition rule reconciliation

Signed-off-by: Ben Perry <bhperry94@gmail.com>

* Return condition status Unknown when an internal CEL error occurs

Signed-off-by: Ben Perry <bhperry94@gmail.com>

* Update api

Signed-off-by: Ben Perry <bhperry94@gmail.com>

* Switch to common CEL lib

Signed-off-by: Ben Perry <bhperry94@gmail.com>

* Update to simplified celExpressions format

Signed-off-by: Ben Perry <bhperry94@gmail.com>

* Formatting

Signed-off-by: Ben Perry <bhperry94@gmail.com>

* tidy

Signed-off-by: Ben Perry <bhperry94@gmail.com>

* Update ocm api

Signed-off-by: Ben Perry <bhperry94@gmail.com>

* Update sdk-go

Signed-off-by: Ben Perry <bhperry94@gmail.com>

* Switch to sdk-go ConditionLib

Signed-off-by: Ben Perry <bhperry94@gmail.com>

* Update API

Signed-off-by: Ben Perry <bhperry94@gmail.com>

* Switch to WellKnownConditions with required Condition field

Signed-off-by: Ben Perry <bhperry94@gmail.com>

* Support CEL evaluation budget

Signed-off-by: Ben Perry <bhperry94@gmail.com>

* Update sdk-go

Signed-off-by: Ben Perry <bhperry94@gmail.com>

* Update API

Signed-off-by: Ben Perry <bhperry94@gmail.com>

* lint

Signed-off-by: Ben Perry <bhperry94@gmail.com>

* Update go.mod

Signed-off-by: Ben Perry <bhperry94@gmail.com>

* Tests and comments

Signed-off-by: Ben Perry <bhperry94@gmail.com>

* Move condition reader to status controller for more frequent updates

Signed-off-by: Ben Perry <bhperry94@gmail.com>

* Ignore missing WellKnownCondition

Signed-off-by: Ben Perry <bhperry94@gmail.com>

* Fix test

Signed-off-by: Ben Perry <bhperry94@gmail.com>

* Update condition tests

Signed-off-by: Ben Perry <bhperry94@gmail.com>

---------

Signed-off-by: Ben Perry <bhperry94@gmail.com>
2025-06-11 15:47:35 +00:00

384 lines
9.4 KiB
Go

package conditions
import (
"context"
"testing"
apiequality "k8s.io/apimachinery/pkg/api/equality"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/utils/ptr"
ocmfeature "open-cluster-management.io/api/feature"
workapiv1 "open-cluster-management.io/api/work/v1"
"open-cluster-management.io/ocm/pkg/features"
)
const (
deploymentJson = `
{
"apiVersion":"apps/v1",
"kind":"Deployment",
"metadata":{
"name":"test"
},
"status":{
"readyReplicas":1,
"replicas":2,
"conditions":[
{
"type":"Available",
"status":"True"
}
]
}
}
`
jobJsonComplete = `
{
"apiVersion": "batch/v1",
"kind": "Job",
"metadata": {
"name": "test"
},
"status": {
"conditions": [
{
"status": "True",
"type": "Complete"
},
{
"status": "True",
"type": "Another"
}
],
"succeeded": 1
}
}
`
jobJsonFailed = `
{
"apiVersion": "batch/v1",
"kind": "Job",
"metadata": {
"name": "test"
},
"status": {
"conditions": [
{
"status": "True",
"type": "Failed"
}
],
"succeeded": 0
}
}
`
jobJsonIncomplete = `
{
"apiVersion": "batch/v1",
"kind": "Job",
"metadata": {
"name": "test"
},
"status": {
"conditions": [
{
"status": "False",
"type": "Complete"
},
{
"status": "True",
"type": "Another"
}
],
"succeeded": 0
}
}
`
podJsonSucceeded = `
{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "test"
},
"status": {
"conditions": [
{
"status": "False",
"type": "Ready"
}
],
"phase": "Succeeded"
}
}
`
podJsonFailed = `
{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "test"
},
"status": {
"conditions": [
{
"status": "False",
"type": "Ready"
}
],
"phase": "Failed"
}
}
`
podJsonRunning = `
{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "test"
},
"status": {
"conditions": [
{
"status": "False",
"type": "Ready"
}
],
"phase": "Running"
}
}
`
)
func unstrctureObject(data string) *unstructured.Unstructured {
obj := &unstructured.Unstructured{}
_ = obj.UnmarshalJSON([]byte(data))
return obj
}
func TestConditionReader(t *testing.T) {
utilruntime.Must(features.SpokeMutableFeatureGate.Add(ocmfeature.DefaultSpokeWorkFeatureGates))
cases := []struct {
name string
object *unstructured.Unstructured
rule workapiv1.ConditionRule
enableRaw bool
expectError bool
expectedCondition metav1.Condition
budget *int64
}{
{
name: "deployment available",
object: unstrctureObject(deploymentJson),
rule: workapiv1.ConditionRule{
Type: workapiv1.CelConditionExpressionsType,
Condition: "Available",
CelExpressions: []string{
`object.status.conditions.exists(c, c.type == "Available" && c.status == "True")`,
},
MessageExpression: `result ? "Deployment available" : "Deployment unavailable"`,
},
expectError: false,
expectedCondition: metav1.Condition{
Type: "Available",
Status: metav1.ConditionTrue,
Reason: workapiv1.ConditionRuleEvaluated,
Message: "Deployment available",
},
},
{
name: "wrong return type",
object: unstrctureObject(deploymentJson),
rule: workapiv1.ConditionRule{
Type: workapiv1.CelConditionExpressionsType,
Condition: "Available",
CelExpressions: []string{
`object.status.conditions.filter(c, c.type == "Available")[0].status`,
},
},
expectError: true,
expectedCondition: metav1.Condition{
Type: "Available",
Reason: workapiv1.ConditionRuleExpressionError,
Status: metav1.ConditionFalse,
Message: "expected bool result, got string",
},
},
{
name: "invalid CEL",
object: unstrctureObject(deploymentJson),
rule: workapiv1.ConditionRule{
Type: workapiv1.CelConditionExpressionsType,
Condition: "Available",
CelExpressions: []string{`object.missing`},
},
expectError: true,
expectedCondition: metav1.Condition{
Type: "Available",
Status: metav1.ConditionFalse,
Reason: workapiv1.ConditionRuleExpressionError,
Message: "no such key: missing",
},
},
{
name: "invalid message CEL",
object: unstrctureObject(deploymentJson),
rule: workapiv1.ConditionRule{
Type: workapiv1.CelConditionExpressionsType,
Condition: "Available",
CelExpressions: []string{`true`},
MessageExpression: `badcel`,
},
expectError: true,
expectedCondition: metav1.Condition{
Type: "Available",
Status: metav1.ConditionTrue,
Reason: workapiv1.ConditionRuleEvaluated,
Message: "ERROR: <input>:1:1: undeclared reference to 'badcel' (in container '')",
},
},
{
name: "cel lib function hasConditions",
object: unstrctureObject(deploymentJson),
rule: workapiv1.ConditionRule{
Type: workapiv1.CelConditionExpressionsType,
Condition: "HasConditions",
CelExpressions: []string{
`hasConditions(object.status)
&& !hasConditions({})
&& !hasConditions("badtype")
&& !hasConditions({"conditions": []})
&& !hasConditions({"conditions": null})`,
},
Message: "should work",
},
expectError: false,
expectedCondition: metav1.Condition{
Type: "HasConditions",
Status: metav1.ConditionTrue,
Reason: workapiv1.ConditionRuleEvaluated,
Message: "should work",
},
},
{
name: "Job complete",
object: unstrctureObject(jobJsonComplete),
rule: workapiv1.ConditionRule{Type: workapiv1.WellKnownConditionsType, Condition: workapiv1.ManifestComplete},
expectedCondition: metav1.Condition{
Type: workapiv1.ManifestComplete,
Status: metav1.ConditionTrue,
Reason: workapiv1.ConditionRuleEvaluated,
Message: "Job is finished",
},
},
{
name: "Job failed",
object: unstrctureObject(jobJsonFailed),
rule: workapiv1.ConditionRule{Type: workapiv1.WellKnownConditionsType, Condition: workapiv1.ManifestComplete},
expectedCondition: metav1.Condition{
Type: workapiv1.ManifestComplete,
Status: metav1.ConditionTrue,
Reason: workapiv1.ConditionRuleEvaluated,
Message: "Job is finished",
},
},
{
name: "Job incomplete",
object: unstrctureObject(jobJsonIncomplete),
rule: workapiv1.ConditionRule{Type: workapiv1.WellKnownConditionsType, Condition: workapiv1.ManifestComplete},
expectedCondition: metav1.Condition{
Type: workapiv1.ManifestComplete,
Status: metav1.ConditionFalse,
Reason: workapiv1.ConditionRuleEvaluated,
Message: "Job is not finished",
},
},
{
name: "Pod complete",
object: unstrctureObject(podJsonSucceeded),
rule: workapiv1.ConditionRule{Type: workapiv1.WellKnownConditionsType, Condition: workapiv1.ManifestComplete},
expectedCondition: metav1.Condition{
Type: workapiv1.ManifestComplete,
Status: metav1.ConditionTrue,
Reason: workapiv1.ConditionRuleEvaluated,
Message: "Pod is in phase Succeeded",
},
},
{
name: "Pod failed",
object: unstrctureObject(podJsonFailed),
rule: workapiv1.ConditionRule{Type: workapiv1.WellKnownConditionsType, Condition: workapiv1.ManifestComplete},
expectedCondition: metav1.Condition{
Type: workapiv1.ManifestComplete,
Status: metav1.ConditionTrue,
Reason: workapiv1.ConditionRuleEvaluated,
Message: "Pod is in phase Failed",
},
},
{
name: "Pod running",
object: unstrctureObject(podJsonRunning),
rule: workapiv1.ConditionRule{Type: workapiv1.WellKnownConditionsType, Condition: workapiv1.ManifestComplete},
expectedCondition: metav1.Condition{
Type: workapiv1.ManifestComplete,
Status: metav1.ConditionFalse,
Reason: workapiv1.ConditionRuleEvaluated,
Message: "Pod is in phase Running",
},
},
{
name: "Budget exceeded",
object: unstrctureObject(podJsonRunning),
rule: workapiv1.ConditionRule{Type: workapiv1.WellKnownConditionsType, Condition: workapiv1.ManifestComplete},
expectError: true,
expectedCondition: metav1.Condition{
Type: workapiv1.ManifestComplete,
Status: metav1.ConditionFalse,
Reason: workapiv1.ConditionRuleExpressionError,
Message: "CEL evaluation budget exceeded",
},
budget: ptr.To(int64(1)),
},
}
reader, err := NewConditionReader()
if err != nil {
t.Fatalf("Expected no err when creating ConditionReader but got %v", err)
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
var budget int64
if c.budget != nil {
budget = *c.budget
} else {
budget = int64(1000)
}
condition, remainingBudget, err := reader.GetConditionByRule(context.TODO(), c.object, c.rule, budget)
if err == nil && c.expectError {
t.Errorf("%s: Expect error but got no error", c.name)
}
if err != nil && !c.expectError {
t.Errorf("%s: Expect no error but got %v", c.name, err)
}
if !apiequality.Semantic.DeepEqual(c.expectedCondition, condition) {
t.Errorf("%s: Expect condition %+v, but got %+v", c.name, c.expectedCondition, condition)
}
if err == nil && remainingBudget >= budget {
t.Errorf("%s: Expect remaining budget to be less than initial budget. Budget: %d, Remaining: %d", c.name, budget, remainingBudget)
}
})
}
}