Introduce getKv(), putKv(), deleteKv()

The key-value portion is about to get a lot more complicated, and as a
prelude I've moved the three verbs into their own functions.

I plan to do the following:

- use an interface for Xip.Etcd (not `*v3client.Client)
- use counterfeiter to create fakes for above interface
- write vanilla Golang, non-Ginkgo test for getKv, putKv, deleteKv
- add local datastructure (non-etcd) to hold kv if no etcd
This commit is contained in:
Brian Cunnie
2022-01-15 08:36:56 -08:00
parent b3fc9837ad
commit 53bc60bc14
2 changed files with 52 additions and 38 deletions

View File

@@ -4,6 +4,7 @@
package xip
import (
"context"
"errors"
"fmt"
"net"
@@ -12,8 +13,6 @@ import (
"strings"
"time"
"golang.org/x/net/context"
v3client "go.etcd.io/etcd/client/v3"
"golang.org/x/net/dns/dnsmessage"
)
@@ -757,9 +756,7 @@ func (x Xip) kvTXTResources(fqdn string) ([]dnsmessage.TXTResource, error) {
labels := strings.Split(fqdn, ".")
labels = labels[:len(labels)-3] // strip ".k-v.io"
// key is always present, always first subdomain of "k-v.io"
// we prepend "d" (data) to differentiate from "t" (time) for future garbage collection
keyPrefix := "d"
key = keyPrefix + strings.ToLower(labels[len(labels)-1])
key = strings.ToLower(labels[len(labels)-1])
switch {
case len(labels) == 1:
verb = "get" // default action if only key, not verb, is not present
@@ -771,48 +768,65 @@ func (x Xip) kvTXTResources(fqdn string) ([]dnsmessage.TXTResource, error) {
value = strings.Join(labels[1:len(labels)-1], ".") // e.g. "put.94.0.2.firefox-version.k-v.io"
}
// prepare to query etcd:
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*500)
defer cancel()
switch verb {
case "get":
resp, err := x.Etcd.Get(ctx, key)
if err != nil {
return nil, fmt.Errorf(`couldn't GET "%s": %w`, strings.TrimPrefix(key, keyPrefix), err)
}
if len(resp.Kvs) > 0 {
return []dnsmessage.TXTResource{{[]string{string(resp.Kvs[0].Value)}}}, nil
}
return []dnsmessage.TXTResource{}, nil
return x.getKv(key)
case "put":
if len(labels) == 2 {
return []dnsmessage.TXTResource{{[]string{"422: no value provided"}}}, nil
return []dnsmessage.TXTResource{{[]string{"422: missing a value: put.value.key.k-v.io"}}}, nil
}
if len(value) > 63 { // too-long TXT records can be used in DNS amplification attacks; Truncate!
value = value[:63]
}
_, err := x.Etcd.Put(ctx, key, value)
if err != nil {
return nil, fmt.Errorf("couldn't PUT (%s: %s): %w", strings.TrimPrefix(key, keyPrefix), value, err)
}
return []dnsmessage.TXTResource{{[]string{value}}}, nil
return x.putKv(key, value)
case "delete":
getResp, err := x.Etcd.Get(ctx, key) // is the key set?
if err != nil {
return nil, fmt.Errorf(`couldn't GET "%s": %w`, strings.TrimPrefix(key, keyPrefix), err)
}
if len(getResp.Kvs) == 0 { // nothing to delete
return []dnsmessage.TXTResource{}, nil
}
// the key is set; we need to delete it
_, err = x.Etcd.Delete(ctx, key)
if err != nil {
return nil, fmt.Errorf("couldn't DELETE (%s: %s): %w", strings.TrimPrefix(key, keyPrefix), value, err)
}
return []dnsmessage.TXTResource{{[]string{string(getResp.Kvs[0].Value)}}}, nil
return x.deleteKv(key)
}
return []dnsmessage.TXTResource{{[]string{"422: valid verbs are get, put, delete"}}}, nil
}
func (x Xip) getKv(key string) ([]dnsmessage.TXTResource, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*500)
defer cancel()
resp, err := x.Etcd.Get(ctx, key)
if err != nil {
return nil, fmt.Errorf(`couldn't GET "%s": %w`, key, err)
}
if len(resp.Kvs) > 0 {
return []dnsmessage.TXTResource{{[]string{string(resp.Kvs[0].Value)}}}, nil
}
return []dnsmessage.TXTResource{}, nil
}
func (x Xip) putKv(key, value string) ([]dnsmessage.TXTResource, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*500)
defer cancel()
if len(value) > 63 { // too-long TXT records can be used in DNS amplification attacks; Truncate!
value = value[:63]
}
_, err := x.Etcd.Put(ctx, key, value)
if err != nil {
return nil, fmt.Errorf("couldn't PUT (%s: %s): %w", key, value, err)
}
return []dnsmessage.TXTResource{{[]string{value}}}, nil
}
func (x Xip) deleteKv(key string) ([]dnsmessage.TXTResource, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*500)
defer cancel()
getResp, err := x.Etcd.Get(ctx, key) // is the key set?
if err != nil {
return nil, fmt.Errorf(`couldn't GET "%s": %w`, key, err)
}
if len(getResp.Kvs) == 0 { // nothing to delete
return []dnsmessage.TXTResource{}, nil
}
value := string(getResp.Kvs[0].Value)
// the key is set; we need to delete it
_, err = x.Etcd.Delete(ctx, key)
if err != nil {
return nil, fmt.Errorf("couldn't DELETE (%s: %s): %w", key, value, err)
}
return []dnsmessage.TXTResource{{[]string{value}}}, nil
}
// soaLogMessage returns an easy-to-read string for logging SOA Answers/Authorities
func soaLogMessage(soaResource dnsmessage.SOAResource) string {
return soaResource.NS.String() + " " +

View File

@@ -191,7 +191,7 @@ var _ = Describe("Xip", func() {
Entry("getting that deleted value → empty array", "my-key.k-v.io.", []string{}),
// errors
Entry("getting a non-existent key → empty array", "nonexistent.k-v.io.", []string{}),
Entry("putting but skipping the value → error txt", "put.my-key.k-v.io.", []string{"422: no value provided"}),
Entry("putting but skipping the value → error txt", "put.my-key.k-v.io.", []string{"422: missing a value: put.value.key.k-v.io"}),
Entry("deleting a non-existent key → silently succeeds", "delete.non-existent.k-v.io.", []string{}),
Entry("using a garbage verb → error txt", "post.my-key.k-v.io.", []string{"422: valid verbs are get, put, delete"}),
// others