Skip to content

Commit daf76e6

Browse files
authored
Merge pull request kubernetes#127778 from tkashem/refactor-conditional-delete
KEP-4795: refactor: etcd store conditional delete
2 parents a6ea7b8 + fecab07 commit daf76e6

File tree

3 files changed

+78
-15
lines changed

3 files changed

+78
-15
lines changed

staging/src/k8s.io/apiserver/pkg/storage/etcd3/store.go

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -271,13 +271,15 @@ func (s *store) Delete(
271271
if err != nil {
272272
return fmt.Errorf("unable to convert output object to pointer: %v", err)
273273
}
274-
return s.conditionalDelete(ctx, preparedKey, out, v, preconditions, validateDeletion, cachedExistingObject)
274+
275+
skipTransformDecode := false
276+
return s.conditionalDelete(ctx, preparedKey, out, v, preconditions, validateDeletion, cachedExistingObject, skipTransformDecode)
275277
}
276278

277279
func (s *store) conditionalDelete(
278280
ctx context.Context, key string, out runtime.Object, v reflect.Value, preconditions *storage.Preconditions,
279-
validateDeletion storage.ValidateObjectFunc, cachedExistingObject runtime.Object) error {
280-
getCurrentState := s.getCurrentState(ctx, key, v, false)
281+
validateDeletion storage.ValidateObjectFunc, cachedExistingObject runtime.Object, skipTransformDecode bool) error {
282+
getCurrentState := s.getCurrentState(ctx, key, v, false, skipTransformDecode)
281283

282284
var origState *objState
283285
var err error
@@ -361,7 +363,7 @@ func (s *store) conditionalDelete(
361363
if !txnResp.Succeeded {
362364
getResp := (*clientv3.GetResponse)(txnResp.Responses[0].GetResponseRange())
363365
klog.V(4).Infof("deletion of %s failed because of a conflict, going to retry", key)
364-
origState, err = s.getState(ctx, getResp, key, v, false)
366+
origState, err = s.getState(ctx, getResp, key, v, false, skipTransformDecode)
365367
if err != nil {
366368
return err
367369
}
@@ -376,10 +378,12 @@ func (s *store) conditionalDelete(
376378
if deleteResp.Header == nil {
377379
return errors.New("invalid DeleteRange response - nil header")
378380
}
379-
err = decode(s.codec, s.versioner, origState.data, out, deleteResp.Header.Revision)
380-
if err != nil {
381-
recordDecodeError(s.groupResourceString, key)
382-
return err
381+
if !skipTransformDecode {
382+
err = decode(s.codec, s.versioner, origState.data, out, deleteResp.Header.Revision)
383+
if err != nil {
384+
recordDecodeError(s.groupResourceString, key)
385+
return err
386+
}
383387
}
384388
return nil
385389
}
@@ -405,7 +409,8 @@ func (s *store) GuaranteedUpdate(
405409
return fmt.Errorf("unable to convert output object to pointer: %v", err)
406410
}
407411

408-
getCurrentState := s.getCurrentState(ctx, preparedKey, v, ignoreNotFound)
412+
skipTransformDecode := false
413+
getCurrentState := s.getCurrentState(ctx, preparedKey, v, ignoreNotFound, skipTransformDecode)
409414

410415
var origState *objState
411416
var origStateIsCurrent bool
@@ -531,7 +536,8 @@ func (s *store) GuaranteedUpdate(
531536
if !txnResp.Succeeded {
532537
getResp := (*clientv3.GetResponse)(txnResp.Responses[0].GetResponseRange())
533538
klog.V(4).Infof("GuaranteedUpdate of %s failed because of a conflict, going to retry", preparedKey)
534-
origState, err = s.getState(ctx, getResp, preparedKey, v, ignoreNotFound)
539+
skipTransformDecode := false
540+
origState, err = s.getState(ctx, getResp, preparedKey, v, ignoreNotFound, skipTransformDecode)
535541
if err != nil {
536542
return err
537543
}
@@ -878,19 +884,25 @@ func (s *store) watchContext(ctx context.Context) context.Context {
878884
return clientv3.WithRequireLeader(ctx)
879885
}
880886

881-
func (s *store) getCurrentState(ctx context.Context, key string, v reflect.Value, ignoreNotFound bool) func() (*objState, error) {
887+
func (s *store) getCurrentState(ctx context.Context, key string, v reflect.Value, ignoreNotFound bool, skipTransformDecode bool) func() (*objState, error) {
882888
return func() (*objState, error) {
883889
startTime := time.Now()
884890
getResp, err := s.client.KV.Get(ctx, key)
885891
metrics.RecordEtcdRequest("get", s.groupResourceString, err, startTime)
886892
if err != nil {
887893
return nil, err
888894
}
889-
return s.getState(ctx, getResp, key, v, ignoreNotFound)
895+
return s.getState(ctx, getResp, key, v, ignoreNotFound, skipTransformDecode)
890896
}
891897
}
892898

893-
func (s *store) getState(ctx context.Context, getResp *clientv3.GetResponse, key string, v reflect.Value, ignoreNotFound bool) (*objState, error) {
899+
// getState constructs a new objState from the given response from the storage.
900+
// skipTransformDecode: if true, the function will neither transform the data
901+
// from the storage nor decode it into an object; otherwise, data from the
902+
// storage will be transformed and decoded.
903+
// NOTE: when skipTransformDecode is true, the 'data', and the 'obj' fields
904+
// of the objState will be nil, and 'stale' will be set to true.
905+
func (s *store) getState(ctx context.Context, getResp *clientv3.GetResponse, key string, v reflect.Value, ignoreNotFound bool, skipTransformDecode bool) (*objState, error) {
894906
state := &objState{
895907
meta: &storage.ResponseMeta{},
896908
}
@@ -909,14 +921,24 @@ func (s *store) getState(ctx context.Context, getResp *clientv3.GetResponse, key
909921
return nil, err
910922
}
911923
} else {
924+
state.rev = getResp.Kvs[0].ModRevision
925+
state.meta.ResourceVersion = uint64(state.rev)
926+
927+
if skipTransformDecode {
928+
// be explicit that we don't have the object
929+
state.obj = nil
930+
state.stale = true // this seems a more sane value here
931+
return state, nil
932+
}
933+
912934
data, stale, err := s.transformer.TransformFromStorage(ctx, getResp.Kvs[0].Value, authenticatedDataString(key))
913935
if err != nil {
914936
return nil, storage.NewInternalError(err.Error())
915937
}
916-
state.rev = getResp.Kvs[0].ModRevision
917-
state.meta.ResourceVersion = uint64(state.rev)
938+
918939
state.data = data
919940
state.stale = stale
941+
920942
if err := decode(s.codec, s.versioner, state.data, state.obj, state.rev); err != nil {
921943
recordDecodeError(s.groupResourceString, key)
922944
return nil, err

staging/src/k8s.io/apiserver/pkg/storage/interfaces.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ type UpdateFunc func(input runtime.Object, res ResponseMeta) (output runtime.Obj
108108
// ValidateObjectFunc is a function to act on a given object. An error may be returned
109109
// if the hook cannot be completed. The function may NOT transform the provided
110110
// object.
111+
// NOTE: the object in obj may be nil if it cannot be read from the
112+
// storage, due to transformation or decode error.
111113
type ValidateObjectFunc func(ctx context.Context, obj runtime.Object) error
112114

113115
// ValidateAllObjectFunc is a "admit everything" instance of ValidateObjectFunc.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package storage
18+
19+
import (
20+
"errors"
21+
"strings"
22+
"testing"
23+
)
24+
25+
func TestPreconditionsCheckWithNilObject(t *testing.T) {
26+
p := &Preconditions{}
27+
err := p.Check("foo", nil)
28+
if err == nil {
29+
t.Fatalf("expected an error")
30+
}
31+
32+
var internalErr InternalError
33+
if !errors.As(err, &internalErr) {
34+
t.Fatalf("expected error to be of type: %T, but got: %#v", InternalError{}, err)
35+
}
36+
if want := "can't enforce preconditions"; !strings.Contains(internalErr.Error(), want) {
37+
t.Errorf("expected error to contain %q", want)
38+
}
39+
}

0 commit comments

Comments
 (0)