Skip to content

Commit

Permalink
drift detection and takeover implementation #1
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelawyu committed Dec 9, 2024
1 parent 5706894 commit a8ed374
Show file tree
Hide file tree
Showing 18 changed files with 1,833 additions and 29 deletions.
2 changes: 1 addition & 1 deletion apis/cluster/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apis/placement/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apis/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

671 changes: 671 additions & 0 deletions pkg/controllers/workapplier/controller.go

Large diffs are not rendered by default.

345 changes: 345 additions & 0 deletions pkg/controllers/workapplier/controller_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,345 @@
/*
Copyright (c) Microsoft Corporation.
Licensed under the MIT license.
*/

package workapplier

import (
"context"
"fmt"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
corev1 "k8s.io/api/core/v1"

fleetv1beta1 "go.goms.io/fleet/apis/placement/v1beta1"
"go.goms.io/fleet/pkg/utils/parallelizer"

"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/dynamic/fake"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/klog/v2"
)

const (
workName = "work-1"

deployName = "deploy-1"
configMapName = "configmap-1"
nsName = "ns-1"

nsNameTemplate = "ns-%s"
)

var (
appliedWorkOwnerRef = &metav1.OwnerReference{
APIVersion: "placement.kubernetes-fleet.io/v1beta1",
Kind: "AppliedWork",
Name: workName,
UID: "uid",
}
)

var (
ignoreFieldTypeMetaInNamespace = cmpopts.IgnoreFields(corev1.Namespace{}, "TypeMeta")
)

// TestRemoveLeftOverManifests tests the removeLeftOverManifests method.
func TestRemoveLeftOverManifests(t *testing.T) {
ctx := context.Background()

additionalOwnerRef := &metav1.OwnerReference{
APIVersion: "v1",
Kind: "SuperNamespace",
Name: "super-ns",
UID: "super-ns-uid",
}

nsName0 := fmt.Sprintf(nsNameTemplate, "0")
nsName1 := fmt.Sprintf(nsNameTemplate, "1")
nsName2 := fmt.Sprintf(nsNameTemplate, "2")
nsName3 := fmt.Sprintf(nsNameTemplate, "3")

testCases := []struct {
name string
leftOverManifests []fleetv1beta1.AppliedResourceMeta
inMemberClusterObjs []runtime.Object
wantInMemberClusterObjs []corev1.Namespace
wantRemovedInMemberClusterObjs []corev1.Namespace
}{
{
name: "mixed",
leftOverManifests: []fleetv1beta1.AppliedResourceMeta{
// The object is present.
{
WorkResourceIdentifier: *nsWRI(0, nsName0),
},
// The object cannot be found.
{
WorkResourceIdentifier: *nsWRI(1, nsName1),
},
// The object is not owned by Fleet.
{
WorkResourceIdentifier: *nsWRI(2, nsName2),
},
// The object has multiple owners.
{
WorkResourceIdentifier: *nsWRI(3, nsName3),
},
},
inMemberClusterObjs: []runtime.Object{
&corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: nsName0,
},
},
&corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: nsName2,
OwnerReferences: []metav1.OwnerReference{
*additionalOwnerRef,
},
},
},
&corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: nsName3,
OwnerReferences: []metav1.OwnerReference{
*additionalOwnerRef,
*appliedWorkOwnerRef,
},
},
},
},
wantInMemberClusterObjs: []corev1.Namespace{
{
ObjectMeta: metav1.ObjectMeta{
Name: nsName2,
OwnerReferences: []metav1.OwnerReference{
*additionalOwnerRef,
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: nsName3,
OwnerReferences: []metav1.OwnerReference{
*additionalOwnerRef,
},
},
},
},
wantRemovedInMemberClusterObjs: []corev1.Namespace{
{
ObjectMeta: metav1.ObjectMeta{
Name: nsName0,
},
},
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
fakeClient := fake.NewSimpleDynamicClient(scheme.Scheme, tc.inMemberClusterObjs...)
r := &Reconciler{
spokeDynamicClient: fakeClient,
parallelizer: parallelizer.NewParallelizer(2),
}
if err := r.removeLeftOverManifests(ctx, tc.leftOverManifests, appliedWorkOwnerRef); err != nil {
t.Errorf("removeLeftOverManifests() = %v, want no error", err)
}

for idx := range tc.wantInMemberClusterObjs {
wantNS := tc.wantInMemberClusterObjs[idx]

gotUnstructured, err := fakeClient.
Resource(nsGVR).
Namespace(wantNS.GetNamespace()).
Get(ctx, wantNS.GetName(), metav1.GetOptions{})
if err != nil {
t.Errorf("Get Namespace(%v) = %v, want no error", klog.KObj(&wantNS), err)
continue
}

gotNS := wantNS.DeepCopy()
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(gotUnstructured.Object, &gotNS); err != nil {
t.Errorf("FromUnstructured() = %v, want no error", err)
}

if diff := cmp.Diff(gotNS, &wantNS, ignoreFieldTypeMetaInNamespace); diff != "" {
t.Errorf("NS(%v) mismatches (-got +want):\n%s", klog.KObj(&wantNS), diff)
}
}

for idx := range tc.wantRemovedInMemberClusterObjs {
wantRemovedNS := tc.wantRemovedInMemberClusterObjs[idx]

gotUnstructured, err := fakeClient.
Resource(nsGVR).
Namespace(wantRemovedNS.GetNamespace()).
Get(ctx, wantRemovedNS.GetName(), metav1.GetOptions{})
if err != nil {
t.Errorf("Get Namespace(%v) = %v, want no error", klog.KObj(&wantRemovedNS), err)
}

gotRemovedNS := wantRemovedNS.DeepCopy()
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(gotUnstructured.Object, &gotRemovedNS); err != nil {
t.Errorf("FromUnstructured() = %v, want no error", err)
}

if !gotRemovedNS.DeletionTimestamp.IsZero() {
t.Errorf("Namespace(%v) has not been deleted", klog.KObj(&wantRemovedNS))
}
}
})
}
}

