Skip to content

Commit

Permalink
[gardenlet] Switch BackupBucket controller to controller-runtime (#…
Browse files Browse the repository at this point in the history
…6837)

* Add documentation

* Use clock.Clock in the reconciler

* Skip queuing on update if `newBackupBucket.Generation == oldBackupBucket.Generation`

This was done because the earlier comparison of
`status.ObservedGeneration` caused multiple reconciliations due to the
Status updation by the flow progressReporter. Later, after refactoring,
`GenerationChangedPredicate` will be used.

* Add integration tests

* Use spec.seedName field selector for BackupBucket cache

This way, we don't have to filter unrelated `BackupBucket`s at
the controller level (e.g., with predicates). The underlying
controller-runtime cache already makes sure that we only see objects
which are related to our seed now.

* Refactor backupbucket controller to native controller-runtime controller

* Drop no longer needed code

* Add spec.bucketName field selector to BackupEntry

* Minor code changes

* Fix failing tests

* Rename files

* Address review comments

* [Address review comments] Do not hardcode Garden namespace name

* Use ExpectWithOffSet and DeferCleanup

* Remove acuator and move code to reconciler

* Move seed creation to BeforeSuite

* Address PR review feedback

* Use clock.Clock in reconciler for BackupEntry controller

* Add unit tests for eventhandler

* Do no restrict gardenlet's cache

* Watch extension BackupBuckets and map to BackupBuckets

* Eliminate wait calls for extension

* Improve integration tests

* Address PR review feedback

* Address PR review feedback: Improve predicates

* Only reconcile extension backupbucket when state is failed

* Reuse v1betahelper.HasOperationAnnotation function

* Address PR review feedback

* Move ExtensionStatusChanged predicate to predicate utils

* Minor nits

stress -ignore "unable to grab random port" -p 32 ./test/integration/gardenlet/backupbucket/backupbucket.test                                                                                                    ─╯
Before:
```
3m55s: 391 runs so far, 2 failures (0.51%)
4m0s: 403 runs so far, 2 failures (0.50%)
4m5s: 413 runs so far, 2 failures (0.48%)
```
After:
```
7m35s: 728 runs so far, 0 failures
7m40s: 729 runs so far, 0 failures
7m45s: 740 runs so far, 0 failures
7m50s: 743 runs so far, 0 failures
7m55s: 760 runs so far, 0 failures
8m0s: 761 runs so far, 0 failures
8m5s: 770 runs so far, 0 failures
```

* Update status.ObservedGeneration at the beginning of reconciliation

* Requeue extension on Create only if State is failed

* Add dependencies in skaffold.yaml

* Apply suggestions from code review

* Remove operation annotation in the apiserver

* Deflake integration tests

```
stress -ignore "unable to grab random port" -p 32 ./test/integration/gardenlet/backupbucket/backupbucket.test
...
26m10s: 1978 runs so far, 0 failures
26m15s: 1987 runs so far, 0 failures
26m20s: 1993 runs so far, 0 failures
26m25s: 1998 runs so far, 0 failures
26m30s: 2004 runs so far, 0 failures
26m35s: 2010 runs so far, 0 failures
```
  • Loading branch information
