Skip to content

Commit

Permalink
Implement k8s kv Delete
Browse files Browse the repository at this point in the history
  • Loading branch information
dimitarvdimitrov committed Dec 3, 2021
1 parent fabeae0 commit 5c38440
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 45 deletions.
70 changes: 70 additions & 0 deletions kv/kubernetes/jsonpatch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package kubernetes

import (
"encoding/base64"
"encoding/json"
)

type operation struct {
Op string `json:"op"`
Path string `json:"path"`
// Value should be a base64-encoded value or nil.
Value *string `json:"value"`
}

// preparePatch prepares a JSON patch (RFC 6902) to update a configmap key. If oldHash is empty or nil
// this is equivalent to adding a key.
func preparePatch(key string, oldHash, newVal, newHash []byte) ([]byte, error) {
hashKey := "/binaryData/" + convertKeyToStoreHash(key)

b64 := func(b []byte) *string {
str := base64.StdEncoding.EncodeToString(b)
return &str
}

// testing with nil is equivalent to testing that the key doesn't exist
var expectedHash *string
if len(oldHash) > 0 {
expectedHash = b64(oldHash)
}

testHashOp := operation{
Op: "test",
Path: hashKey,
Value: expectedHash,
}

setHashOp := operation{
Op: "replace",
Path: hashKey,
Value: b64(newHash),
}

setDataOp := operation{
Op: "replace",
Path: "/binaryData/" + convertKeyToStore(key),
Value: b64(newVal),
}

patch := []operation{testHashOp, setHashOp, setDataOp}

return json.Marshal(patch)
}

func prepareDeletePatch(key string) ([]byte, error) {
removeHashOp := operation{
Op: "remove",
Path: "/binaryData/" + convertKeyToStoreHash(key),
Value: nil,
}

removeObjectOp := operation{
Op: "remove",
Path: "/binaryData/" + convertKeyToStore(key),
Value: nil,
}

patch := []operation{removeHashOp, removeObjectOp}

return json.Marshal(patch)
}
69 changes: 25 additions & 44 deletions kv/kubernetes/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"crypto/sha1"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"sort"
Expand Down Expand Up @@ -190,7 +189,31 @@ func (c *Client) Get(ctx context.Context, key string) (interface{}, error) {
// Delete a specific key. Deletions are best-effort and no error will
// be returned if the key does not exist.
func (c *Client) Delete(ctx context.Context, key string) error {
return fmt.Errorf("unimplemented")
c.configMapMtx.RLock()
cm := c.configMap
c.configMapMtx.RUnlock()

_, ok := cm.BinaryData[convertKeyToStore(key)]
if !ok {
// Object is already deleted or never existed
return nil
}

patch, err := prepareDeletePatch(key)
if err != nil {
return err
}

updatedCM, err := c.client.CoreV1().ConfigMaps(c.namespace).Patch(ctx, c.name, types.JSONPatchType, patch, metav1.PatchOptions{})
if err != nil {
return err
}

c.configMapMtx.Lock()
c.configMap = updatedCM
c.configMapMtx.Unlock()

return nil
}

// CAS stands for Compare-And-Swap. Will call provided callback f with the
Expand Down Expand Up @@ -254,48 +277,6 @@ func (c *Client) CAS(ctx context.Context, key string, f func(in interface{}) (ou
return nil
}

func preparePatch(key string, oldHash, newVal, newHash []byte) ([]byte, error) {
hashKey := "/binaryData/" + convertKeyToStoreHash(key)

b64 := func(b []byte) *string {
str := base64.StdEncoding.EncodeToString(b)
return &str
}

type operation struct {
Op string `json:"op"`
Path string `json:"path"`
Value *string `json:"value"`
}

var expectedHash *string
if len(oldHash) > 0 {
expectedHash = b64(oldHash)
}

testHashOp := operation{
Op: "test",
Path: hashKey,
Value: expectedHash,
}

setHashOp := operation{
Op: "replace",
Path: hashKey,
Value: b64(newHash),
}

setDataOp := operation{
Op: "replace",
Path: "/binaryData/" + convertKeyToStore(key),
Value: b64(newVal),
}

patch := []operation{testHashOp, setHashOp, setDataOp}

return json.Marshal(patch)
}

func hash(b []byte) []byte {
hasher := sha1.New()
_, err := hasher.Write(b)
Expand Down
38 changes: 37 additions & 1 deletion kv/kubernetes/kubernetes_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func Test_Integration(t *testing.T) {

keys, err := c.List(context.Background(), "")
require.NoError(t, err)
assert.ElementsMatch(t, []string{}, keys)
assert.Empty(t, keys)

value, err := c.Get(context.Background(), "not-exists")
require.NoError(t, err)
Expand All @@ -97,3 +97,39 @@ func Test_Integration(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "test", value)
}

func Test_Delete(t *testing.T) {
t.Run("happy flow", func(t *testing.T) {
c := newClient(t)

require.NoError(t, c.CAS(context.Background(), "/test", func(_ interface{}) (out interface{}, retry bool, err error) {
out = "test"
retry = false
return
}))

require.NoError(t, c.Delete(context.Background(), "/test"))

keys, err := c.List(context.TODO(), "/test")
require.NoError(t, err)
assert.Empty(t, keys)

value, err := c.Get(context.TODO(), "/test")
assert.NoError(t, err)
assert.Nil(t, value)
})

t.Run("deleting non-existent key also works", func(t *testing.T) {
c := newClient(t)

require.NoError(t, c.Delete(context.Background(), "/test"))

keys, err := c.List(context.TODO(), "/test")
require.NoError(t, err)
assert.Empty(t, keys)

value, err := c.Get(context.TODO(), "/test")
assert.NoError(t, err)
assert.Nil(t, value)
})
}

0 comments on commit 5c38440

Please sign in to comment.