// TestRemoveOneLeftOverManifest tests the removeOneLeftOverManifest method.
func TestRemoveOneLeftOverManifest(t *testing.T) {
ctx := context.Background()
now := metav1.Now().Rfc3339Copy()
leftOverManifest := fleetv1beta1.AppliedResourceMeta{
WorkResourceIdentifier: *nsWRI(0, nsName),
}
additionalOwnerRef := &metav1.OwnerReference{
APIVersion: "v1",
Kind: "SuperNamespace",
Name: "super-ns",
UID: "super-ns-uid",
}

testCases := []struct {
name string
// To simplify things, for this test Fleet uses a fixed concrete type.
inMemberClusterObj *corev1.Namespace
wantInMemberClusterObj *corev1.Namespace
}{
{
name: "not found",
},
{
name: "already deleted",
inMemberClusterObj: &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: nsName,
DeletionTimestamp: &now,
},
},
wantInMemberClusterObj: &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: nsName,
DeletionTimestamp: &now,
},
},
},
{
name: "not derived from manifest object",
inMemberClusterObj: &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: nsName,
OwnerReferences: []metav1.OwnerReference{
*additionalOwnerRef,
},
},
},
wantInMemberClusterObj: &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: nsName,
OwnerReferences: []metav1.OwnerReference{
*additionalOwnerRef,
},
},
},
},
{
name: "multiple owners",
inMemberClusterObj: &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: nsName,
OwnerReferences: []metav1.OwnerReference{
*additionalOwnerRef,
*appliedWorkOwnerRef,
},
},
},
wantInMemberClusterObj: &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: nsName,
OwnerReferences: []metav1.OwnerReference{
*additionalOwnerRef,
},
},
},
},
{
name: "deletion",
inMemberClusterObj: &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: nsName,
OwnerReferences: []metav1.OwnerReference{
*appliedWorkOwnerRef,
},
},
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var fakeClient *fake.FakeDynamicClient
if tc.inMemberClusterObj != nil {
fakeClient = fake.NewSimpleDynamicClient(scheme.Scheme, tc.inMemberClusterObj)
} else {
fakeClient = fake.NewSimpleDynamicClient(scheme.Scheme)
}

r := &Reconciler{
spokeDynamicClient: fakeClient,
}
if err := r.removeOneLeftOverManifest(ctx, leftOverManifest, appliedWorkOwnerRef); err != nil {
t.Errorf("removeOneLeftOverManifest() = %v, want no error", err)
}

if tc.inMemberClusterObj != nil {
var gotUnstructured *unstructured.Unstructured
var err error
// The method is expected to modify the object.
gotUnstructured, err = fakeClient.
Resource(nsGVR).
Namespace(tc.inMemberClusterObj.GetNamespace()).
Get(ctx, tc.inMemberClusterObj.GetName(), metav1.GetOptions{})
switch {
case errors.IsNotFound(err) && tc.wantInMemberClusterObj == nil:
// The object is expected to be deleted.
return
case errors.IsNotFound(err):
// An object is expected to be found.
t.Errorf("Get(%v) = %v, want no error", klog.KObj(tc.inMemberClusterObj), err)
return
case err != nil:
// An unexpected error occurred.
t.Errorf("Get(%v) = %v, want no error", klog.KObj(tc.inMemberClusterObj), err)
return
}

got := tc.wantInMemberClusterObj.DeepCopy()
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(gotUnstructured.Object, &got); err != nil {
t.Errorf("FromUnstructured() = %v, want no error", err)
return
}

if diff := cmp.Diff(got, tc.wantInMemberClusterObj, ignoreFieldTypeMetaInNamespace); diff != "" {
t.Errorf("NS(%v) mismatches (-got +want):\n%s", klog.KObj(tc.inMemberClusterObj), diff)
}
return
}
})
}
}
Loading

0 comments on commit a8ed374

Please sign in to comment.