shafeeqes committed Nov 9, 2022
1 parent fc2fa73 commit b9a7250
Show file tree
Hide file tree
Showing 36 changed files with 1,848 additions and 928 deletions.
1 change: 1 addition & 0 deletions cmd/gardenlet/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,7 @@ func addAllFieldIndexes(ctx context.Context, i client.FieldIndexer) error {
indexer.AddShootStatusSeedName,
indexer.AddBackupBucketSeedName,
indexer.AddBackupEntrySeedName,
indexer.AddBackupEntryBucketName,
indexer.AddControllerInstallationSeedRefName,
indexer.AddControllerInstallationRegistrationRefName,
// operations API group
Expand Down
10 changes: 10 additions & 0 deletions docs/concepts/gardenlet.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,16 @@ retries until the connection is reestablished.

The gardenlet consists out of several controllers which are now described in more detail.

### [`BackupBucket` Controller](../../pkg/gardenlet/controller/backupbucket)

The `BackupBucket` controller reconciles those `core.gardener.cloud/v1beta1.BackupBucket` resources whose `.spec.seedName` value is equal to the name of the `Seed` the respective `gardenlet` is responsible for.
A `core.gardener.cloud/v1beta1.BackupBucket` resource is created by the `Seed` controller if `.spec.backup` is defined in the `Seed`.

The controller adds finalizers to the `BackupBucket` and the secret mentioned in the `.spec.secretRef` of the `BackupBucket`. The controller also copies this secret to the seed cluster. Additionally, it creates an `extensions.gardener.cloud/v1alpha1.BackupBucket` resource (non-namespaced) in the seed cluster and waits until the responsible extension controller reconciles it (see [this](../extensions/backupbucket.md) for more details).
The status from the reconciliation is reported in the `.status.lastOperation` field. Once the extension resource is ready and the `.status.generatedSecretRef` is set by the extension controller, `gardenlet` copies the referenced secret to the `garden` namespace in the garden cluster. An owner reference to the `core.gardener.cloud/v1beta1.BackupBucket` is added to this secret.

If the `core.gardener.cloud/v1beta1.BackupBucket` is deleted, the controller deletes the generated secret in the garden cluster and the `extensions.gardener.cloud/v1alpha1.BackupBucket` resource in the seed cluster and it waits for the respective extension controller to remove its finalizers from the `extensions.gardener.cloud/v1alpha1.BackupBucket`. Then it deletes the secret in the seed cluster and finally removes the finalizers from the `core.gardener.cloud/v1beta1.BackupBucket` and the referred secret.

### `BackupEntry` Controller

The `BackupEntry` controller reconciles those `core.gardener.cloud/v1beta1.BackupEntry` resources whose `.spec.seedName` value is equal to the name of a `Seed` the respective gardenlet is responsible for.
Expand Down
14 changes: 14 additions & 0 deletions pkg/api/indexer/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,20 @@ func AddBackupEntrySeedName(ctx context.Context, indexer client.FieldIndexer) er
return nil
}

// AddBackupEntryBucketName adds an index for core.BackupEntryBucketName to the given indexer.
func AddBackupEntryBucketName(ctx context.Context, indexer client.FieldIndexer) error {
if err := indexer.IndexField(ctx, &gardencorev1beta1.BackupEntry{}, core.BackupEntryBucketName, func(obj client.Object) []string {
backupEntry, ok := obj.(*gardencorev1beta1.BackupEntry)
if !ok {
return []string{""}
}
return []string{backupEntry.Spec.BucketName}
}); err != nil {
return fmt.Errorf("failed to add indexer for %s to BackupEntry Informer: %w", core.BackupEntryBucketName, err)
}
return nil
}

