975 lines
26 KiB
Go
975 lines
26 KiB
Go
/*
|
|
Copyright 2022 The Karmada Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package helper
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
appsv1 "k8s.io/api/apps/v1"
|
|
corev1 "k8s.io/api/core/v1"
|
|
"k8s.io/apimachinery/pkg/api/meta"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/util/sets"
|
|
"k8s.io/client-go/tools/record"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
|
|
|
workv1alpha1 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha1"
|
|
workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2"
|
|
)
|
|
|
|
func TestAggregateResourceBindingWorkStatus(t *testing.T) {
|
|
scheme := setupScheme()
|
|
|
|
// Helper functions to create binding
|
|
createBinding := func(name, bindingID string, clusters []string) *workv1alpha2.ResourceBinding {
|
|
return &workv1alpha2.ResourceBinding{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: "default",
|
|
Name: name,
|
|
Labels: map[string]string{
|
|
workv1alpha2.ResourceBindingPermanentIDLabel: bindingID,
|
|
},
|
|
},
|
|
Spec: workv1alpha2.ResourceBindingSpec{
|
|
Resource: workv1alpha2.ObjectReference{
|
|
APIVersion: "apps/v1",
|
|
Kind: "Deployment",
|
|
Name: "test-deployment",
|
|
Namespace: "default",
|
|
},
|
|
Clusters: func() []workv1alpha2.TargetCluster {
|
|
var targetClusters []workv1alpha2.TargetCluster
|
|
for _, cluster := range clusters {
|
|
targetClusters = append(targetClusters, workv1alpha2.TargetCluster{Name: cluster})
|
|
}
|
|
return targetClusters
|
|
}(),
|
|
},
|
|
}
|
|
}
|
|
|
|
// Helper functions to create work
|
|
createWork := func(name, namespace, bindingID string, applied bool, withStatus bool) *workv1alpha1.Work {
|
|
work := &workv1alpha1.Work{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: namespace,
|
|
Name: name,
|
|
Labels: map[string]string{
|
|
workv1alpha2.ResourceBindingPermanentIDLabel: bindingID,
|
|
},
|
|
},
|
|
Spec: workv1alpha1.WorkSpec{
|
|
Workload: workv1alpha1.WorkloadTemplate{
|
|
Manifests: []workv1alpha1.Manifest{
|
|
{
|
|
RawExtension: runtime.RawExtension{
|
|
Raw: []byte(`{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"name":"test-deployment","namespace":"default"}}`),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
if withStatus {
|
|
status := metav1.ConditionTrue
|
|
if !applied {
|
|
status = metav1.ConditionFalse
|
|
}
|
|
work.Status = workv1alpha1.WorkStatus{
|
|
Conditions: []metav1.Condition{
|
|
{
|
|
Type: workv1alpha1.WorkApplied,
|
|
Status: status,
|
|
Message: "test message",
|
|
},
|
|
},
|
|
ManifestStatuses: []workv1alpha1.ManifestStatus{
|
|
{
|
|
Identifier: workv1alpha1.ResourceIdentifier{
|
|
Group: "apps",
|
|
Version: "v1",
|
|
Kind: "Deployment",
|
|
Name: "test-deployment",
|
|
Namespace: "default",
|
|
},
|
|
Status: &runtime.RawExtension{Raw: []byte(`{"replicas": 3}`)},
|
|
Health: "Healthy",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
return work
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
bindingID string
|
|
binding *workv1alpha2.ResourceBinding
|
|
works []*workv1alpha1.Work
|
|
expectedError bool
|
|
expectedStatus metav1.ConditionStatus
|
|
expectedApplied bool
|
|
expectedEvent string
|
|
}{
|
|
{
|
|
name: "successful single work aggregation",
|
|
bindingID: "test-id-1",
|
|
binding: createBinding("binding-1", "test-id-1", []string{"member1"}),
|
|
works: []*workv1alpha1.Work{
|
|
createWork("work-1", "karmada-es-member1", "test-id-1", true, true),
|
|
},
|
|
expectedError: false,
|
|
expectedStatus: metav1.ConditionTrue,
|
|
expectedApplied: true,
|
|
expectedEvent: "Update ResourceBinding(default/binding-1) with AggregatedStatus successfully",
|
|
},
|
|
{
|
|
name: "work not found",
|
|
bindingID: "test-id-2",
|
|
binding: createBinding("binding-2", "test-id-2", []string{"member1"}),
|
|
works: []*workv1alpha1.Work{},
|
|
expectedError: false,
|
|
expectedStatus: metav1.ConditionFalse,
|
|
expectedApplied: false,
|
|
},
|
|
{
|
|
name: "work not applied",
|
|
bindingID: "test-id-3",
|
|
binding: createBinding("binding-3", "test-id-3", []string{"member1"}),
|
|
works: []*workv1alpha1.Work{
|
|
createWork("work-3", "karmada-es-member1", "test-id-3", false, true),
|
|
},
|
|
expectedError: false,
|
|
expectedStatus: metav1.ConditionFalse,
|
|
expectedApplied: false,
|
|
},
|
|
{
|
|
name: "multiple works for different clusters",
|
|
bindingID: "test-id-4",
|
|
binding: createBinding("binding-4", "test-id-4", []string{"member1", "member2"}),
|
|
works: []*workv1alpha1.Work{
|
|
createWork("work-4-1", "karmada-es-member1", "test-id-4", true, true),
|
|
createWork("work-4-2", "karmada-es-member2", "test-id-4", true, true),
|
|
},
|
|
expectedError: false,
|
|
expectedStatus: metav1.ConditionTrue,
|
|
expectedApplied: true,
|
|
},
|
|
{
|
|
name: "work without status",
|
|
bindingID: "test-id-5",
|
|
binding: createBinding("binding-5", "test-id-5", []string{"member1"}),
|
|
works: []*workv1alpha1.Work{
|
|
createWork("work-5", "karmada-es-member1", "test-id-5", true, false),
|
|
},
|
|
expectedError: false,
|
|
expectedStatus: metav1.ConditionFalse,
|
|
expectedApplied: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
objects := []client.Object{tt.binding}
|
|
for _, work := range tt.works {
|
|
objects = append(objects, work)
|
|
}
|
|
|
|
c := fake.NewClientBuilder().
|
|
WithScheme(scheme).
|
|
WithIndex(
|
|
&workv1alpha1.Work{},
|
|
workv1alpha2.ResourceBindingPermanentIDLabel,
|
|
IndexerFuncBasedOnLabel(workv1alpha2.ResourceBindingPermanentIDLabel),
|
|
).
|
|
WithObjects(objects...).
|
|
WithStatusSubresource(tt.binding).
|
|
Build()
|
|
|
|
recorder := record.NewFakeRecorder(10)
|
|
|
|
err := AggregateResourceBindingWorkStatus(context.TODO(), c, tt.binding, recorder)
|
|
if tt.expectedError {
|
|
assert.Error(t, err)
|
|
return
|
|
}
|
|
assert.NoError(t, err)
|
|
|
|
// Verify updated binding
|
|
updatedBinding := &workv1alpha2.ResourceBinding{}
|
|
err = c.Get(context.TODO(), client.ObjectKey{Namespace: tt.binding.Namespace, Name: tt.binding.Name}, updatedBinding)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify conditions
|
|
fullyAppliedCond := meta.FindStatusCondition(updatedBinding.Status.Conditions, workv1alpha2.FullyApplied)
|
|
if assert.NotNil(t, fullyAppliedCond) {
|
|
assert.Equal(t, tt.expectedStatus, fullyAppliedCond.Status)
|
|
}
|
|
|
|
// Verify aggregated status
|
|
if tt.works != nil && len(tt.works) > 0 {
|
|
assert.Len(t, updatedBinding.Status.AggregatedStatus, len(tt.works))
|
|
for _, status := range updatedBinding.Status.AggregatedStatus {
|
|
assert.Equal(t, tt.expectedApplied, status.Applied)
|
|
}
|
|
}
|
|
|
|
// Verify events if expected
|
|
if tt.expectedEvent != "" {
|
|
select {
|
|
case event := <-recorder.Events:
|
|
assert.Contains(t, event, tt.expectedEvent)
|
|
default:
|
|
t.Error("Expected event not received")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAggregateClusterResourceBindingWorkStatus(t *testing.T) {
|
|
scheme := setupScheme()
|
|
|
|
// Helper function to create cluster binding
|
|
createClusterBinding := func(name, bindingID string, clusters []string) *workv1alpha2.ClusterResourceBinding {
|
|
return &workv1alpha2.ClusterResourceBinding{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
Labels: map[string]string{
|
|
workv1alpha2.ClusterResourceBindingPermanentIDLabel: bindingID,
|
|
},
|
|
},
|
|
Spec: workv1alpha2.ResourceBindingSpec{
|
|
Resource: workv1alpha2.ObjectReference{
|
|
APIVersion: "apps/v1",
|
|
Kind: "Deployment",
|
|
Name: "test-deployment",
|
|
},
|
|
Clusters: func() []workv1alpha2.TargetCluster {
|
|
var targetClusters []workv1alpha2.TargetCluster
|
|
for _, cluster := range clusters {
|
|
targetClusters = append(targetClusters, workv1alpha2.TargetCluster{Name: cluster})
|
|
}
|
|
return targetClusters
|
|
}(),
|
|
},
|
|
}
|
|
}
|
|
|
|
// Helper function to create work
|
|
createWork := func(name, namespace, bindingID string, applied bool, withStatus bool) *workv1alpha1.Work {
|
|
workObj := &workv1alpha1.Work{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: namespace,
|
|
Name: name,
|
|
Labels: map[string]string{
|
|
workv1alpha2.ClusterResourceBindingPermanentIDLabel: bindingID,
|
|
},
|
|
},
|
|
Spec: workv1alpha1.WorkSpec{
|
|
Workload: workv1alpha1.WorkloadTemplate{
|
|
Manifests: []workv1alpha1.Manifest{
|
|
{
|
|
RawExtension: runtime.RawExtension{
|
|
Raw: []byte(`{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"name":"test-deployment"}}`),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
if withStatus {
|
|
status := metav1.ConditionTrue
|
|
if !applied {
|
|
status = metav1.ConditionFalse
|
|
}
|
|
workObj.Status = workv1alpha1.WorkStatus{
|
|
Conditions: []metav1.Condition{
|
|
{
|
|
Type: workv1alpha1.WorkApplied,
|
|
Status: status,
|
|
Message: "test message",
|
|
},
|
|
},
|
|
ManifestStatuses: []workv1alpha1.ManifestStatus{
|
|
{
|
|
Identifier: workv1alpha1.ResourceIdentifier{
|
|
Group: "apps",
|
|
Version: "v1",
|
|
Kind: "Deployment",
|
|
Name: "test-deployment",
|
|
},
|
|
Status: &runtime.RawExtension{Raw: []byte(`{"replicas": 3}`)},
|
|
Health: "Healthy",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
return workObj
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
bindingID string
|
|
binding *workv1alpha2.ClusterResourceBinding
|
|
works []*workv1alpha1.Work
|
|
expectedError bool
|
|
expectedStatus metav1.ConditionStatus
|
|
expectedApplied bool
|
|
expectedEvent string
|
|
}{
|
|
{
|
|
name: "successful single work aggregation",
|
|
bindingID: "test-id-1",
|
|
binding: createClusterBinding("binding-1", "test-id-1", []string{"member1"}),
|
|
works: []*workv1alpha1.Work{
|
|
createWork("work-1", "karmada-es-member1", "test-id-1", true, true),
|
|
},
|
|
expectedError: false,
|
|
expectedStatus: metav1.ConditionTrue,
|
|
expectedApplied: true,
|
|
expectedEvent: "Update ClusterResourceBinding(binding-1) with AggregatedStatus successfully",
|
|
},
|
|
{
|
|
name: "no works found",
|
|
bindingID: "test-id-2",
|
|
binding: createClusterBinding("binding-2", "test-id-2", []string{"member1"}),
|
|
works: []*workv1alpha1.Work{},
|
|
expectedError: false,
|
|
expectedStatus: metav1.ConditionFalse,
|
|
expectedApplied: false,
|
|
},
|
|
{
|
|
name: "work not applied",
|
|
bindingID: "test-id-3",
|
|
binding: createClusterBinding("binding-3", "test-id-3", []string{"member1"}),
|
|
works: []*workv1alpha1.Work{
|
|
createWork("work-3", "karmada-es-member1", "test-id-3", false, true),
|
|
},
|
|
expectedError: false,
|
|
expectedStatus: metav1.ConditionFalse,
|
|
expectedApplied: false,
|
|
},
|
|
{
|
|
name: "multiple clusters with mixed status",
|
|
bindingID: "test-id-4",
|
|
binding: createClusterBinding("binding-4", "test-id-4", []string{"member1", "member2"}),
|
|
works: []*workv1alpha1.Work{
|
|
createWork("work-4-1", "karmada-es-member1", "test-id-4", true, true),
|
|
createWork("work-4-2", "karmada-es-member2", "test-id-4", false, true),
|
|
},
|
|
expectedError: false,
|
|
expectedStatus: metav1.ConditionFalse,
|
|
expectedApplied: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
objects := []client.Object{tt.binding}
|
|
for _, work := range tt.works {
|
|
objects = append(objects, work)
|
|
}
|
|
|
|
c := fake.NewClientBuilder().
|
|
WithScheme(scheme).
|
|
WithIndex(
|
|
&workv1alpha1.Work{},
|
|
workv1alpha2.ClusterResourceBindingPermanentIDLabel,
|
|
IndexerFuncBasedOnLabel(workv1alpha2.ClusterResourceBindingPermanentIDLabel),
|
|
).
|
|
WithObjects(objects...).
|
|
WithStatusSubresource(tt.binding).
|
|
Build()
|
|
|
|
recorder := record.NewFakeRecorder(10)
|
|
|
|
err := AggregateClusterResourceBindingWorkStatus(context.TODO(), c, tt.binding, recorder)
|
|
if tt.expectedError {
|
|
assert.Error(t, err)
|
|
return
|
|
}
|
|
assert.NoError(t, err)
|
|
|
|
// Verify updated binding
|
|
updatedBinding := &workv1alpha2.ClusterResourceBinding{}
|
|
err = c.Get(context.TODO(), client.ObjectKey{Name: tt.binding.Name}, updatedBinding)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify conditions
|
|
fullyAppliedCond := meta.FindStatusCondition(updatedBinding.Status.Conditions, workv1alpha2.FullyApplied)
|
|
if assert.NotNil(t, fullyAppliedCond) {
|
|
assert.Equal(t, tt.expectedStatus, fullyAppliedCond.Status)
|
|
}
|
|
|
|
// Verify aggregated status
|
|
if tt.works != nil && len(tt.works) > 0 {
|
|
assert.Len(t, updatedBinding.Status.AggregatedStatus, len(tt.works))
|
|
// For multiple clusters case, verify specific cluster status
|
|
for _, status := range updatedBinding.Status.AggregatedStatus {
|
|
if strings.Contains(status.ClusterName, "member2") {
|
|
assert.Equal(t, tt.expectedApplied, status.Applied)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Verify events
|
|
if tt.expectedEvent != "" {
|
|
select {
|
|
case event := <-recorder.Events:
|
|
assert.Contains(t, event, tt.expectedEvent)
|
|
default:
|
|
t.Error("Expected event not received")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildStatusRawExtension(t *testing.T) {
|
|
// Use a fixed time for deterministic tests
|
|
fixedTime := metav1.NewTime(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC))
|
|
|
|
tests := []struct {
|
|
name string
|
|
status interface{}
|
|
wantErr bool
|
|
expectedRaw string
|
|
}{
|
|
{
|
|
name: "simple pod status",
|
|
status: corev1.PodStatus{
|
|
Phase: corev1.PodRunning,
|
|
},
|
|
wantErr: false,
|
|
expectedRaw: `{"phase":"Running"}`,
|
|
},
|
|
{
|
|
name: "complex pod status",
|
|
status: corev1.PodStatus{
|
|
Phase: corev1.PodRunning,
|
|
Conditions: []corev1.PodCondition{
|
|
{
|
|
Type: corev1.PodReady,
|
|
Status: corev1.ConditionTrue,
|
|
LastProbeTime: fixedTime,
|
|
LastTransitionTime: fixedTime,
|
|
},
|
|
},
|
|
},
|
|
wantErr: false,
|
|
expectedRaw: `{"phase":"Running","conditions":[{"type":"Ready","status":"True","lastProbeTime":"2024-01-01T00:00:00Z","lastTransitionTime":"2024-01-01T00:00:00Z"}]}`,
|
|
},
|
|
{
|
|
name: "nil status",
|
|
status: nil,
|
|
wantErr: false,
|
|
expectedRaw: `null`,
|
|
},
|
|
{
|
|
name: "unmarshallable status",
|
|
status: make(chan int),
|
|
wantErr: true,
|
|
expectedRaw: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := BuildStatusRawExtension(tt.status)
|
|
if tt.wantErr {
|
|
assert.Error(t, err)
|
|
assert.Nil(t, got)
|
|
return
|
|
}
|
|
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, got)
|
|
assert.JSONEq(t, tt.expectedRaw, string(got.Raw))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAssembleWorkStatus(t *testing.T) {
|
|
// Common test data
|
|
statusRaw := []byte(`{"replicas": 3}`)
|
|
baseManifest := workv1alpha1.Manifest{
|
|
RawExtension: runtime.RawExtension{
|
|
Raw: []byte(`{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"name":"test-deployment","namespace":"test-ns"}}`),
|
|
},
|
|
}
|
|
|
|
baseObjRef := workv1alpha2.ObjectReference{
|
|
APIVersion: "apps/v1",
|
|
Kind: "Deployment",
|
|
Namespace: "test-ns",
|
|
Name: "test-deployment",
|
|
}
|
|
|
|
// Helper function to create a basic work
|
|
createWork := func(name string, manifest workv1alpha1.Manifest) workv1alpha1.Work {
|
|
return workv1alpha1.Work{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
Namespace: "karmada-es-member1",
|
|
},
|
|
Spec: workv1alpha1.WorkSpec{
|
|
Workload: workv1alpha1.WorkloadTemplate{
|
|
Manifests: []workv1alpha1.Manifest{manifest},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
works []workv1alpha1.Work
|
|
objRef workv1alpha2.ObjectReference
|
|
expectedItems []workv1alpha2.AggregatedStatusItem
|
|
wantErr bool
|
|
errorMessage string
|
|
}{
|
|
{
|
|
name: "empty work list",
|
|
works: []workv1alpha1.Work{},
|
|
objRef: baseObjRef,
|
|
expectedItems: []workv1alpha2.AggregatedStatusItem{},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "work with invalid manifest",
|
|
works: []workv1alpha1.Work{
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-work",
|
|
Namespace: "karmada-es-member1",
|
|
},
|
|
Spec: workv1alpha1.WorkSpec{
|
|
Workload: workv1alpha1.WorkloadTemplate{
|
|
Manifests: []workv1alpha1.Manifest{
|
|
{
|
|
RawExtension: runtime.RawExtension{Raw: []byte(`invalid json`)},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
objRef: baseObjRef,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "work being deleted",
|
|
works: []workv1alpha1.Work{
|
|
func() workv1alpha1.Work {
|
|
w := createWork("test-work", baseManifest)
|
|
now := metav1.NewTime(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC))
|
|
w.DeletionTimestamp = &now
|
|
return w
|
|
}(),
|
|
},
|
|
objRef: baseObjRef,
|
|
expectedItems: []workv1alpha2.AggregatedStatusItem{},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "work applied successfully with health status",
|
|
works: []workv1alpha1.Work{
|
|
func() workv1alpha1.Work {
|
|
w := createWork("test-work", baseManifest)
|
|
w.Status = workv1alpha1.WorkStatus{
|
|
Conditions: []metav1.Condition{
|
|
{
|
|
Type: workv1alpha1.WorkApplied,
|
|
Status: metav1.ConditionTrue,
|
|
},
|
|
},
|
|
ManifestStatuses: []workv1alpha1.ManifestStatus{
|
|
{
|
|
Identifier: workv1alpha1.ResourceIdentifier{
|
|
Ordinal: 0,
|
|
Group: "apps",
|
|
Version: "v1",
|
|
Kind: "Deployment",
|
|
Namespace: "test-ns",
|
|
Name: "test-deployment",
|
|
},
|
|
Status: &runtime.RawExtension{Raw: statusRaw},
|
|
Health: "Healthy",
|
|
},
|
|
},
|
|
}
|
|
return w
|
|
}(),
|
|
},
|
|
objRef: baseObjRef,
|
|
expectedItems: []workv1alpha2.AggregatedStatusItem{
|
|
{
|
|
ClusterName: "member1",
|
|
Status: &runtime.RawExtension{Raw: statusRaw},
|
|
Applied: true,
|
|
Health: workv1alpha2.ResourceHealthy,
|
|
},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "work not applied with error message",
|
|
works: []workv1alpha1.Work{
|
|
func() workv1alpha1.Work {
|
|
w := createWork("test-work", baseManifest)
|
|
w.Status = workv1alpha1.WorkStatus{
|
|
Conditions: []metav1.Condition{
|
|
{
|
|
Type: workv1alpha1.WorkApplied,
|
|
Status: metav1.ConditionFalse,
|
|
Message: "Failed to apply",
|
|
},
|
|
},
|
|
}
|
|
return w
|
|
}(),
|
|
},
|
|
objRef: baseObjRef,
|
|
expectedItems: []workv1alpha2.AggregatedStatusItem{
|
|
{
|
|
ClusterName: "member1",
|
|
Applied: false,
|
|
AppliedMessage: "Failed to apply",
|
|
Health: workv1alpha2.ResourceUnknown,
|
|
},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "work with unknown condition status",
|
|
works: []workv1alpha1.Work{
|
|
func() workv1alpha1.Work {
|
|
w := createWork("test-work", baseManifest)
|
|
w.Status = workv1alpha1.WorkStatus{
|
|
Conditions: []metav1.Condition{
|
|
{
|
|
Type: workv1alpha1.WorkApplied,
|
|
Status: metav1.ConditionUnknown,
|
|
Message: "Status unknown",
|
|
},
|
|
},
|
|
}
|
|
return w
|
|
}(),
|
|
},
|
|
objRef: baseObjRef,
|
|
expectedItems: []workv1alpha2.AggregatedStatusItem{
|
|
{
|
|
ClusterName: "member1",
|
|
Applied: false,
|
|
AppliedMessage: "Status unknown",
|
|
Health: workv1alpha2.ResourceUnknown,
|
|
},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := assembleWorkStatus(tt.works, tt.objRef)
|
|
if tt.wantErr {
|
|
assert.Error(t, err)
|
|
if tt.errorMessage != "" {
|
|
assert.Contains(t, err.Error(), tt.errorMessage)
|
|
}
|
|
return
|
|
}
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, tt.expectedItems, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGenerateFullyAppliedCondition(t *testing.T) {
|
|
spec := workv1alpha2.ResourceBindingSpec{
|
|
Clusters: []workv1alpha2.TargetCluster{
|
|
{Name: "cluster1"},
|
|
{Name: "cluster2"},
|
|
},
|
|
}
|
|
statuses := []workv1alpha2.AggregatedStatusItem{
|
|
{ClusterName: "cluster1", Applied: true, Health: workv1alpha2.ResourceHealthy},
|
|
{ClusterName: "cluster2", Applied: true, Health: workv1alpha2.ResourceUnknown},
|
|
}
|
|
|
|
expectedTrue := metav1.ConditionTrue
|
|
expectedFalse := metav1.ConditionFalse
|
|
|
|
resultTrue := generateFullyAppliedCondition(spec, statuses)
|
|
assert.Equal(t, expectedTrue, resultTrue.Status, "generateFullyAppliedCondition with fully applied statuses")
|
|
|
|
resultFalse := generateFullyAppliedCondition(spec, statuses[:1])
|
|
assert.Equal(t, expectedFalse, resultFalse.Status, "generateFullyAppliedCondition with partially applied statuses")
|
|
}
|
|
|
|
func TestWorksFullyApplied(t *testing.T) {
|
|
type args struct {
|
|
aggregatedStatuses []workv1alpha2.AggregatedStatusItem
|
|
targetClusters sets.Set[string]
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
want bool
|
|
}{
|
|
{
|
|
name: "no cluster",
|
|
args: args{
|
|
aggregatedStatuses: []workv1alpha2.AggregatedStatusItem{
|
|
{
|
|
ClusterName: "member1",
|
|
Applied: true,
|
|
},
|
|
},
|
|
targetClusters: nil,
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "no aggregatedStatuses",
|
|
args: args{
|
|
aggregatedStatuses: nil,
|
|
targetClusters: sets.New("member1"),
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "cluster size is not equal to aggregatedStatuses",
|
|
args: args{
|
|
aggregatedStatuses: []workv1alpha2.AggregatedStatusItem{
|
|
{
|
|
ClusterName: "member1",
|
|
Applied: true,
|
|
},
|
|
},
|
|
targetClusters: sets.New("member1", "member2"),
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "aggregatedStatuses is equal to clusterNames and all applied",
|
|
args: args{
|
|
aggregatedStatuses: []workv1alpha2.AggregatedStatusItem{
|
|
{
|
|
ClusterName: "member1",
|
|
Applied: true,
|
|
},
|
|
{
|
|
ClusterName: "member2",
|
|
Applied: true,
|
|
},
|
|
},
|
|
targetClusters: sets.New("member1", "member2"),
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "aggregatedStatuses is equal to clusterNames but not all applied",
|
|
args: args{
|
|
aggregatedStatuses: []workv1alpha2.AggregatedStatusItem{
|
|
{
|
|
ClusterName: "member1",
|
|
Applied: true,
|
|
},
|
|
{
|
|
ClusterName: "member2",
|
|
Applied: false,
|
|
},
|
|
},
|
|
targetClusters: sets.New("member1", "member2"),
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "target clusters not match expected status",
|
|
args: args{
|
|
aggregatedStatuses: []workv1alpha2.AggregatedStatusItem{
|
|
{
|
|
ClusterName: "member1",
|
|
Applied: true,
|
|
},
|
|
},
|
|
targetClusters: sets.New("member2"),
|
|
},
|
|
want: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := worksFullyApplied(tt.args.aggregatedStatuses, tt.args.targetClusters)
|
|
assert.Equal(t, tt.want, got, "worksFullyApplied() result")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetManifestIndex(t *testing.T) {
|
|
manifest1 := workv1alpha1.Manifest{
|
|
RawExtension: runtime.RawExtension{
|
|
Raw: []byte(`{"apiVersion":"v1","kind":"Service","metadata":{"name":"test-service","namespace":"test-namespace"}}`),
|
|
},
|
|
}
|
|
manifest2 := workv1alpha1.Manifest{
|
|
RawExtension: runtime.RawExtension{
|
|
Raw: []byte(`{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"name":"test-deployment","namespace":"test-namespace"}}`),
|
|
},
|
|
}
|
|
manifests := []workv1alpha1.Manifest{manifest1, manifest2}
|
|
|
|
service := &unstructured.Unstructured{
|
|
Object: map[string]interface{}{
|
|
"apiVersion": "v1",
|
|
"kind": "Service",
|
|
"metadata": map[string]interface{}{
|
|
"name": "test-service",
|
|
"namespace": "test-namespace",
|
|
},
|
|
},
|
|
}
|
|
|
|
deployment := &unstructured.Unstructured{
|
|
Object: map[string]interface{}{
|
|
"apiVersion": "apps/v1",
|
|
"kind": "Deployment",
|
|
"metadata": map[string]interface{}{
|
|
"name": "test-deployment",
|
|
"namespace": "test-namespace",
|
|
},
|
|
},
|
|
}
|
|
|
|
t.Run("Service", func(t *testing.T) {
|
|
manifestRef := ManifestReference{APIVersion: service.GetAPIVersion(), Kind: service.GetKind(),
|
|
Namespace: service.GetNamespace(), Name: service.GetName()}
|
|
index, err := GetManifestIndex(manifests, &manifestRef)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 0, index, "Service manifest index")
|
|
})
|
|
|
|
t.Run("Deployment", func(t *testing.T) {
|
|
manifestRef := ManifestReference{APIVersion: deployment.GetAPIVersion(), Kind: deployment.GetKind(),
|
|
Namespace: deployment.GetNamespace(), Name: deployment.GetName()}
|
|
index, err := GetManifestIndex(manifests, &manifestRef)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 1, index, "Deployment manifest index")
|
|
})
|
|
|
|
t.Run("No match", func(t *testing.T) {
|
|
_, err := GetManifestIndex(manifests, &ManifestReference{})
|
|
assert.Error(t, err, "Expected error for no match")
|
|
})
|
|
}
|
|
|
|
func TestEqualIdentifier(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
target *workv1alpha1.ResourceIdentifier
|
|
ordinal int
|
|
workload *ManifestReference
|
|
expectedOutput bool
|
|
}{
|
|
{
|
|
name: "identifiers are equal",
|
|
target: &workv1alpha1.ResourceIdentifier{
|
|
Ordinal: 0,
|
|
Group: "apps",
|
|
Version: "v1",
|
|
Kind: "Deployment",
|
|
Namespace: "default",
|
|
Name: "test-deployment",
|
|
},
|
|
ordinal: 0,
|
|
workload: &ManifestReference{
|
|
APIVersion: "apps/v1",
|
|
Kind: "Deployment",
|
|
Namespace: "default",
|
|
Name: "test-deployment",
|
|
},
|
|
expectedOutput: true,
|
|
},
|
|
{
|
|
name: "identifiers are not equal",
|
|
target: &workv1alpha1.ResourceIdentifier{
|
|
Ordinal: 1,
|
|
Group: "apps",
|
|
Version: "v1",
|
|
Kind: "Deployment",
|
|
Namespace: "default",
|
|
Name: "test-deployment",
|
|
},
|
|
ordinal: 0,
|
|
workload: &ManifestReference{
|
|
APIVersion: "apps/v1",
|
|
Kind: "Deployment",
|
|
Namespace: "default",
|
|
Name: "test-deployment",
|
|
},
|
|
expectedOutput: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
equal, err := equalIdentifier(tc.target, tc.ordinal, tc.workload)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, tc.expectedOutput, equal, "equalIdentifier() result")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsResourceApplied(t *testing.T) {
|
|
// Create a WorkStatus struct with a WorkApplied condition set to True
|
|
workStatus := &workv1alpha1.WorkStatus{
|
|
Conditions: []metav1.Condition{
|
|
{
|
|
Type: workv1alpha1.WorkApplied,
|
|
Status: metav1.ConditionTrue,
|
|
},
|
|
},
|
|
}
|
|
|
|
// Call IsResourceApplied and assert that it returns true
|
|
assert.True(t, IsResourceApplied(workStatus))
|
|
}
|
|
|
|
// Helper Functions
|
|
|
|
// setupScheme initializes a new scheme
|
|
func setupScheme() *runtime.Scheme {
|
|
scheme := runtime.NewScheme()
|
|
_ = workv1alpha1.Install(scheme)
|
|
_ = workv1alpha2.Install(scheme)
|
|
_ = corev1.AddToScheme(scheme)
|
|
_ = appsv1.AddToScheme(scheme)
|
|
return scheme
|
|
}
|