mirror of https://github.com/tailscale/tailscale/
Introduce a state store to LocalBackend.
The store is passed-in by callers of NewLocalBackend and ipnserver.Run, but currently all callers are hardcoded to an in-memory store. The store is unused. Signed-Off-By: David Anderson <dave@natulte.net>pull/10/head
parent
21280ca2d1
commit
5bc632271b
@ -0,0 +1,117 @@
|
|||||||
|
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package ipn
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"tailscale.com/atomicfile"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrStateNotExist is returned by StateStore.ReadState when the
|
||||||
|
// requested state id doesn't exist.
|
||||||
|
var ErrStateNotExist = errors.New("no state with given id")
|
||||||
|
|
||||||
|
// StateStore persists state, and produces it back on request.
|
||||||
|
type StateStore interface {
|
||||||
|
// ReadState returns the bytes associated with id. Returns (nil,
|
||||||
|
// ErrStateNotExist) if the id doesn't have associated state.
|
||||||
|
ReadState(id StateKey) ([]byte, error)
|
||||||
|
// WriteState saves bs as the state associated with id.
|
||||||
|
WriteState(id StateKey, bs []byte) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// MemoryStore is a store that keeps state in memory only.
|
||||||
|
type MemoryStore struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
cache map[StateKey][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MemoryStore) ReadState(id StateKey) ([]byte, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
if s.cache == nil {
|
||||||
|
s.cache = map[StateKey][]byte{}
|
||||||
|
}
|
||||||
|
bs, ok := s.cache[id]
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrStateNotExist
|
||||||
|
}
|
||||||
|
return bs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MemoryStore) WriteState(id StateKey, bs []byte) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
if s.cache == nil {
|
||||||
|
s.cache = map[StateKey][]byte{}
|
||||||
|
}
|
||||||
|
s.cache[id] = append([]byte(nil), bs...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileStore is a StateStore that uses a JSON file for persistence.
|
||||||
|
type FileStore struct {
|
||||||
|
path string
|
||||||
|
|
||||||
|
mu sync.RWMutex
|
||||||
|
cache map[StateKey][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFileStore returns a new file store that persists to path.
|
||||||
|
func NewFileStore(path string) (*FileStore, error) {
|
||||||
|
bs, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
// Write out an initial file, to verify that we can write
|
||||||
|
// to the path.
|
||||||
|
if err = atomicfile.WriteFile(path, []byte("{}"), 0600); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &FileStore{
|
||||||
|
path: path,
|
||||||
|
cache: map[StateKey][]byte{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := &FileStore{
|
||||||
|
path: path,
|
||||||
|
cache: map[StateKey][]byte{},
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(bs, &ret.cache); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadState returns the bytes persisted for id, if any.
|
||||||
|
func (s *FileStore) ReadState(id StateKey) ([]byte, error) {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
bs, ok := s.cache[id]
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrStateNotExist
|
||||||
|
}
|
||||||
|
return bs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteState persists bs under the key id.
|
||||||
|
func (s *FileStore) WriteState(id StateKey, bs []byte) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
s.cache[id] = append([]byte(nil), bs...)
|
||||||
|
bs, err := json.MarshalIndent(s.cache, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return atomicfile.WriteFile(s.path, bs, 0600)
|
||||||
|
}
|
@ -0,0 +1,121 @@
|
|||||||
|
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package ipn
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testStoreSemantics(t *testing.T, store StateStore) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
// if true, data is data to write. If false, data is expected
|
||||||
|
// output of read.
|
||||||
|
write bool
|
||||||
|
id StateKey
|
||||||
|
data string
|
||||||
|
// If write=false, true if we expect a not-exist error.
|
||||||
|
notExists bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
id: "foo",
|
||||||
|
notExists: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
write: true,
|
||||||
|
id: "foo",
|
||||||
|
data: "bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "foo",
|
||||||
|
data: "bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "baz",
|
||||||
|
notExists: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
write: true,
|
||||||
|
id: "baz",
|
||||||
|
data: "quux",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "foo",
|
||||||
|
data: "bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "baz",
|
||||||
|
data: "quux",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
if test.write {
|
||||||
|
if err := store.WriteState(test.id, []byte(test.data)); err != nil {
|
||||||
|
t.Errorf("writing %q to %q: %v", test.data, test.id, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bs, err := store.ReadState(test.id)
|
||||||
|
if err != nil {
|
||||||
|
if test.notExists && err == ErrStateNotExist {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t.Errorf("reading %q: %v", test.id, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if string(bs) != test.data {
|
||||||
|
t.Errorf("reading %q: got %q, want %q", test.id, string(bs), test.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemoryStore(t *testing.T) {
|
||||||
|
store := &MemoryStore{}
|
||||||
|
testStoreSemantics(t, store)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileStore(t *testing.T) {
|
||||||
|
f, err := ioutil.TempFile("", "test_ipn_store")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
path := f.Name()
|
||||||
|
f.Close()
|
||||||
|
if err := os.Remove(path); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
store, err := NewFileStore(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("creating file store failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testStoreSemantics(t, store)
|
||||||
|
|
||||||
|
// Build a brand new file store and check that both IDs written
|
||||||
|
// above are still there.
|
||||||
|
store, err = NewFileStore(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("creating second file store failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := map[StateKey]string{
|
||||||
|
"foo": "bar",
|
||||||
|
"baz": "quux",
|
||||||
|
}
|
||||||
|
for id, want := range expected {
|
||||||
|
bs, err := store.ReadState(id)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("reading %q (2nd store): %v", id, err)
|
||||||
|
}
|
||||||
|
if string(bs) != want {
|
||||||
|
t.Errorf("reading %q (2nd store): got %q, want %q", id, string(bs), want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue