Skip to content

Commit 37f2663

Browse files
author
Antoine Pelisse
committed
Fix eviction dry-run
1 parent bd12b01 commit 37f2663

File tree

4 files changed

+167
-9
lines changed

4 files changed

+167
-9
lines changed

pkg/registry/core/pod/storage/BUILD

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,17 @@ go_test(
2020
"//pkg/securitycontext:go_default_library",
2121
"//staging/src/k8s.io/api/core/v1:go_default_library",
2222
"//staging/src/k8s.io/api/policy/v1beta1:go_default_library",
23+
"//staging/src/k8s.io/apimachinery/pkg/api/apitesting:go_default_library",
2324
"//staging/src/k8s.io/apimachinery/pkg/api/equality:go_default_library",
2425
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
2526
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
2627
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1beta1:go_default_library",
2728
"//staging/src/k8s.io/apimachinery/pkg/fields:go_default_library",
2829
"//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library",
2930
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
31+
"//staging/src/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
3032
"//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library",
33+
"//staging/src/k8s.io/apiserver/pkg/apis/example/v1:go_default_library",
3134
"//staging/src/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
3235
"//staging/src/k8s.io/apiserver/pkg/registry/generic:go_default_library",
3336
"//staging/src/k8s.io/apiserver/pkg/registry/generic/registry:go_default_library",

pkg/registry/core/pod/storage/eviction.go

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package storage
1919
import (
2020
"context"
2121
"fmt"
22+
"reflect"
2223
"time"
2324

2425
policyv1beta1 "k8s.io/api/policy/v1beta1"
@@ -30,6 +31,7 @@ import (
3031
"k8s.io/apimachinery/pkg/util/wait"
3132
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
3233
"k8s.io/apiserver/pkg/registry/rest"
34+
"k8s.io/apiserver/pkg/util/dryrun"
3335
policyclient "k8s.io/client-go/kubernetes/typed/policy/v1beta1"
3436
"k8s.io/client-go/util/retry"
3537
api "k8s.io/kubernetes/pkg/apis/core"
@@ -78,19 +80,44 @@ func (r *EvictionREST) New() runtime.Object {
7880
return &policy.Eviction{}
7981
}
8082

83+
// Propagate dry-run takes the dry-run option from the request and pushes it into the eviction object.
84+
// It returns an error if they have non-matching dry-run options.
85+
func propagateDryRun(eviction *policy.Eviction, options *metav1.CreateOptions) (*metav1.DeleteOptions, error) {
86+
if eviction.DeleteOptions == nil {
87+
return &metav1.DeleteOptions{DryRun: options.DryRun}, nil
88+
}
89+
if len(eviction.DeleteOptions.DryRun) == 0 {
90+
eviction.DeleteOptions.DryRun = options.DryRun
91+
return eviction.DeleteOptions, nil
92+
}
93+
if len(options.DryRun) == 0 {
94+
return eviction.DeleteOptions, nil
95+
}
96+
97+
if !reflect.DeepEqual(options.DryRun, eviction.DeleteOptions.DryRun) {
98+
return nil, fmt.Errorf("Non-matching dry-run options in request and content: %v and %v", options.DryRun, eviction.DeleteOptions.DryRun)
99+
}
100+
return eviction.DeleteOptions, nil
101+
}
102+
81103
// Create attempts to create a new eviction. That is, it tries to evict a pod.
82104
func (r *EvictionREST) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
83105
eviction := obj.(*policy.Eviction)
84106

85-
obj, err := r.store.Get(ctx, eviction.Name, &metav1.GetOptions{})
107+
deletionOptions, err := propagateDryRun(eviction, options)
108+
if err != nil {
109+
return nil, err
110+
}
111+
112+
obj, err = r.store.Get(ctx, eviction.Name, &metav1.GetOptions{})
86113
if err != nil {
87114
return nil, err
88115
}
89116
pod := obj.(*api.Pod)
90117
// Evicting a terminal pod should result in direct deletion of pod as it already caused disruption by the time we are evicting.
91118
// There is no need to check for pdb.
92119
if pod.Status.Phase == api.PodSucceeded || pod.Status.Phase == api.PodFailed {
93-
_, _, err = r.store.Delete(ctx, eviction.Name, eviction.DeleteOptions)
120+
_, _, err = r.store.Delete(ctx, eviction.Name, deletionOptions)
94121
if err != nil {
95122
return nil, err
96123
}
@@ -119,7 +146,7 @@ func (r *EvictionREST) Create(ctx context.Context, obj runtime.Object, createVal
119146

120147
// If it was false already, or if it becomes false during the course of our retries,
121148
// raise an error marked as a 429.
122-
if err := r.checkAndDecrement(pod.Namespace, pod.Name, pdb); err != nil {
149+
if err := r.checkAndDecrement(pod.Namespace, pod.Name, pdb, dryrun.IsDryRun(deletionOptions.DryRun)); err != nil {
123150
return err
124151
}
125152
}
@@ -139,11 +166,6 @@ func (r *EvictionREST) Create(ctx context.Context, obj runtime.Object, createVal
139166
// At this point there was either no PDB or we succeeded in decrementing
140167

141168
// Try the delete
142-
deletionOptions := eviction.DeleteOptions
143-
if deletionOptions == nil {
144-
// default to non-nil to trigger graceful deletion
145-
deletionOptions = &metav1.DeleteOptions{}
146-
}
147169
_, _, err = r.store.Delete(ctx, eviction.Name, deletionOptions)
148170
if err != nil {
149171
return nil, err
@@ -154,7 +176,7 @@ func (r *EvictionREST) Create(ctx context.Context, obj runtime.Object, createVal
154176
}
155177

156178
// checkAndDecrement checks if the provided PodDisruptionBudget allows any disruption.
157-
func (r *EvictionREST) checkAndDecrement(namespace string, podName string, pdb policyv1beta1.PodDisruptionBudget) error {
179+
func (r *EvictionREST) checkAndDecrement(namespace string, podName string, pdb policyv1beta1.PodDisruptionBudget, dryRun bool) error {
158180
if pdb.Status.ObservedGeneration < pdb.Generation {
159181
// TODO(mml): Add a Retry-After header. Once there are time-based
160182
// budgets, we can sometimes compute a sensible suggested value. But
@@ -180,6 +202,12 @@ func (r *EvictionREST) checkAndDecrement(namespace string, podName string, pdb p
180202
if pdb.Status.DisruptedPods == nil {
181203
pdb.Status.DisruptedPods = make(map[string]metav1.Time)
182204
}
205+
206+
// If this is a dry-run, we don't need to go any further than that.
207+
if dryRun == true {
208+
return nil
209+
}
210+
183211
// Eviction handler needs to inform the PDB controller that it is about to delete a pod
184212
// so it should not consider it as available in calculations when updating PodDisruptions allowed.
185213
// If the pod is not deleted within a reasonable time limit PDB controller will assume that it won't

pkg/registry/core/pod/storage/eviction_test.go

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,25 @@ limitations under the License.
1717
package storage
1818

1919
import (
20+
"context"
2021
"testing"
2122

2223
policyv1beta1 "k8s.io/api/policy/v1beta1"
24+
"k8s.io/apimachinery/pkg/api/apitesting"
2325
apierrors "k8s.io/apimachinery/pkg/api/errors"
2426
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2527
"k8s.io/apimachinery/pkg/runtime"
28+
"k8s.io/apimachinery/pkg/runtime/serializer"
29+
examplev1 "k8s.io/apiserver/pkg/apis/example/v1"
2630
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
31+
"k8s.io/apiserver/pkg/registry/generic"
32+
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
33+
"k8s.io/apiserver/pkg/storage"
34+
etcdtesting "k8s.io/apiserver/pkg/storage/etcd/testing"
2735
"k8s.io/client-go/kubernetes/fake"
2836
api "k8s.io/kubernetes/pkg/apis/core"
2937
"k8s.io/kubernetes/pkg/apis/policy"
38+
"k8s.io/kubernetes/pkg/registry/registrytest"
3039
)
3140

3241
func TestEviction(t *testing.T) {
@@ -136,3 +145,119 @@ func TestEviction(t *testing.T) {
136145
})
137146
}
138147
}
148+
149+
type FailDeleteUpdateStorage struct {
150+
storage.Interface
151+
}
152+
153+
func (f FailDeleteUpdateStorage) Delete(ctx context.Context, key string, out runtime.Object, precondition *storage.Preconditions) error {
154+
return storage.NewKeyNotFoundError(key, 0)
155+
}
156+
157+
func (f FailDeleteUpdateStorage) GuaranteedUpdate(ctx context.Context, key string, ptrToType runtime.Object, ignoreNotFound bool,
158+
preconditions *storage.Preconditions, tryUpdate storage.UpdateFunc, suggestion ...runtime.Object) error {
159+
return storage.NewKeyNotFoundError(key, 0)
160+
}
161+
162+
var scheme = runtime.NewScheme()
163+
var codecs = serializer.NewCodecFactory(scheme)
164+
165+
func newFailDeleteUpdateStorage(t *testing.T) (*REST, *etcdtesting.EtcdTestServer) {
166+
etcdStorage, server := registrytest.NewEtcdStorage(t, "")
167+
restOptions := generic.RESTOptions{
168+
StorageConfig: etcdStorage,
169+
Decorator: generic.UndecoratedStorage,
170+
DeleteCollectionWorkers: 3,
171+
ResourcePrefix: "pods",
172+
}
173+
storage := NewStorage(restOptions, nil, nil, nil)
174+
storage.Pod.Store.Storage = genericregistry.DryRunnableStorage{
175+
Storage: FailDeleteUpdateStorage{storage.Pod.Store.Storage.Storage},
176+
Codec: apitesting.TestStorageCodec(codecs, examplev1.SchemeGroupVersion),
177+
}
178+
return storage.Pod, server
179+
}
180+
181+
func TestEvictionDryRun(t *testing.T) {
182+
testcases := []struct {
183+
name string
184+
evictionOptions *metav1.DeleteOptions
185+
requestOptions *metav1.CreateOptions
186+
pod *api.Pod
187+
pdbs []runtime.Object
188+
}{
189+
{
190+
name: "just request-options",
191+
requestOptions: &metav1.CreateOptions{DryRun: []string{"All"}},
192+
evictionOptions: &metav1.DeleteOptions{},
193+
pod: func() *api.Pod {
194+
pod := validNewPod()
195+
pod.Labels = map[string]string{"a": "true"}
196+
pod.Spec.NodeName = "foo"
197+
return pod
198+
}(),
199+
},
200+
{
201+
name: "just eviction-options",
202+
requestOptions: &metav1.CreateOptions{},
203+
evictionOptions: &metav1.DeleteOptions{DryRun: []string{"All"}},
204+
pod: func() *api.Pod {
205+
pod := validNewPod()
206+
pod.Labels = map[string]string{"a": "true"}
207+
pod.Spec.NodeName = "foo"
208+
return pod
209+
}(),
210+
},
211+
{
212+
name: "both options",
213+
evictionOptions: &metav1.DeleteOptions{DryRun: []string{"All"}},
214+
requestOptions: &metav1.CreateOptions{DryRun: []string{"All"}},
215+
pod: func() *api.Pod {
216+
pod := validNewPod()
217+
pod.Labels = map[string]string{"a": "true"}
218+
pod.Spec.NodeName = "foo"
219+
return pod
220+
}(),
221+
},
222+
{
223+
name: "with pdbs",
224+
evictionOptions: &metav1.DeleteOptions{DryRun: []string{"All"}},
225+
requestOptions: &metav1.CreateOptions{DryRun: []string{"All"}},
226+
pod: func() *api.Pod {
227+
pod := validNewPod()
228+
pod.Labels = map[string]string{"a": "true"}
229+
pod.Spec.NodeName = "foo"
230+
return pod
231+
}(),
232+
pdbs: []runtime.Object{&policyv1beta1.PodDisruptionBudget{
233+
ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "default"},
234+
Spec: policyv1beta1.PodDisruptionBudgetSpec{Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"a": "true"}}},
235+
Status: policyv1beta1.PodDisruptionBudgetStatus{PodDisruptionsAllowed: 1},
236+
}},
237+
},
238+
}
239+
240+
for _, tc := range testcases {
241+
t.Run(tc.name, func(t *testing.T) {
242+
testContext := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
243+
storage, server := newFailDeleteUpdateStorage(t)
244+
defer server.Terminate(t)
245+
defer storage.Store.DestroyFunc()
246+
247+
pod := validNewPod()
248+
pod.Labels = map[string]string{"a": "true"}
249+
pod.Spec.NodeName = "foo"
250+
if _, err := storage.Create(testContext, pod, nil, &metav1.CreateOptions{}); err != nil {
251+
t.Error(err)
252+
}
253+
254+
client := fake.NewSimpleClientset()
255+
evictionRest := newEvictionStorage(storage.Store, client.PolicyV1beta1())
256+
eviction := &policy.Eviction{ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "default"}, DeleteOptions: tc.evictionOptions}
257+
_, err := evictionRest.Create(testContext, eviction, nil, tc.requestOptions)
258+
if err != nil {
259+
t.Fatalf("Failed to run eviction: %v", err)
260+
}
261+
})
262+
}
263+
}

vendor/modules.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1202,6 +1202,8 @@ k8s.io/apiserver/pkg/apis/audit/v1beta1
12021202
k8s.io/apiserver/pkg/apis/audit/validation
12031203
k8s.io/apiserver/pkg/apis/config
12041204
k8s.io/apiserver/pkg/apis/config/v1
1205+
k8s.io/apiserver/pkg/apis/example
1206+
k8s.io/apiserver/pkg/apis/example/v1
12051207
k8s.io/apiserver/pkg/audit
12061208
k8s.io/apiserver/pkg/audit/event
12071209
k8s.io/apiserver/pkg/audit/policy

0 commit comments

Comments
 (0)