// AddControllerInstallationSeedRefName adds an index for core.ControllerInstallationSeedRefName to the given indexer.
func AddControllerInstallationSeedRefName(ctx context.Context, indexer client.FieldIndexer) error {
if err := indexer.IndexField(ctx, &gardencorev1beta1.ControllerInstallation{}, core.SeedRefName, func(obj client.Object) []string {
Expand Down
15 changes: 15 additions & 0 deletions pkg/api/indexer/core_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,21 @@ var _ = Describe("Core", func() {
Entry("BackupEntry w/ seedName", &gardencorev1beta1.BackupEntry{Spec: gardencorev1beta1.BackupEntrySpec{SeedName: pointer.String("seed")}}, ConsistOf("seed")),
)

DescribeTable("#AddBackupEntryBucketName",
func(obj client.Object, matcher gomegatypes.GomegaMatcher) {
Expect(AddBackupEntryBucketName(context.TODO(), indexer)).To(Succeed())

Expect(indexer.obj).To(Equal(&gardencorev1beta1.BackupEntry{}))
Expect(indexer.field).To(Equal("spec.bucketName"))
Expect(indexer.extractValue).NotTo(BeNil())
Expect(indexer.extractValue(obj)).To(matcher)
},

Entry("no BackupEntry", &corev1.Secret{}, ConsistOf("")),
Entry("BackupEntry w/o bucketName", &gardencorev1beta1.BackupEntry{}, ConsistOf("")),
Entry("BackupEntry w/ bucketName", &gardencorev1beta1.BackupEntry{Spec: gardencorev1beta1.BackupEntrySpec{BucketName: "bucket"}}, ConsistOf("bucket")),
)

DescribeTable("#AddControllerInstallationSeedRefName",
func(obj client.Object, matcher gomegatypes.GomegaMatcher) {
Expect(AddControllerInstallationSeedRefName(context.TODO(), indexer)).To(Succeed())
Expand Down
9 changes: 6 additions & 3 deletions pkg/apis/core/field_constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,19 @@ const (
// BackupEntrySeedName is the field selector path for finding
// the Seed cluster of a core.gardener.cloud/v1beta1 BackupEntry.
BackupEntrySeedName = "spec.seedName"
// BackupEntrySeedName is the field selector path for finding
// the BackupBucket for a core.gardener.cloud/v1beta1 BackupEntry.
BackupEntryBucketName = "spec.bucketName"

// ProjectNamespace is the field selector path for filtering by namespace
// for core.gardener.cloud/{v1beta1,v1beta1} Project.
// for core.gardener.cloud/v1beta1 Project.
ProjectNamespace = "spec.namespace"

// RegistrationRefName is the field selector path for finding
// the ControllerRegistration name of a core.gardener.cloud/{v1beta1,v1beta1} ControllerInstallation.
// the ControllerRegistration name of a core.gardener.cloud/{v1alpha1,v1beta1} ControllerInstallation.
RegistrationRefName = "spec.registrationRef.name"
// SeedRefName is the field selector path for finding
// the Seed name of a core.gardener.cloud/{v1beta1,v1beta1} ControllerInstallation.
// the Seed name of a core.gardener.cloud/{v1alpha1,v1beta1} ControllerInstallation.
SeedRefName = "spec.seedRef.name"

// ShootCloudProfileName is the field selector path for finding
Expand Down
2 changes: 1 addition & 1 deletion pkg/apis/core/v1beta1/conversions.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func addConversionFuncs(scheme *runtime.Scheme) error {
SchemeGroupVersion.WithKind("BackupEntry"),
func(label, value string) (string, string, error) {
switch label {
case "metadata.name", "metadata.namespace", core.BackupEntrySeedName:
case "metadata.name", "metadata.namespace", core.BackupEntrySeedName, core.BackupEntryBucketName:
return label, value, nil
default:
return "", "", fmt.Errorf("field label not supported: %s", label)
Expand Down
8 changes: 4 additions & 4 deletions pkg/apis/core/v1beta1/helper/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,10 +243,10 @@ func ComputeOperationType(meta metav1.ObjectMeta, lastOperation *gardencorev1bet
}

// HasOperationAnnotation returns true if the operation annotation is present and its value is "reconcile", "restore, or "migrate".
func HasOperationAnnotation(meta metav1.ObjectMeta) bool {
return meta.Annotations[v1beta1constants.GardenerOperation] == v1beta1constants.GardenerOperationReconcile ||
meta.Annotations[v1beta1constants.GardenerOperation] == v1beta1constants.GardenerOperationRestore ||
meta.Annotations[v1beta1constants.GardenerOperation] == v1beta1constants.GardenerOperationMigrate
func HasOperationAnnotation(annotations map[string]string) bool {
return annotations[v1beta1constants.GardenerOperation] == v1beta1constants.GardenerOperationReconcile ||
annotations[v1beta1constants.GardenerOperation] == v1beta1constants.GardenerOperationRestore ||
annotations[v1beta1constants.GardenerOperation] == v1beta1constants.GardenerOperationMigrate
}

// TaintsHave returns true if the given key is part of the taints list.
Expand Down
2 changes: 1 addition & 1 deletion pkg/apis/core/v1beta1/helper/helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,7 @@ var _ = Describe("helper", func() {

DescribeTable("#HasOperationAnnotation",
func(objectMeta metav1.ObjectMeta, expected bool) {
Expect(HasOperationAnnotation(objectMeta)).To(Equal(expected))
Expect(HasOperationAnnotation(objectMeta.Annotations)).To(Equal(expected))
},
Entry("reconcile", metav1.ObjectMeta{Annotations: map[string]string{v1beta1constants.GardenerOperation: v1beta1constants.GardenerOperationReconcile}}, true),
Entry("restore", metav1.ObjectMeta{Annotations: map[string]string{v1beta1constants.GardenerOperation: v1beta1constants.GardenerOperationRestore}}, true),
Expand Down
73 changes: 73 additions & 0 deletions pkg/controllerutils/eventhandler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright (c) 2022 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file
//
// 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 controllerutils

import (
"time"

"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/workqueue"
"k8s.io/utils/clock"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/reconcile"

gardencorev1beta1 "github.com/gardener/gardener/pkg/apis/core/v1beta1"
)

// EnqueueCreateEventsOncePer24hDuration returns handler.Funcs which enqueues the object for Create events only once per 24h.
// All other events are normally enqueued.
func EnqueueCreateEventsOncePer24hDuration(clock clock.Clock) handler.Funcs {
return handler.Funcs{
CreateFunc: func(evt event.CreateEvent, q workqueue.RateLimitingInterface) {
if evt.Object == nil {
return
}
q.AddAfter(reconcile.Request{NamespacedName: types.NamespacedName{
Name: evt.Object.GetName(),
Namespace: evt.Object.GetNamespace(),
}}, getDuration(evt.Object, clock))
},
UpdateFunc: func(evt event.UpdateEvent, q workqueue.RateLimitingInterface) {
if evt.ObjectNew == nil {
return
}
q.Add(reconcile.Request{NamespacedName: types.NamespacedName{
Name: evt.ObjectNew.GetName(),
Namespace: evt.ObjectNew.GetNamespace(),
}})
},
DeleteFunc: func(evt event.DeleteEvent, q workqueue.RateLimitingInterface) {
if evt.Object == nil {
return
}
q.Add(reconcile.Request{NamespacedName: types.NamespacedName{
Name: evt.Object.GetName(),
Namespace: evt.Object.GetNamespace(),
}})
},
}
}

func getDuration(obj client.Object, clock clock.Clock) time.Duration {
switch obj := obj.(type) {
case *gardencorev1beta1.BackupBucket:
return ReconcileOncePer24hDuration(clock, obj.ObjectMeta, obj.Status.ObservedGeneration, obj.Status.LastOperation)
case *gardencorev1beta1.BackupEntry:
return ReconcileOncePer24hDuration(clock, obj.ObjectMeta, obj.Status.ObservedGeneration, obj.Status.LastOperation)
}
return 0
}
120 changes: 120 additions & 0 deletions pkg/controllerutils/eventhandler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright (c) 2022 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file
//
// 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 controllerutils_test

import (
"time"

gardencorev1beta1 "github.com/gardener/gardener/pkg/apis/core/v1beta1"
. "github.com/gardener/gardener/pkg/controllerutils"
"github.com/gardener/gardener/pkg/utils/test"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/workqueue"
testclock "k8s.io/utils/clock/testing"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

var _ = Describe("EventHandler", func() {
Describe("#EnqueueCreateEventsOncePer24hDuration", func() {
var (
handlerFuncs = handler.Funcs{}
queue workqueue.RateLimitingInterface
fakeClock *testclock.FakeClock

backupBucket *gardencorev1beta1.BackupBucket
)

BeforeEach(func() {
fakeClock = testclock.NewFakeClock(time.Now())
queue = workqueue.NewRateLimitingQueueWithDelayingInterface(workqueue.NewDelayingQueueWithCustomClock(fakeClock, ""), workqueue.DefaultControllerRateLimiter())
handlerFuncs = EnqueueCreateEventsOncePer24hDuration(fakeClock)

backupBucket = &gardencorev1beta1.BackupBucket{
ObjectMeta: metav1.ObjectMeta{
Name: "bar",
Namespace: "foo",
Generation: 1,
},
Status: gardencorev1beta1.BackupBucketStatus{
ObservedGeneration: 1,
LastOperation: &gardencorev1beta1.LastOperation{
Type: gardencorev1beta1.LastOperationTypeReconcile,
State: gardencorev1beta1.LastOperationStateSucceeded,
LastUpdateTime: metav1.NewTime(fakeClock.Now()),
},
},
}
})

It("should enqueue a Request with the Name / Namespace of the object in the CreateEvent immediately if the last reconciliation was 24H ago", func() {
evt := event.CreateEvent{
Object: backupBucket,
}
fakeClock.Step(24 * time.Hour)
handlerFuncs.Create(evt, queue)
verifyQueue(queue)
})

It("should enqueue a Request with the Name / Namespace of the object in the CreateEvent after a random duration if the last reconciliation was not 24H ago", func() {
DeferCleanup(test.WithVars(&RandomDuration, func(time.Duration) time.Duration { return 250 * time.Millisecond }))
evt := event.CreateEvent{
Object: backupBucket,
}
handlerFuncs.Create(evt, queue)
Expect(queue.Len()).To(Equal(0))
fakeClock.Step(1 * time.Second)
Eventually(func() int {
return queue.Len()
}).Should(Equal(1))
verifyQueue(queue)
})

It("should enqueue a Request with the Name / Namespace of the object in the UpdateEvent.", func() {
evt := event.UpdateEvent{
ObjectNew: backupBucket,
ObjectOld: backupBucket,
}
handlerFuncs.Update(evt, queue)
verifyQueue(queue)
})

It("should enqueue a Request with the Name / Namespace of the object in the DeleteEvent.", func() {
evt := event.DeleteEvent{
Object: backupBucket,
}
handlerFuncs.Delete(evt, queue)
verifyQueue(queue)
})
})
})

func verifyQueue(queue workqueue.RateLimitingInterface) {
ExpectWithOffset(1, queue.Len()).To(Equal(1))

i, _ := queue.Get()
ExpectWithOffset(1, i).NotTo(BeNil())
req, ok := i.(reconcile.Request)
ExpectWithOffset(1, ok).To(BeTrue())
ExpectWithOffset(1, req.NamespacedName).To(Equal(types.NamespacedName{
Name: "bar",
Namespace: "foo",
}))
}
11 changes: 5 additions & 6 deletions pkg/controllerutils/miscellaneous.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/gardener/gardener/pkg/utils"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/clock"
)

const separator = ","
Expand Down Expand Up @@ -87,8 +88,6 @@ func setTaskAnnotations(annotations map[string]string, tasks []string) {
}

var (
// Now is a function for returning the current time.
Now = time.Now
// RandomDuration is a function for returning a random duration.
RandomDuration = utils.RandomDuration
)
Expand All @@ -97,7 +96,7 @@ var (
// only one reconciliation should happen per 24h. If the deletion timestamp is set or the generation has changed or the
// last operation does not indicate success or indicates that the last reconciliation happened more than 24h ago then 0
// will be returned.
func ReconcileOncePer24hDuration(objectMeta metav1.ObjectMeta, observedGeneration int64, lastOperation *gardencorev1beta1.LastOperation) time.Duration {
func ReconcileOncePer24hDuration(clock clock.Clock, objectMeta metav1.ObjectMeta, observedGeneration int64, lastOperation *gardencorev1beta1.LastOperation) time.Duration {
if objectMeta.DeletionTimestamp != nil {
return 0
}
Expand All @@ -106,7 +105,7 @@ func ReconcileOncePer24hDuration(objectMeta metav1.ObjectMeta, observedGeneratio
return 0
}

if v1beta1helper.HasOperationAnnotation(objectMeta) {
if v1beta1helper.HasOperationAnnotation(objectMeta.Annotations) {
return 0
}

Expand All @@ -118,8 +117,8 @@ func ReconcileOncePer24hDuration(objectMeta metav1.ObjectMeta, observedGeneratio

// If last reconciliation happened more than 24h ago then we want to reconcile immediately, so let's only compute
// a delay if the last reconciliation was within the last 24h.
if lastReconciliation := lastOperation.LastUpdateTime.Time; Now().UTC().Before(lastReconciliation.UTC().Add(24 * time.Hour)) {
durationUntilLastReconciliationWas24hAgo := lastReconciliation.UTC().Add(24 * time.Hour).Sub(Now().UTC())
if lastReconciliation := lastOperation.LastUpdateTime.Time; clock.Now().UTC().Before(lastReconciliation.UTC().Add(24 * time.Hour)) {
durationUntilLastReconciliationWas24hAgo := lastReconciliation.UTC().Add(24 * time.Hour).Sub(clock.Now().UTC())
return RandomDuration(durationUntilLastReconciliationWas24hAgo)
}

Expand Down
7 changes: 3 additions & 4 deletions pkg/controllerutils/miscellaneous_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
testclock "k8s.io/utils/clock/testing"
)

var _ = Describe("controller", func() {
Expand Down Expand Up @@ -117,15 +118,13 @@ var _ = Describe("controller", func() {
var deletionTimestamp = metav1.Now()
DescribeTable("#ReconcileOncePer24hDuration",
func(objectMeta metav1.ObjectMeta, observedGeneration int64, lastOperation *gardencorev1beta1.LastOperation, expectedDuration time.Duration) {
oldNow := Now
defer func() { Now = oldNow }()
Now = func() time.Time { return time.Date(1, 1, 2, 1, 0, 0, 0, time.UTC) }
fakeClock := testclock.NewFakeClock(time.Date(1, 1, 2, 1, 0, 0, 0, time.UTC))

oldRandomDuration := RandomDuration
defer func() { RandomDuration = oldRandomDuration }()
RandomDuration = func(time.Duration) time.Duration { return time.Minute }

Expect(ReconcileOncePer24hDuration(objectMeta, observedGeneration, lastOperation)).To(Equal(expectedDuration))
Expect(ReconcileOncePer24hDuration(fakeClock, objectMeta, observedGeneration, lastOperation)).To(Equal(expectedDuration))
},

Entry("deletion timestamp set", metav1.ObjectMeta{DeletionTimestamp: &deletionTimestamp}, int64(0), nil, time.Duration(0)),
Expand Down
Loading

0 comments on commit b9a7250

Please sign in to comment.