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>main
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