Files
open-cluster-management/pkg/work/spoke/controllers/statuscontroller/availablestatus_controller_test.go
Ben Perry c5e776cdd9
Some checks failed
Scorecard supply-chain security / Scorecard analysis (push) Failing after 1m58s
Post / coverage (push) Failing after 36m24s
Post / images (amd64) (push) Failing after 9m7s
Post / images (arm64) (push) Failing after 8m30s
Post / image manifest (push) Has been skipped
Post / trigger clusteradm e2e (push) Has been skipped
Close stale issues and PRs / stale (push) Successful in 57s
Manifest completion (#1033)
* Skip manifests in work reconcile that are marked Complete

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

* Aggregate Complete condition to work from manifests

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

* Delete work that is complete and satisfies configured TTL

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

* tests

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

* lint

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

* go.mod

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

* Helper funcs for conditions

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

* Generic condition aggregation

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

* Support integration test args

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

* Remove work deletion from spoke, will be moved to hub GC

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

* Cleanup

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

* update api

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

* Wait for NS to exist before testing

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

---------

Signed-off-by: Ben Perry <bhperry94@gmail.com>
2025-07-14 04:53:04 +00:00

803 lines
28 KiB
Go

package statuscontroller
import (
"context"
"encoding/json"
"testing"
"github.com/davecgh/go-spew/spew"
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
fakedynamic "k8s.io/client-go/dynamic/fake"
clienttesting "k8s.io/client-go/testing"
"k8s.io/utils/pointer"
fakeworkclient "open-cluster-management.io/api/client/work/clientset/versioned/fake"
ocmfeature "open-cluster-management.io/api/feature"
workapiv1 "open-cluster-management.io/api/work/v1"
"open-cluster-management.io/sdk-go/pkg/patcher"
testingcommon "open-cluster-management.io/ocm/pkg/common/testing"
"open-cluster-management.io/ocm/pkg/features"
"open-cluster-management.io/ocm/pkg/work/spoke/conditions"
"open-cluster-management.io/ocm/pkg/work/spoke/spoketesting"
"open-cluster-management.io/ocm/pkg/work/spoke/statusfeedback"
"open-cluster-management.io/ocm/test/integration/util"
)
func TestSyncManifestWork(t *testing.T) {
cases := []struct {
name string
existingResources []runtime.Object
manifests []workapiv1.ManifestCondition
workConditions []metav1.Condition
validateActions func(t *testing.T, actions []clienttesting.Action)
}{
{
name: "unknown available status from work whose manifests become empty",
workConditions: []metav1.Condition{
{
Type: workapiv1.WorkApplied,
},
{
Type: workapiv1.WorkAvailable,
},
},
validateActions: func(t *testing.T, actions []clienttesting.Action) {
testingcommon.AssertActions(t, actions, "patch")
p := actions[0].(clienttesting.PatchActionImpl).Patch
work := &workapiv1.ManifestWork{}
if err := json.Unmarshal(p, work); err != nil {
t.Fatal(err)
}
if !hasStatusCondition(work.Status.Conditions, workapiv1.WorkAvailable, metav1.ConditionUnknown) {
t.Fatal(spew.Sdump(work.Status.Conditions))
}
},
},
{
name: "No work applied condition",
existingResources: []runtime.Object{
testingcommon.NewUnstructuredSecret("ns1", "n1", false, "ns1-n1"),
},
manifests: []workapiv1.ManifestCondition{
newManifestWthCondition("", "v1", "secrets", "ns1", "n1"),
},
validateActions: testingcommon.AssertNoActions,
},
{
name: "Do not update if existing conditions are correct",
existingResources: []runtime.Object{
testingcommon.NewUnstructuredSecret("ns1", "n1", false, "ns1-n1"),
},
manifests: []workapiv1.ManifestCondition{
newManifestWthCondition("", "v1", "secrets", "ns1", "n1"),
},
workConditions: []metav1.Condition{
{
Type: workapiv1.WorkApplied,
},
{
Type: workapiv1.WorkAvailable,
Status: metav1.ConditionTrue,
Reason: "ResourcesAvailable",
Message: "All resources are available",
},
},
validateActions: testingcommon.AssertNoActions,
},
{
name: "build status with existing resource",
existingResources: []runtime.Object{
testingcommon.NewUnstructuredSecret("ns1", "n1", false, "ns1-n1"),
},
manifests: []workapiv1.ManifestCondition{
newManifest("", "v1", "secrets", "ns1", "n1"),
},
workConditions: []metav1.Condition{
{
Type: workapiv1.WorkApplied,
},
},
validateActions: func(t *testing.T, actions []clienttesting.Action) {
testingcommon.AssertActions(t, actions, "patch")
p := actions[0].(clienttesting.PatchActionImpl).Patch
work := &workapiv1.ManifestWork{}
if err := json.Unmarshal(p, work); err != nil {
t.Fatal(err)
}
if len(work.Status.ResourceStatus.Manifests) != 1 {
t.Fatal(spew.Sdump(work.Status.ResourceStatus.Manifests))
}
if !hasStatusCondition(work.Status.ResourceStatus.Manifests[0].Conditions, workapiv1.ManifestAvailable, metav1.ConditionTrue) {
t.Fatal(spew.Sdump(work.Status.ResourceStatus.Manifests[0].Conditions))
}
if !hasStatusCondition(work.Status.Conditions, workapiv1.WorkAvailable, metav1.ConditionTrue) {
t.Fatal(spew.Sdump(work.Status.Conditions))
}
},
},
{
name: "build status when one of resources does not exists",
existingResources: []runtime.Object{
testingcommon.NewUnstructuredSecret("ns1", "n1", false, "ns1-n1"),
},
manifests: []workapiv1.ManifestCondition{
newManifest("", "v1", "secrets", "ns1", "n1"),
newManifest("", "v1", "secrets", "ns2", "n2"),
},
workConditions: []metav1.Condition{
{
Type: workapiv1.WorkApplied,
},
},
validateActions: func(t *testing.T, actions []clienttesting.Action) {
testingcommon.AssertActions(t, actions, "patch")
p := actions[0].(clienttesting.PatchActionImpl).Patch
work := &workapiv1.ManifestWork{}
if err := json.Unmarshal(p, work); err != nil {
t.Fatal(err)
}
if !hasStatusCondition(work.Status.ResourceStatus.Manifests[0].Conditions, workapiv1.ManifestAvailable, metav1.ConditionTrue) {
t.Fatal(spew.Sdump(work.Status.ResourceStatus.Manifests[0].Conditions))
}
if !hasStatusCondition(work.Status.ResourceStatus.Manifests[1].Conditions, workapiv1.ManifestAvailable, metav1.ConditionFalse) {
t.Fatal(spew.Sdump(work.Status.ResourceStatus.Manifests[1].Conditions))
}
if !hasStatusCondition(work.Status.Conditions, workapiv1.WorkAvailable, metav1.ConditionFalse) {
t.Fatal(spew.Sdump(work.Status.Conditions))
}
},
},
{
name: "build status when one of resosurce has incompleted meta",
existingResources: []runtime.Object{
testingcommon.NewUnstructuredSecret("ns1", "n1", false, "ns1-n1"),
},
manifests: []workapiv1.ManifestCondition{
newManifest("", "v1", "secrets", "ns1", "n1"),
newManifest("", "", "", "", ""),
},
workConditions: []metav1.Condition{
{
Type: workapiv1.WorkApplied,
},
},
validateActions: func(t *testing.T, actions []clienttesting.Action) {
testingcommon.AssertActions(t, actions, "patch")
p := actions[0].(clienttesting.PatchActionImpl).Patch
work := &workapiv1.ManifestWork{}
if err := json.Unmarshal(p, work); err != nil {
t.Fatal(err)
}
if len(work.Status.ResourceStatus.Manifests) != 2 {
t.Fatal(spew.Sdump(work.Status.ResourceStatus.Manifests))
}
if !hasStatusCondition(work.Status.ResourceStatus.Manifests[0].Conditions, workapiv1.ManifestAvailable, metav1.ConditionTrue) {
t.Fatal(spew.Sdump(work.Status.ResourceStatus.Manifests[0].Conditions))
}
if !hasStatusCondition(work.Status.ResourceStatus.Manifests[1].Conditions, workapiv1.ManifestAvailable, metav1.ConditionUnknown) {
t.Fatal(spew.Sdump(work.Status.ResourceStatus.Manifests[1].Conditions))
}
if !hasStatusCondition(work.Status.Conditions, workapiv1.WorkAvailable, metav1.ConditionUnknown) {
t.Fatal(spew.Sdump(work.Status.Conditions))
}
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
testingWork, _ := spoketesting.NewManifestWork(0)
testingWork.Finalizers = []string{workapiv1.ManifestWorkFinalizer}
testingWork.Status = workapiv1.ManifestWorkStatus{
Conditions: c.workConditions,
ResourceStatus: workapiv1.ManifestResourceStatus{
Manifests: c.manifests,
},
}
fakeClient := fakeworkclient.NewSimpleClientset(testingWork)
fakeDynamicClient := fakedynamic.NewSimpleDynamicClient(runtime.NewScheme(), c.existingResources...)
controller := AvailableStatusController{
spokeDynamicClient: fakeDynamicClient,
patcher: patcher.NewPatcher[
*workapiv1.ManifestWork, workapiv1.ManifestWorkSpec, workapiv1.ManifestWorkStatus](
fakeClient.WorkV1().ManifestWorks(testingWork.Namespace)),
}
err := controller.syncManifestWork(context.TODO(), testingWork)
if err != nil {
t.Fatal(err)
}
c.validateActions(t, fakeClient.Actions())
})
}
}
func TestStatusFeedback(t *testing.T) {
utilruntime.Must(features.SpokeMutableFeatureGate.Add(ocmfeature.DefaultSpokeWorkFeatureGates))
cases := []struct {
name string
existingResources []runtime.Object
configOption []workapiv1.ManifestConfigOption
manifests []workapiv1.ManifestCondition
validateActions func(t *testing.T, actions []clienttesting.Action)
}{
{
name: "resource identifier is not matched",
existingResources: []runtime.Object{
testingcommon.NewUnstructuredSecret("ns1", "n1", false, "ns1-n1"),
},
configOption: []workapiv1.ManifestConfigOption{
{
ResourceIdentifier: workapiv1.ResourceIdentifier{Group: "apps", Resource: "deployments", Name: "deploy1", Namespace: "ns1"},
},
},
manifests: []workapiv1.ManifestCondition{
newManifest("", "v1", "secrets", "ns1", "n1"),
},
validateActions: func(t *testing.T, actions []clienttesting.Action) {
testingcommon.AssertActions(t, actions, "patch")
p := actions[0].(clienttesting.PatchActionImpl).Patch
work := &workapiv1.ManifestWork{}
if err := json.Unmarshal(p, work); err != nil {
t.Fatal(err)
}
if len(work.Status.ResourceStatus.Manifests) != 1 {
t.Fatal(spew.Sdump(work.Status.ResourceStatus.Manifests))
}
if len(work.Status.ResourceStatus.Manifests[0].StatusFeedbacks.Values) != 0 {
t.Fatal(spew.Sdump(work.Status.ResourceStatus.Manifests[0].StatusFeedbacks.Values))
}
if !hasStatusCondition(work.Status.ResourceStatus.Manifests[0].Conditions, statusFeedbackConditionType, metav1.ConditionTrue) {
t.Fatal(spew.Sdump(work.Status.ResourceStatus.Manifests[0].Conditions))
}
},
},
{
name: "get well known status",
existingResources: []runtime.Object{
testingcommon.NewUnstructuredSecret("ns1", "n1", false, "ns1-n1"),
testingcommon.NewUnstructuredWithContent("apps/v1", "Deployment", "ns1", "deploy1",
map[string]interface{}{
"status": map[string]interface{}{"readyReplicas": int64(2), "replicas": int64(3), "availableReplicas": int64(2)},
}),
},
configOption: []workapiv1.ManifestConfigOption{
{
ResourceIdentifier: workapiv1.ResourceIdentifier{Group: "apps", Resource: "deployments", Name: "*", Namespace: "ns*"},
FeedbackRules: []workapiv1.FeedbackRule{{Type: workapiv1.WellKnownStatusType}},
},
},
manifests: []workapiv1.ManifestCondition{
newManifest("", "v1", "secrets", "ns1", "n1"),
newManifest("apps", "v1", "deployments", "ns1", "deploy1"),
},
validateActions: func(t *testing.T, actions []clienttesting.Action) {
testingcommon.AssertActions(t, actions, "patch")
p := actions[0].(clienttesting.PatchActionImpl).Patch
work := &workapiv1.ManifestWork{}
if err := json.Unmarshal(p, work); err != nil {
t.Fatal(err)
}
if len(work.Status.ResourceStatus.Manifests) != 2 {
t.Fatal(spew.Sdump(work.Status.ResourceStatus.Manifests))
}
expectedValues := []workapiv1.FeedbackValue{
{
Name: "ReadyReplicas",
Value: workapiv1.FieldValue{
Type: workapiv1.Integer,
Integer: pointer.Int64(2),
},
},
{
Name: "Replicas",
Value: workapiv1.FieldValue{
Type: workapiv1.Integer,
Integer: pointer.Int64(3),
},
},
{
Name: "AvailableReplicas",
Value: workapiv1.FieldValue{
Type: workapiv1.Integer,
Integer: pointer.Int64(2),
},
},
}
if !equality.Semantic.DeepEqual(work.Status.ResourceStatus.Manifests[1].StatusFeedbacks.Values, expectedValues) {
t.Fatal(spew.Sdump(work.Status.ResourceStatus.Manifests[1].StatusFeedbacks.Values))
}
if !hasStatusCondition(work.Status.ResourceStatus.Manifests[1].Conditions, statusFeedbackConditionType, metav1.ConditionTrue) {
t.Fatal(spew.Sdump(work.Status.ResourceStatus.Manifests[1].Conditions))
}
},
},
{
name: "get wrong json path",
existingResources: []runtime.Object{
testingcommon.NewUnstructuredWithContent("apps/v1", "Deployment", "ns1", "deploy1",
map[string]interface{}{
"status": map[string]interface{}{
"replicas": int64(3),
"conditions": map[string]interface{}{"status": "True"},
},
}),
},
configOption: []workapiv1.ManifestConfigOption{
{
ResourceIdentifier: workapiv1.ResourceIdentifier{Group: "apps", Resource: "deployments", Name: "deploy1", Namespace: "*"},
FeedbackRules: []workapiv1.FeedbackRule{
{
Type: workapiv1.WellKnownStatusType,
},
{
Type: workapiv1.JSONPathsType,
JsonPaths: []workapiv1.JsonPath{
{
Name: "condition",
Path: ".status.conditions",
},
},
},
},
},
},
manifests: []workapiv1.ManifestCondition{
newManifest("apps", "v1", "deployments", "ns1", "deploy1"),
},
validateActions: func(t *testing.T, actions []clienttesting.Action) {
testingcommon.AssertActions(t, actions, "patch")
p := actions[0].(clienttesting.PatchActionImpl).Patch
work := &workapiv1.ManifestWork{}
if err := json.Unmarshal(p, work); err != nil {
t.Fatal(err)
}
if len(work.Status.ResourceStatus.Manifests) != 1 {
t.Fatal(spew.Sdump(work.Status.ResourceStatus.Manifests))
}
expectedValues := []workapiv1.FeedbackValue{
{
Name: "Replicas",
Value: workapiv1.FieldValue{
Type: workapiv1.Integer,
Integer: pointer.Int64(3),
},
},
}
if !equality.Semantic.DeepEqual(work.Status.ResourceStatus.Manifests[0].StatusFeedbacks.Values, expectedValues) {
t.Fatal(spew.Sdump(work.Status.ResourceStatus.Manifests[0].StatusFeedbacks.Values))
}
if !hasStatusCondition(work.Status.ResourceStatus.Manifests[0].Conditions, statusFeedbackConditionType, metav1.ConditionFalse) {
t.Fatal(spew.Sdump(work.Status.ResourceStatus.Manifests[0].Conditions))
}
},
},
{
name: "one option matches multi resources",
existingResources: []runtime.Object{
testingcommon.NewUnstructuredSecret("ns1", "n1", false, "ns1-n1"),
testingcommon.NewUnstructuredWithContent("apps/v1", "Deployment", "ns1", "deploy1",
map[string]interface{}{
"status": map[string]interface{}{"readyReplicas": int64(2), "replicas": int64(3), "availableReplicas": int64(2)},
}),
testingcommon.NewUnstructuredWithContent("apps/v1", "Deployment", "ns2", "deploy2",
map[string]interface{}{
"status": map[string]interface{}{"readyReplicas": int64(2), "replicas": int64(3), "availableReplicas": int64(2)},
}),
},
configOption: []workapiv1.ManifestConfigOption{
{
ResourceIdentifier: workapiv1.ResourceIdentifier{Group: "apps", Resource: "deployments", Name: "*", Namespace: "ns*"},
FeedbackRules: []workapiv1.FeedbackRule{{Type: workapiv1.WellKnownStatusType}},
},
},
manifests: []workapiv1.ManifestCondition{
newManifest("", "v1", "secrets", "ns1", "n1"),
newManifest("apps", "v1", "deployments", "ns1", "deploy1"),
newManifest("apps", "v1", "deployments", "ns2", "deploy2"),
},
validateActions: func(t *testing.T, actions []clienttesting.Action) {
testingcommon.AssertActions(t, actions, "patch")
p := actions[0].(clienttesting.PatchActionImpl).Patch
work := &workapiv1.ManifestWork{}
if err := json.Unmarshal(p, work); err != nil {
t.Fatal(err)
}
if len(work.Status.ResourceStatus.Manifests) != 3 {
t.Fatal(spew.Sdump(work.Status.ResourceStatus.Manifests))
}
expectedValues := []workapiv1.FeedbackValue{
{
Name: "ReadyReplicas",
Value: workapiv1.FieldValue{
Type: workapiv1.Integer,
Integer: pointer.Int64(2),
},
},
{
Name: "Replicas",
Value: workapiv1.FieldValue{
Type: workapiv1.Integer,
Integer: pointer.Int64(3),
},
},
{
Name: "AvailableReplicas",
Value: workapiv1.FieldValue{
Type: workapiv1.Integer,
Integer: pointer.Int64(2),
},
},
}
for i := 1; i < len(work.Status.ResourceStatus.Manifests); i++ {
if !equality.Semantic.DeepEqual(work.Status.ResourceStatus.Manifests[i].StatusFeedbacks.Values, expectedValues) {
t.Fatal(spew.Sdump(work.Status.ResourceStatus.Manifests[i].StatusFeedbacks.Values))
}
if !hasStatusCondition(work.Status.ResourceStatus.Manifests[i].Conditions, statusFeedbackConditionType, metav1.ConditionTrue) {
t.Fatal(spew.Sdump(work.Status.ResourceStatus.Manifests[i].Conditions))
}
}
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
testingWork, _ := spoketesting.NewManifestWork(0)
testingWork.Finalizers = []string{workapiv1.ManifestWorkFinalizer}
testingWork.Spec.ManifestConfigs = c.configOption
testingWork.Status = workapiv1.ManifestWorkStatus{
ResourceStatus: workapiv1.ManifestResourceStatus{
Manifests: c.manifests,
},
Conditions: []metav1.Condition{
{Type: workapiv1.WorkApplied},
},
}
fakeClient := fakeworkclient.NewSimpleClientset(testingWork)
fakeDynamicClient := fakedynamic.NewSimpleDynamicClient(runtime.NewScheme(), c.existingResources...)
controller := AvailableStatusController{
spokeDynamicClient: fakeDynamicClient,
statusReader: statusfeedback.NewStatusReader(),
patcher: patcher.NewPatcher[
*workapiv1.ManifestWork, workapiv1.ManifestWorkSpec, workapiv1.ManifestWorkStatus](
fakeClient.WorkV1().ManifestWorks(testingWork.Namespace)),
}
err := controller.syncManifestWork(context.TODO(), testingWork)
if err != nil {
t.Fatal(err)
}
c.validateActions(t, fakeClient.Actions())
})
}
}
func TestConditionRules(t *testing.T) {
activeRule := workapiv1.ConditionRule{
Type: workapiv1.CelConditionExpressionsType,
Condition: "Active",
CelExpressions: []string{
"has(object.status) && hasConditions(object.status) ? object.status.conditions.exists(c, c.type == 'Active' && c.status == 'True') : false",
},
MessageExpression: "result ? 'NewObject is active' : 'NewObject is not active'",
}
cases := []struct {
name string
existingResources []runtime.Object
existingConditions []metav1.Condition
configOption []workapiv1.ManifestConfigOption
manifests []workapiv1.ManifestCondition
expectedManifestConditions [][]metav1.Condition
expectedWorkConditions []metav1.Condition
}{
{
name: "condition rule successful evaluation",
existingResources: []runtime.Object{
testingcommon.NewUnstructuredWithContent(
"v1", "NewObject", "ns1", "n1",
map[string]any{
"spec": map[string]any{"key1": "val1"},
"status": map[string]any{
"conditions": []any{
map[string]any{"type": "Active", "status": "True"},
},
}}),
testingcommon.NewUnstructuredWithContent(
"v1", "NewObject", "ns1", "n2",
map[string]any{"spec": map[string]any{"key1": "val2"}}),
},
configOption: []workapiv1.ManifestConfigOption{
{
ResourceIdentifier: workapiv1.ResourceIdentifier{Resource: "newobjects", Namespace: "ns1", Name: "n1"},
ConditionRules: []workapiv1.ConditionRule{activeRule},
},
{
ResourceIdentifier: workapiv1.ResourceIdentifier{Resource: "newobjects", Namespace: "ns1", Name: "n2"},
ConditionRules: []workapiv1.ConditionRule{activeRule},
},
},
manifests: []workapiv1.ManifestCondition{
newManifest("", "v1", "newobjects", "ns1", "n1"),
newManifest("", "v1", "newobjects", "ns1", "n2"),
},
expectedManifestConditions: [][]metav1.Condition{
{{Type: "Active", Status: metav1.ConditionTrue, Reason: workapiv1.ConditionRuleEvaluated, Message: "NewObject is active"}},
{{Type: "Active", Status: metav1.ConditionFalse, Reason: workapiv1.ConditionRuleEvaluated, Message: "NewObject is not active"}},
},
},
{
name: "condition rule invalid expression",
existingResources: []runtime.Object{
testingcommon.NewUnstructuredWithContent(
"v1", "NewObject", "ns1", "n1",
map[string]any{"spec": map[string]any{"key1": "val1"}}),
},
configOption: []workapiv1.ManifestConfigOption{
{
ResourceIdentifier: workapiv1.ResourceIdentifier{Resource: "newobjects", Namespace: "ns1", Name: "n1"},
ConditionRules: []workapiv1.ConditionRule{
{
Type: workapiv1.CelConditionExpressionsType,
Condition: "Active",
CelExpressions: []string{"object.invalid.key == 'Active'"},
},
},
},
},
manifests: []workapiv1.ManifestCondition{
newManifest("", "v1", "newobjects", "ns1", "n1"),
},
expectedManifestConditions: [][]metav1.Condition{
{{Type: "Active", Status: metav1.ConditionFalse, Reason: workapiv1.ConditionRuleExpressionError, Message: "no such key: invalid"}},
},
},
{
name: "condition rule Complete aggregates to work condition",
existingResources: []runtime.Object{
testingcommon.NewUnstructuredWithContent(
"v1", "NewObject", "ns1", "n1",
map[string]any{"spec": map[string]any{"key1": "val1"}}),
},
configOption: []workapiv1.ManifestConfigOption{
{
ResourceIdentifier: workapiv1.ResourceIdentifier{Resource: "newobjects", Namespace: "ns1", Name: "n1"},
ConditionRules: []workapiv1.ConditionRule{
{
Type: workapiv1.CelConditionExpressionsType,
Condition: workapiv1.ManifestComplete,
CelExpressions: []string{"true"},
},
},
},
},
manifests: []workapiv1.ManifestCondition{
newManifest("", "v1", "newobjects", "ns1", "n1"),
},
expectedManifestConditions: [][]metav1.Condition{
{{Type: workapiv1.ManifestComplete, Status: metav1.ConditionTrue, Reason: workapiv1.ConditionRuleEvaluated}},
},
expectedWorkConditions: []metav1.Condition{
{Type: workapiv1.WorkComplete, Status: metav1.ConditionTrue, Reason: "ConditionRulesAggregated"},
},
},
{
name: "work Complete requires all completable manifests to Complete",
existingResources: []runtime.Object{
testingcommon.NewUnstructuredWithContent(
"v1", "NewObject", "ns1", "n1",
map[string]any{"spec": map[string]any{"key1": "val1"}}),
testingcommon.NewUnstructuredWithContent(
"v1", "NewObject", "ns1", "n2",
map[string]any{"spec": map[string]any{"key1": "val2"}}),
},
configOption: []workapiv1.ManifestConfigOption{
{
ResourceIdentifier: workapiv1.ResourceIdentifier{Resource: "newobjects", Namespace: "ns1", Name: "n1"},
ConditionRules: []workapiv1.ConditionRule{
{
Type: workapiv1.CelConditionExpressionsType,
Condition: workapiv1.ManifestComplete,
CelExpressions: []string{"true"},
},
},
},
{
ResourceIdentifier: workapiv1.ResourceIdentifier{Resource: "newobjects", Namespace: "ns1", Name: "n2"},
ConditionRules: []workapiv1.ConditionRule{
{
Type: workapiv1.CelConditionExpressionsType,
Condition: workapiv1.ManifestComplete,
CelExpressions: []string{"false"},
},
},
},
},
manifests: []workapiv1.ManifestCondition{
newManifest("", "v1", "newobjects", "ns1", "n1"),
newManifest("", "v1", "newobjects", "ns1", "n2"),
},
expectedManifestConditions: [][]metav1.Condition{
{{Type: workapiv1.ManifestComplete, Status: metav1.ConditionTrue, Reason: workapiv1.ConditionRuleEvaluated}},
{{Type: workapiv1.ManifestComplete, Status: metav1.ConditionFalse, Reason: workapiv1.ConditionRuleEvaluated}},
},
expectedWorkConditions: []metav1.Condition{
{Type: workapiv1.WorkComplete, Status: metav1.ConditionFalse, Reason: "ConditionRulesAggregated"},
},
},
{
name: "remove conditions set by deleted rules",
existingResources: []runtime.Object{
testingcommon.NewUnstructuredWithContent(
"v1", "NewObject", "ns1", "n1",
map[string]any{"spec": map[string]any{"key1": "val1"}}),
},
existingConditions: []metav1.Condition{{Type: workapiv1.WorkComplete, Status: metav1.ConditionTrue, Reason: "ConditionRulesAggregated"}},
configOption: []workapiv1.ManifestConfigOption{
{
ResourceIdentifier: workapiv1.ResourceIdentifier{Resource: "newobjects", Namespace: "ns1", Name: "n1"},
ConditionRules: []workapiv1.ConditionRule{
{
Type: workapiv1.CelConditionExpressionsType,
Condition: "SomeOtherRule",
CelExpressions: []string{"true"},
},
},
},
},
manifests: []workapiv1.ManifestCondition{
newManifest(
"", "v1", "newobjects", "ns1", "n1",
metav1.Condition{Type: workapiv1.ManifestComplete, Status: metav1.ConditionTrue, Reason: workapiv1.ConditionRuleEvaluated},
),
},
expectedManifestConditions: [][]metav1.Condition{
{
{Type: "SomeOtherRule", Status: metav1.ConditionTrue, Reason: workapiv1.ConditionRuleEvaluated},
{Type: workapiv1.ManifestComplete, Status: util.ConditionNotFound},
},
},
expectedWorkConditions: []metav1.Condition{
{Type: workapiv1.WorkComplete, Status: util.ConditionNotFound},
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
testingWork, _ := spoketesting.NewManifestWork(0)
// Deleting conditions from old rules relies on change in ObservedGeneration
// Emualate that by setting generation to something other than 0
testingWork.Generation = 2
testingWork.Finalizers = []string{workapiv1.ManifestWorkFinalizer}
testingWork.Spec.ManifestConfigs = c.configOption
existingCondition := c.existingConditions
if meta.FindStatusCondition(c.existingConditions, workapiv1.WorkApplied) == nil {
existingCondition = append([]metav1.Condition{{Type: workapiv1.WorkApplied}}, existingCondition...)
}
testingWork.Status = workapiv1.ManifestWorkStatus{
ResourceStatus: workapiv1.ManifestResourceStatus{
Manifests: c.manifests,
},
Conditions: existingCondition,
}
fakeClient := fakeworkclient.NewSimpleClientset(testingWork)
fakeDynamicClient := fakedynamic.NewSimpleDynamicClient(runtime.NewScheme(), c.existingResources...)
conditionReader, err := conditions.NewConditionReader()
if err != nil {
t.Fatal(err)
}
controller := AvailableStatusController{
spokeDynamicClient: fakeDynamicClient,
statusReader: statusfeedback.NewStatusReader(),
conditionReader: conditionReader,
patcher: patcher.NewPatcher[
*workapiv1.ManifestWork, workapiv1.ManifestWorkSpec, workapiv1.ManifestWorkStatus](
fakeClient.WorkV1().ManifestWorks(testingWork.Namespace)),
}
err = controller.syncManifestWork(context.TODO(), testingWork)
if err != nil {
t.Fatal(err)
}
// Parse work
actions := fakeClient.Actions()
testingcommon.AssertActions(t, actions, "patch")
p := actions[0].(clienttesting.PatchActionImpl).Patch
work := &workapiv1.ManifestWork{}
if err := json.Unmarshal(p, work); err != nil {
t.Fatal(err)
}
if len(work.Status.ResourceStatus.Manifests) != len(c.manifests) {
t.Fatal(spew.Sdump(work.Status.ResourceStatus.Manifests))
}
// Check expected conditions on manifests and work
conditionsFailed := false
for i, expectedConditions := range c.expectedManifestConditions {
manifestStatus := work.Status.ResourceStatus.Manifests[i]
errs := util.CheckExpectedConditions(manifestStatus.Conditions, expectedConditions...)
if errs != nil {
conditionsFailed = true
for _, err := range errs.Errors() {
t.Errorf("%s: %v", manifestStatus.ResourceMeta.Name, err)
}
}
}
errs := util.CheckExpectedConditions(work.Status.Conditions, c.expectedWorkConditions...)
if errs != nil {
conditionsFailed = true
for _, err := range errs.Errors() {
t.Errorf("ManifestWork: %v", err)
}
}
if conditionsFailed {
t.FailNow()
}
})
}
}
func newManifest(group, version, resource, namespace, name string, conditions ...metav1.Condition) workapiv1.ManifestCondition {
return workapiv1.ManifestCondition{
ResourceMeta: workapiv1.ManifestResourceMeta{
Group: group,
Version: version,
Resource: resource,
Namespace: namespace,
Name: name,
},
Conditions: conditions,
}
}
func newManifestWthCondition(group, version, resource, namespace, name string) workapiv1.ManifestCondition {
cond := newManifest(
group, version, resource, namespace, name,
metav1.Condition{
Type: workapiv1.ManifestAvailable,
Status: metav1.ConditionTrue,
Reason: "ResourceAvailable",
Message: "Resource is available",
},
metav1.Condition{
Type: statusFeedbackConditionType,
Reason: "NoStatusFeedbackSynced",
Status: metav1.ConditionTrue,
},
)
return cond
}
func hasStatusCondition(conditions []metav1.Condition, conditionType string, status metav1.ConditionStatus) bool {
for _, condition := range conditions {
if condition.Type != conditionType {
continue
}
return condition.Status == status
}
return false
}