appliedmanifestwork eviction (#190)

Signed-off-by: Wei Liu <liuweixa@redhat.com>
This commit is contained in:
Wei Liu
2023-03-14 05:01:12 -04:00
committed by GitHub
parent aca3b66371
commit ee923cd5f6
9 changed files with 392 additions and 283 deletions
@@ -72,6 +72,10 @@ spec:
version:
description: Version is the version of the Kubernetes resource.
type: string
evictionStartTime:
description: 'EvictionStartTime represents the current appliedmanifestwork will be evicted after a grace period. An appliedmanifestwork will be evicted from the managed cluster in the following two scenarios: - the manifestwork of the current appliedmanifestwork is missing on the hub, or - the appliedmanifestwork hub hash does not match the current hub hash of the work agent.'
type: string
format: date-time
served: true
storage: true
subresources:
+2 -1
View File
@@ -4,11 +4,12 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/google/go-cmp/cmp"
"reflect"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/openshift/library-go/pkg/operator/events/eventstesting"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
@@ -68,13 +68,11 @@ func (m *ManifestWorkFinalizeController) sync(ctx context.Context, controllerCon
manifestWork, err := m.manifestWorkLister.Get(manifestWorkName)
// Delete appliedmanifestwork if relating manfiestwork is not found or being deleted
// Delete appliedmanifestwork if relating manfiestwork is being deleted
switch {
case errors.IsNotFound(err):
err := m.deleteAppliedManifestWork(ctx, appliedManifestWorkName)
if err != nil {
return err
}
// the appliedmanifestwork will be evicted if the relating manfiestwork is not found
return nil
case err != nil:
return err
case !manifestWork.DeletionTimestamp.IsZero():
@@ -185,19 +185,13 @@ func TestSyncManifestWorkController(t *testing.T) {
Name: fmt.Sprintf("%s-work", hubHash),
},
},
validateAppliedManifestWorkActions: func(t *testing.T, actions []clienttesting.Action) {
if len(actions) != 1 {
t.Errorf("Expect 2 actions on appliedmanifestwork, but have %d", len(actions))
}
spoketesting.AssertAction(t, actions[0], "delete")
},
validateAppliedManifestWorkActions: noAction,
validateManifestWorkActions: func(t *testing.T, actions []clienttesting.Action) {
if len(actions) != 0 {
t.Errorf("Suppose nothing done for manifestwork")
}
},
expectedQueueLen: 1,
expectedQueueLen: 0,
},
}
@@ -2,17 +2,20 @@ package finalizercontroller
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
jsonpatch "github.com/evanphx/json-patch"
"github.com/openshift/library-go/pkg/controller/factory"
"github.com/openshift/library-go/pkg/operator/events"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/client-go/tools/cache"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/workqueue"
"k8s.io/klog/v2"
workv1client "open-cluster-management.io/api/client/work/clientset/versioned/typed/work/v1"
workinformer "open-cluster-management.io/api/client/work/informers/externalversions/work/v1"
@@ -21,41 +24,42 @@ import (
"open-cluster-management.io/work/pkg/helper"
)
const byWorkNameAndAgentID = "UnManagedAppliedManifestWork-byWorkNameAndAgentID"
// UnManagedAppliedWorkController deletes unmanaged applied works.
type UnManagedAppliedWorkController struct {
manifestWorkLister worklister.ManifestWorkNamespaceLister
appliedManifestWorkClient workv1client.AppliedManifestWorkInterface
appliedManifestWorkLister worklister.AppliedManifestWorkLister
appliedManifestWorkIndexer cache.Indexer
hubHash string
agentID string
type unmanagedAppliedWorkController struct {
manifestWorkLister worklister.ManifestWorkNamespaceLister
appliedManifestWorkClient workv1client.AppliedManifestWorkInterface
appliedManifestWorkLister worklister.AppliedManifestWorkLister
hubHash string
agentID string
evictionGracePeriod time.Duration
rateLimiter workqueue.RateLimiter
}
// NewUnManagedAppliedWorkController returns a controller to evict the unmanaged appliedmanifestworks.
//
// An appliedmanifestwork will be considered unmanaged in the following scenarios:
// - the manifestwork of the current appliedmanifestwork is missing on the hub, or
// - the appliedmanifestwork hub hash does not match the current hub hash of the work agent.
//
// One unmanaged appliedmanifestwork will be evicted from the managed cluster after a grace period (by
// default, 10 minutes), after one appliedmanifestwork is evicted from the managed cluster, its owned
// resources will also be evicted from the managed cluster with Kubernetes garbage collection.
func NewUnManagedAppliedWorkController(
recorder events.Recorder,
manifestWorkInformer workinformer.ManifestWorkInformer,
manifestWorkLister worklister.ManifestWorkNamespaceLister,
appliedManifestWorkClient workv1client.AppliedManifestWorkInterface,
appliedManifestWorkInformer workinformer.AppliedManifestWorkInformer,
evictionGracePeriod time.Duration,
hubHash, agentID string,
) factory.Controller {
controller := &UnManagedAppliedWorkController{
manifestWorkLister: manifestWorkLister,
appliedManifestWorkClient: appliedManifestWorkClient,
appliedManifestWorkLister: appliedManifestWorkInformer.Lister(),
appliedManifestWorkIndexer: appliedManifestWorkInformer.Informer().GetIndexer(),
hubHash: hubHash,
agentID: agentID,
}
err := appliedManifestWorkInformer.Informer().AddIndexers(cache.Indexers{
byWorkNameAndAgentID: indexByWorkNameAndAgentID,
})
if err != nil {
utilruntime.HandleError(err)
controller := &unmanagedAppliedWorkController{
manifestWorkLister: manifestWorkLister,
appliedManifestWorkClient: appliedManifestWorkClient,
appliedManifestWorkLister: appliedManifestWorkInformer.Lister(),
hubHash: hubHash,
agentID: agentID,
evictionGracePeriod: evictionGracePeriod,
rateLimiter: workqueue.NewItemExponentialFailureRateLimiter(1*time.Minute, evictionGracePeriod),
}
return factory.New().
@@ -63,92 +67,102 @@ func NewUnManagedAppliedWorkController(
accessor, _ := meta.Accessor(obj)
return fmt.Sprintf("%s-%s", hubHash, accessor.GetName())
}, manifestWorkInformer.Informer()).
WithFilteredEventsInformersQueueKeyFunc(func(obj runtime.Object) string {
accessor, _ := meta.Accessor(obj)
return accessor.GetName()
}, helper.AppliedManifestworkHubHashFilter(hubHash), appliedManifestWorkInformer.Informer()).
WithFilteredEventsInformersQueueKeyFunc(
func(obj runtime.Object) string {
accessor, _ := meta.Accessor(obj)
return accessor.GetName()
}, helper.AppliedManifestworkAgentIDFilter(agentID), appliedManifestWorkInformer.Informer()).
WithSync(controller.sync).ToController("UnManagedAppliedManifestWork", recorder)
}
func (m *UnManagedAppliedWorkController) sync(ctx context.Context, controllerContext factory.SyncContext) error {
func (m *unmanagedAppliedWorkController) sync(ctx context.Context, controllerContext factory.SyncContext) error {
appliedManifestWorkName := controllerContext.QueueKey()
klog.V(4).Infof("Reconciling ManifestWork %q", appliedManifestWorkName)
klog.V(4).Infof("Reconciling AppliedManifestWork %q", appliedManifestWorkName)
appliedManifestWork, err := m.appliedManifestWorkLister.Get(appliedManifestWorkName)
if errors.IsNotFound(err) {
// work not found, could have been deleted, do nothing.
// appliedmanifestwork not found, could have been deleted, do nothing.
return nil
}
if err != nil {
return err
}
// We delete the old AppliedManifestWork only when the related ManifestWork is applied with the new
// AppliedManifestWork as the new owner. This can avoid deleting the old AppliedManifestWork prematurely
// before the new AppliedManifestWork takes the ownership of the applied resources.
manifestWork, err := m.manifestWorkLister.Get(appliedManifestWork.Spec.ManifestWorkName)
_, err = m.manifestWorkLister.Get(appliedManifestWork.Spec.ManifestWorkName)
if errors.IsNotFound(err) {
// work not found, could have been deleted, do nothing.
return nil
// evict the current appliedmanifestwork when its relating manifestwork is missing on the hub
return m.evictAppliedManifestWork(ctx, controllerContext, appliedManifestWork)
}
if err != nil {
return err
}
if !meta.IsStatusConditionTrue(manifestWork.Status.Conditions, workapiv1.WorkApplied) {
// the work is not applied, do nothing.
// manifestwork exists but hub changed
if !strings.HasPrefix(appliedManifestWork.Name, m.hubHash) {
return m.evictAppliedManifestWork(ctx, controllerContext, appliedManifestWork)
}
// stop to evict the current appliedmanifestwork when its relating manifestwork is recreated on the hub
return m.stopToEvictAppliedManifestWork(ctx, appliedManifestWork)
}
func (m *unmanagedAppliedWorkController) evictAppliedManifestWork(ctx context.Context,
controllerContext factory.SyncContext, appliedManifestWork *workapiv1.AppliedManifestWork) error {
now := time.Now()
evictionStartTime := appliedManifestWork.Status.EvictionStartTime
if evictionStartTime == nil {
return m.patchEvictionStartTime(ctx, appliedManifestWork, &metav1.Time{Time: now})
}
if now.Before(evictionStartTime.Add(m.evictionGracePeriod)) {
controllerContext.Queue().AddAfter(appliedManifestWork.Name, m.rateLimiter.When(appliedManifestWork.Name))
return nil
}
unManagedAppliedWorks, err := m.getUnManagedAppliedManifestWorksByIndex(appliedManifestWork.Spec.ManifestWorkName, appliedManifestWork.Spec.AgentID)
klog.V(2).Infof("Delete appliedWork %s by agent %s after eviction grace periodby", appliedManifestWork.Name, m.agentID)
return m.appliedManifestWorkClient.Delete(ctx, appliedManifestWork.Name, metav1.DeleteOptions{})
}
func (m *unmanagedAppliedWorkController) stopToEvictAppliedManifestWork(
ctx context.Context, appliedManifestWork *workapiv1.AppliedManifestWork) error {
if appliedManifestWork.Status.EvictionStartTime == nil {
return nil
}
m.rateLimiter.Forget(appliedManifestWork.Name)
return m.patchEvictionStartTime(ctx, appliedManifestWork, nil)
}
func (m *unmanagedAppliedWorkController) patchEvictionStartTime(ctx context.Context,
appliedManifestWork *workapiv1.AppliedManifestWork, evictionStartTime *metav1.Time) error {
oldData, err := json.Marshal(workapiv1.AppliedManifestWork{
Status: workapiv1.AppliedManifestWorkStatus{
EvictionStartTime: appliedManifestWork.Status.EvictionStartTime,
},
})
if err != nil {
return err
return fmt.Errorf("failed to Marshal old data for appliedmanifestwork status %s: %w", appliedManifestWork.Name, err)
}
var errs []error
for _, appliedWork := range unManagedAppliedWorks {
klog.V(2).Infof("Delete appliedWork %s since it is not managed by agent %s anymore", appliedWork.Name, m.agentID)
err := m.appliedManifestWorkClient.Delete(ctx, appliedWork.Name, metav1.DeleteOptions{})
if err != nil {
errs = append(errs, err)
}
}
return utilerrors.NewAggregate(errs)
}
// getUnManagedAppliedManifestWorksByIndex finds appliedmanifestwork with the same workname and agent ID but different hubhash.
// These appliedManifestWorks is considered to be not managed by this work agent anymore and should be deleted.
// The reason of marking them as unmanaged is because the only reason under this conditions is work agent is switched to connect
// to a recovered hub or a fresh new hub. Those appliedmanifestwork needs to be deleted to avoid conflict with the newly connected
// hub.
func (m *UnManagedAppliedWorkController) getUnManagedAppliedManifestWorksByIndex(workName, agentID string) ([]*workapiv1.AppliedManifestWork, error) {
index := agentIDWorkNameIndex(workName, agentID)
items, err := m.appliedManifestWorkIndexer.ByIndex(byWorkNameAndAgentID, index)
newData, err := json.Marshal(workapiv1.AppliedManifestWork{
ObjectMeta: metav1.ObjectMeta{
UID: appliedManifestWork.UID,
ResourceVersion: appliedManifestWork.ResourceVersion,
},
Status: workapiv1.AppliedManifestWorkStatus{
EvictionStartTime: evictionStartTime,
},
})
if err != nil {
return nil, err
return fmt.Errorf("failed to Marshal new data for appliedmanifestwork status %s: %w", appliedManifestWork.Name, err)
}
ret := make([]*workapiv1.AppliedManifestWork, 0, len(items))
for _, item := range items {
appliedWork := item.(*workapiv1.AppliedManifestWork)
if appliedWork.Spec.HubHash == m.hubHash {
continue
}
ret = append(ret, item.(*workapiv1.AppliedManifestWork))
patchBytes, err := jsonpatch.CreateMergePatch(oldData, newData)
if err != nil {
return fmt.Errorf("failed to create patch for cluster %s: %w", appliedManifestWork.Name, err)
}
return ret, nil
}
func indexByWorkNameAndAgentID(obj interface{}) ([]string, error) {
appliedWork, ok := obj.(*workapiv1.AppliedManifestWork)
if !ok {
return []string{}, fmt.Errorf("obj is supposed to be a AppliedManifestWork, but is %T", obj)
}
return []string{agentIDWorkNameIndex(appliedWork.Spec.ManifestWorkName, appliedWork.Spec.AgentID)}, nil
}
func agentIDWorkNameIndex(workName, agentID string) string {
return fmt.Sprintf("%s/%s", workName, agentID)
_, err = m.appliedManifestWorkClient.Patch(ctx, appliedManifestWork.Name, types.MergePatchType, patchBytes, metav1.PatchOptions{}, "status")
return err
}
@@ -8,7 +8,8 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
clienttesting "k8s.io/client-go/testing"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
fakeworkclient "open-cluster-management.io/api/client/work/clientset/versioned/fake"
workinformers "open-cluster-management.io/api/client/work/informers/externalversions"
workapiv1 "open-cluster-management.io/api/work/v1"
@@ -18,32 +19,61 @@ import (
func TestSyncUnamanagedAppliedWork(t *testing.T) {
cases := []struct {
name string
workName string
appliedManifestWorkName string
hubHash string
agentID string
evictionGracePeriod time.Duration
works []runtime.Object
appliedWorks []runtime.Object
expectedQueueLen int
validateAppliedManifestWorkActions func(t *testing.T, actions []clienttesting.Action)
}{
{
name: "delete applied work if unmanaged",
workName: "test",
hubHash: "hubhash1",
agentID: "test-agent",
name: "appliedmanifestwork is not found",
appliedManifestWorkName: "hubhash-test",
hubHash: "hubhash",
agentID: "test-agent",
works: []runtime.Object{},
appliedWorks: []runtime.Object{},
validateAppliedManifestWorkActions: noAction,
},
{
name: "evict appliedmanifestwork when its relating manifestwork is missing on the hub",
appliedManifestWorkName: "hubhash-test",
hubHash: "hubhash",
agentID: "test-agent",
works: []runtime.Object{},
appliedWorks: []runtime.Object{
&workapiv1.AppliedManifestWork{
ObjectMeta: metav1.ObjectMeta{
Name: "hubhash-test",
},
Spec: workapiv1.AppliedManifestWorkSpec{
ManifestWorkName: "test",
HubHash: "hubhash",
AgentID: "test-agent",
},
},
},
validateAppliedManifestWorkActions: func(t *testing.T, actions []clienttesting.Action) {
if len(actions) != 1 {
t.Errorf("Expect 1 actions on appliedmanifestwork, but have %d", len(actions))
}
spoketesting.AssertAction(t, actions[0], "patch")
},
},
{
name: "evict appliedmanifestwork after the hub switched",
appliedManifestWorkName: "hubhash-test",
hubHash: "hubhash-new",
agentID: "test-agent",
works: []runtime.Object{
&workapiv1.ManifestWork{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "test",
},
Status: workapiv1.ManifestWorkStatus{
Conditions: []metav1.Condition{
{
Type: workapiv1.WorkApplied,
Status: metav1.ConditionTrue,
},
},
},
},
},
appliedWorks: []runtime.Object{
@@ -57,15 +87,44 @@ func TestSyncUnamanagedAppliedWork(t *testing.T) {
AgentID: "test-agent",
},
},
},
validateAppliedManifestWorkActions: func(t *testing.T, actions []clienttesting.Action) {
if len(actions) != 1 {
t.Errorf("Expect 1 actions on appliedmanifestwork, but have %d", len(actions))
}
spoketesting.AssertAction(t, actions[0], "patch")
},
},
{
name: "delete appliedmanifestwork after eviction grace period ",
appliedManifestWorkName: "hubhash-test",
hubHash: "hubhash-new",
agentID: "test-agent",
evictionGracePeriod: 10 * time.Minute,
works: []runtime.Object{
&workapiv1.ManifestWork{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "test",
},
},
},
appliedWorks: []runtime.Object{
&workapiv1.AppliedManifestWork{
ObjectMeta: metav1.ObjectMeta{
Name: "hubhash1-test",
Name: "hubhash-test",
},
Spec: workapiv1.AppliedManifestWorkSpec{
ManifestWorkName: "test",
HubHash: "hubhash1",
HubHash: "hubhash",
AgentID: "test-agent",
},
Status: workapiv1.AppliedManifestWorkStatus{
EvictionStartTime: &metav1.Time{
Time: time.Now().Add(-10 * time.Minute),
},
},
},
},
validateAppliedManifestWorkActions: func(t *testing.T, actions []clienttesting.Action) {
@@ -77,10 +136,10 @@ func TestSyncUnamanagedAppliedWork(t *testing.T) {
},
},
{
name: "no action if the work is not applied",
workName: "test",
hubHash: "hubhash1",
agentID: "test-agent",
name: "stop to evicte appliedmanifestwork when its relating manifestwork is recreated on the hub",
appliedManifestWorkName: "hubhash-test",
hubHash: "hubhash",
agentID: "test-agent",
works: []runtime.Object{
&workapiv1.ManifestWork{
ObjectMeta: metav1.ObjectMeta{
@@ -99,41 +158,28 @@ func TestSyncUnamanagedAppliedWork(t *testing.T) {
HubHash: "hubhash",
AgentID: "test-agent",
},
},
&workapiv1.AppliedManifestWork{
ObjectMeta: metav1.ObjectMeta{
Name: "hubhash1-test",
},
Spec: workapiv1.AppliedManifestWorkSpec{
ManifestWorkName: "test",
HubHash: "hubhash1",
AgentID: "test-agent",
},
},
},
validateAppliedManifestWorkActions: noAction,
},
{
name: "no action for different AgentID",
workName: "test",
hubHash: "hubhash1",
agentID: "test-agent",
works: []runtime.Object{
&workapiv1.ManifestWork{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "test",
},
Status: workapiv1.ManifestWorkStatus{
Conditions: []metav1.Condition{
{
Type: workapiv1.WorkApplied,
Status: metav1.ConditionTrue,
},
Status: workapiv1.AppliedManifestWorkStatus{
EvictionStartTime: &metav1.Time{
Time: time.Now(),
},
},
},
},
validateAppliedManifestWorkActions: func(t *testing.T, actions []clienttesting.Action) {
if len(actions) != 1 {
t.Errorf("Expect 1 actions on appliedmanifestwork, but have %d", len(actions))
}
spoketesting.AssertAction(t, actions[0], "patch")
},
},
{
name: "requeue eviction appliedmanifestwork",
appliedManifestWorkName: "hubhash-test",
hubHash: "hubhash",
agentID: "test-agent",
evictionGracePeriod: 10 * time.Minute,
works: []runtime.Object{},
appliedWorks: []runtime.Object{
&workapiv1.AppliedManifestWork{
ObjectMeta: metav1.ObjectMeta{
@@ -142,65 +188,16 @@ func TestSyncUnamanagedAppliedWork(t *testing.T) {
Spec: workapiv1.AppliedManifestWorkSpec{
ManifestWorkName: "test",
HubHash: "hubhash",
AgentID: "test-agent1",
},
},
&workapiv1.AppliedManifestWork{
ObjectMeta: metav1.ObjectMeta{
Name: "hubhash1-test",
},
Spec: workapiv1.AppliedManifestWorkSpec{
ManifestWorkName: "test",
HubHash: "hubhash1",
AgentID: "test-agent",
},
},
},
validateAppliedManifestWorkActions: noAction,
},
{
name: "no action for different work",
workName: "test",
hubHash: "hubhash1",
agentID: "test-agent",
works: []runtime.Object{
&workapiv1.ManifestWork{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "test",
},
Status: workapiv1.ManifestWorkStatus{
Conditions: []metav1.Condition{
{
Type: workapiv1.WorkApplied,
Status: metav1.ConditionTrue,
},
Status: workapiv1.AppliedManifestWorkStatus{
EvictionStartTime: &metav1.Time{
Time: time.Now().Add(-5 * time.Minute),
},
},
},
},
appliedWorks: []runtime.Object{
&workapiv1.AppliedManifestWork{
ObjectMeta: metav1.ObjectMeta{
Name: "hubhash-test1",
},
Spec: workapiv1.AppliedManifestWorkSpec{
ManifestWorkName: "test1",
HubHash: "hubhash",
AgentID: "test-agent",
},
},
&workapiv1.AppliedManifestWork{
ObjectMeta: metav1.ObjectMeta{
Name: "hubhash1-test",
},
Spec: workapiv1.AppliedManifestWorkSpec{
ManifestWorkName: "test",
HubHash: "hubhash1",
AgentID: "test-agent",
},
},
},
expectedQueueLen: 1,
validateAppliedManifestWorkActions: noAction,
},
}
@@ -209,12 +206,6 @@ func TestSyncUnamanagedAppliedWork(t *testing.T) {
t.Run(c.name, func(t *testing.T) {
fakeClient := fakeworkclient.NewSimpleClientset(c.appliedWorks...)
informerFactory := workinformers.NewSharedInformerFactory(fakeClient, 5*time.Minute)
err := informerFactory.Work().V1().AppliedManifestWorks().Informer().AddIndexers(cache.Indexers{
byWorkNameAndAgentID: indexByWorkNameAndAgentID,
})
if err != nil {
t.Fatal(err)
}
for _, work := range c.works {
if err := informerFactory.Work().V1().ManifestWorks().Informer().GetStore().Add(work); err != nil {
t.Fatal(err)
@@ -226,23 +217,28 @@ func TestSyncUnamanagedAppliedWork(t *testing.T) {
}
}
controller := &UnManagedAppliedWorkController{
manifestWorkLister: informerFactory.Work().V1().ManifestWorks().Lister().ManifestWorks("test"),
appliedManifestWorkClient: fakeClient.WorkV1().AppliedManifestWorks(),
appliedManifestWorkLister: informerFactory.Work().V1().AppliedManifestWorks().Lister(),
appliedManifestWorkIndexer: informerFactory.Work().V1().AppliedManifestWorks().Informer().GetIndexer(),
hubHash: c.hubHash,
agentID: c.agentID,
controller := &unmanagedAppliedWorkController{
manifestWorkLister: informerFactory.Work().V1().ManifestWorks().Lister().ManifestWorks("test"),
appliedManifestWorkClient: fakeClient.WorkV1().AppliedManifestWorks(),
appliedManifestWorkLister: informerFactory.Work().V1().AppliedManifestWorks().Lister(),
hubHash: c.hubHash,
agentID: c.agentID,
evictionGracePeriod: c.evictionGracePeriod,
rateLimiter: workqueue.NewItemExponentialFailureRateLimiter(0, c.evictionGracePeriod),
}
controllerContext := spoketesting.NewFakeSyncContext(t, c.hubHash+"-"+c.workName)
err = controller.sync(context.TODO(), controllerContext)
if err != nil {
controllerContext := spoketesting.NewFakeSyncContext(t, c.appliedManifestWorkName)
if err := controller.sync(context.TODO(), controllerContext); err != nil {
t.Errorf("Expect no sync error, but got %v", err)
}
appliedWorkAction := fakeClient.Actions()
c.validateAppliedManifestWorkActions(t, appliedWorkAction)
queueLen := controllerContext.Queue().Len()
if queueLen != c.expectedQueueLen {
t.Errorf("expected %d, but %d", c.expectedQueueLen, queueLen)
}
})
}
}
+14 -10
View File
@@ -28,21 +28,23 @@ import (
// WorkloadAgentOptions defines the flags for workload agent
type WorkloadAgentOptions struct {
HubKubeconfigFile string
SpokeKubeconfigFile string
SpokeClusterName string
QPS float32
Burst int
StatusSyncInterval time.Duration
AgentID string
HubKubeconfigFile string
SpokeKubeconfigFile string
SpokeClusterName string
AgentID string
Burst int
StatusSyncInterval time.Duration
AppliedManifestWorkEvictionGracePeriod time.Duration
QPS float32
}
// NewWorkloadAgentOptions returns the flags with default value set
func NewWorkloadAgentOptions() *WorkloadAgentOptions {
return &WorkloadAgentOptions{
QPS: 50,
Burst: 100,
StatusSyncInterval: 10 * time.Second,
QPS: 50,
Burst: 100,
StatusSyncInterval: 10 * time.Second,
AppliedManifestWorkEvictionGracePeriod: 10 * time.Minute,
}
}
@@ -59,6 +61,7 @@ func (o *WorkloadAgentOptions) AddFlags(cmd *cobra.Command) {
flags.Float32Var(&o.QPS, "spoke-kube-api-qps", o.QPS, "QPS to use while talking with apiserver on spoke cluster.")
flags.IntVar(&o.Burst, "spoke-kube-api-burst", o.Burst, "Burst to use while talking with apiserver on spoke cluster.")
flags.DurationVar(&o.StatusSyncInterval, "status-sync-interval", o.StatusSyncInterval, "Interval to sync resource status to hub.")
flags.DurationVar(&o.AppliedManifestWorkEvictionGracePeriod, "appliedmanifestwork-eviction-grace-period", o.AppliedManifestWorkEvictionGracePeriod, "Grace period for appliedmanifestwork eviction")
}
// RunWorkloadAgent starts the controllers on agent to process work from hub.
@@ -164,6 +167,7 @@ func (o *WorkloadAgentOptions) RunWorkloadAgent(ctx context.Context, controllerC
workInformerFactory.Work().V1().ManifestWorks().Lister().ManifestWorks(o.SpokeClusterName),
spokeWorkClient.WorkV1().AppliedManifestWorks(),
spokeWorkInformerFactory.Work().V1().AppliedManifestWorks(),
o.AppliedManifestWorkEvictionGracePeriod,
hubhash, agentID,
)
appliedManifestWorkController := appliedmanifestcontroller.NewAppliedManifestWorkController(
+131 -47
View File
@@ -29,12 +29,7 @@ var _ = ginkgo.Describe("Unmanaged ApplieManifestWork", func() {
var manifests []workapiv1.Manifest
var appliedManifestWorkName string
var err error
var newHubKubeConfigFile string
var newKubeClient kubernetes.Interface
var newWorkClient workclientset.Interface
var newHub *envtest.Environment
var newHubTempDir string
var ns *corev1.Namespace
ginkgo.BeforeEach(func() {
o = spoke.NewWorkloadAgentOptions()
@@ -42,8 +37,9 @@ var _ = ginkgo.Describe("Unmanaged ApplieManifestWork", func() {
o.SpokeClusterName = utilrand.String(5)
o.StatusSyncInterval = 3 * time.Second
o.AgentID = utilrand.String(5)
o.AppliedManifestWorkEvictionGracePeriod = 10 * time.Second
ns := &corev1.Namespace{}
ns = &corev1.Namespace{}
ns.Name = o.SpokeClusterName
_, err := spokeKubeClient.CoreV1().Namespaces().Create(context.Background(), ns, metav1.CreateOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
@@ -52,43 +48,15 @@ var _ = ginkgo.Describe("Unmanaged ApplieManifestWork", func() {
ctx, cancel = context.WithCancel(context.Background())
go startWorkAgent(ctx, o)
// start another hub
newHub = &envtest.Environment{
ErrorIfCRDPathMissing: true,
CRDDirectoryPaths: []string{
filepath.Join(".", "deploy", "hub"),
filepath.Join(".", "deploy", "spoke"),
},
manifests = []workapiv1.Manifest{
util.ToManifest(util.NewConfigmap(o.SpokeClusterName, "cm1", map[string]string{"a": "b"}, nil)),
}
newCfg, err := newHub.Start()
gomega.Expect(err).ToNot(gomega.HaveOccurred())
newHubTempDir, err = os.MkdirTemp("", "unmanaged_work_test")
gomega.Expect(err).ToNot(gomega.HaveOccurred())
newHubKubeConfigFile = path.Join(newHubTempDir, "kubeconfig")
err = util.CreateKubeconfigFile(newCfg, newHubKubeConfigFile)
gomega.Expect(err).ToNot(gomega.HaveOccurred())
newKubeClient, err = kubernetes.NewForConfig(newCfg)
gomega.Expect(err).ToNot(gomega.HaveOccurred())
newWorkClient, err = workclientset.NewForConfig(newCfg)
gomega.Expect(err).ToNot(gomega.HaveOccurred())
_, err = newKubeClient.CoreV1().Namespaces().Create(context.Background(), ns, metav1.CreateOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
// reset manifests
manifests = nil
})
ginkgo.JustBeforeEach(func() {
work = util.NewManifestWork(o.SpokeClusterName, "unmanaged-appliedwork", manifests)
_, err = hubWorkClient.WorkV1().ManifestWorks(o.SpokeClusterName).Create(context.Background(), work, metav1.CreateOptions{})
appliedManifestWorkName = fmt.Sprintf("%s-%s", hubHash, work.Name)
gomega.Expect(err).ToNot(gomega.HaveOccurred())
appliedManifestWorkName = fmt.Sprintf("%s-%s", hubHash, work.Name)
})
ginkgo.AfterEach(func() {
@@ -97,17 +65,53 @@ var _ = ginkgo.Describe("Unmanaged ApplieManifestWork", func() {
}
err := spokeKubeClient.CoreV1().Namespaces().Delete(context.Background(), o.SpokeClusterName, metav1.DeleteOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
err = newHub.Stop()
gomega.Expect(err).ToNot(gomega.HaveOccurred())
if newHubTempDir != "" {
os.RemoveAll(newHubTempDir)
}
})
ginkgo.Context("Should delete unmanaged applied work", func() {
ginkgo.Context("Should delete unmanaged applied work when hub changed", func() {
var newHubKubeConfigFile string
var newKubeClient kubernetes.Interface
var newWorkClient workclientset.Interface
var newHub *envtest.Environment
var newHubTempDir string
ginkgo.BeforeEach(func() {
manifests = []workapiv1.Manifest{
util.ToManifest(util.NewConfigmap(o.SpokeClusterName, "cm1", map[string]string{"a": "b"}, nil)),
// start another hub
newHub = &envtest.Environment{
ErrorIfCRDPathMissing: true,
CRDDirectoryPaths: []string{
filepath.Join(".", "deploy", "hub"),
filepath.Join(".", "deploy", "spoke"),
},
}
newCfg, err := newHub.Start()
gomega.Expect(err).ToNot(gomega.HaveOccurred())
newHubTempDir, err = os.MkdirTemp("", "unmanaged_work_test")
gomega.Expect(err).ToNot(gomega.HaveOccurred())
newHubKubeConfigFile = path.Join(newHubTempDir, "kubeconfig")
err = util.CreateKubeconfigFile(newCfg, newHubKubeConfigFile)
gomega.Expect(err).ToNot(gomega.HaveOccurred())
newKubeClient, err = kubernetes.NewForConfig(newCfg)
gomega.Expect(err).ToNot(gomega.HaveOccurred())
newWorkClient, err = workclientset.NewForConfig(newCfg)
gomega.Expect(err).ToNot(gomega.HaveOccurred())
_, err = newKubeClient.CoreV1().Namespaces().Create(context.Background(), ns, metav1.CreateOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
})
ginkgo.AfterEach(func() {
if cancel != nil {
cancel()
}
err := newHub.Stop()
gomega.Expect(err).ToNot(gomega.HaveOccurred())
if newHubTempDir != "" {
os.RemoveAll(newHubTempDir)
}
})
@@ -128,6 +132,7 @@ var _ = ginkgo.Describe("Unmanaged ApplieManifestWork", func() {
newOption.HubKubeconfigFile = newHubKubeConfigFile
newOption.SpokeClusterName = o.SpokeClusterName
newOption.AgentID = utilrand.String(5)
newOption.AppliedManifestWorkEvictionGracePeriod = 5 * time.Second
var ctx context.Context
ctx, cancel = context.WithCancel(context.Background())
@@ -172,6 +177,7 @@ var _ = ginkgo.Describe("Unmanaged ApplieManifestWork", func() {
newOption.HubKubeconfigFile = newHubKubeConfigFile
newOption.SpokeClusterName = o.SpokeClusterName
newOption.AgentID = o.AgentID
newOption.AppliedManifestWorkEvictionGracePeriod = 5 * time.Second
var ctx context.Context
ctx, cancel = context.WithCancel(context.Background())
@@ -215,4 +221,82 @@ var _ = ginkgo.Describe("Unmanaged ApplieManifestWork", func() {
})
})
ginkgo.Context("Should evict applied work when its manifestwork is missing on the hub", func() {
ginkgo.BeforeEach(func() {
util.AssertExistenceOfConfigMaps(manifests, spokeKubeClient, eventuallyTimeout, eventuallyInterval)
util.AssertWorkCondition(work.Namespace, work.Name, hubWorkClient, string(workapiv1.WorkApplied), metav1.ConditionTrue,
[]metav1.ConditionStatus{metav1.ConditionTrue}, eventuallyTimeout, eventuallyInterval)
util.AssertWorkCondition(work.Namespace, work.Name, hubWorkClient, string(workapiv1.WorkAvailable), metav1.ConditionTrue,
[]metav1.ConditionStatus{metav1.ConditionTrue}, eventuallyTimeout, eventuallyInterval)
// stop the agent
if cancel != nil {
cancel()
}
// fore delete the work from hub
err := forceDeleteManifestWork(context.TODO(), work)
gomega.Expect(err).ToNot(gomega.HaveOccurred())
})
ginkgo.It("should evict appliemanifestwork after the work agent reconnected to the hub", func() {
// restart the work agent
var ctx context.Context
ctx, cancel = context.WithCancel(context.Background())
go startWorkAgent(ctx, o)
// ensure the manifestwork is removed.
gomega.Eventually(func() error {
_, err := spokeWorkClient.WorkV1().AppliedManifestWorks().Get(context.TODO(), appliedManifestWorkName, metav1.GetOptions{})
if errors.IsNotFound(err) {
return nil
}
if err != nil {
return err
}
return fmt.Errorf("appliedmanifestwork %s still exists", appliedManifestWorkName)
}, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred())
})
ginkgo.It("should keep appliemanifestwork if its manifestwork is recreated on new hub", func() {
// restart the work agent
var ctx context.Context
ctx, cancel = context.WithCancel(context.Background())
go startWorkAgent(ctx, o)
// recreate the work on the hub
_, err = hubWorkClient.WorkV1().ManifestWorks(o.SpokeClusterName).Create(context.Background(), work, metav1.CreateOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
// ensure the appliemanifestwork eviction is stopped
gomega.Eventually(func() error {
appliedWork, err := spokeWorkClient.WorkV1().AppliedManifestWorks().Get(context.TODO(), appliedManifestWorkName, metav1.GetOptions{})
if err != nil {
return err
}
if appliedWork.Status.EvictionStartTime != nil {
return fmt.Errorf("the appliedmanifestwork %s evication is not stopped", appliedManifestWorkName)
}
return nil
}, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred())
})
})
})
func forceDeleteManifestWork(ctx context.Context, work *workapiv1.ManifestWork) error {
err := hubWorkClient.WorkV1().ManifestWorks(work.Namespace).Delete(ctx, work.Name, metav1.DeleteOptions{})
if err != nil {
return err
}
deleted, err := hubWorkClient.WorkV1().ManifestWorks(work.Namespace).Get(ctx, work.Name, metav1.GetOptions{})
if errors.IsNotFound(err) {
return nil
}
deleted.Finalizers = []string{}
_, err = hubWorkClient.WorkV1().ManifestWorks(work.Namespace).Update(ctx, deleted, metav1.UpdateOptions{})
return err
}
+19 -5
View File
@@ -46,6 +46,7 @@ var _ = ginkgo.Describe("ManifestWork", func() {
o.HubKubeconfigFile = hubKubeconfigFileName
o.SpokeClusterName = utilrand.String(5)
o.StatusSyncInterval = 3 * time.Second
o.AppliedManifestWorkEvictionGracePeriod = 5 * time.Second
ns := &corev1.Namespace{}
ns.Name = o.SpokeClusterName
@@ -396,11 +397,23 @@ var _ = ginkgo.Describe("ManifestWork", func() {
return errors.IsNotFound(err)
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
// Once manifest work is deleted, all CRs/CRD should have already been deleted too
for i := range gvrs {
_, err := util.GetResource(namespaces[i], names[i], gvrs[i], spokeDynamicClient)
gomega.Expect(errors.IsNotFound(err)).To(gomega.BeTrue())
}
// Once manifest work is not found, its relating appliedmanifestwork will be evicted, and finally,
// all CRs/CRD should been deleted too
gomega.Eventually(func() error {
for i := range gvrs {
_, err := util.GetResource(namespaces[i], names[i], gvrs[i], spokeDynamicClient)
if errors.IsNotFound(err) {
continue
}
if err != nil {
return err
}
return fmt.Errorf("the resource %s/%s still exists", namespaces[i], names[i])
}
return nil
}, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred())
})
})
@@ -673,6 +686,7 @@ var _ = ginkgo.Describe("ManifestWork", func() {
},
Spec: workapiv1.AppliedManifestWorkSpec{
HubHash: hubHash,
AgentID: hubHash,
ManifestWorkName: "fakework",
},
}