mirror of https://github.com/docker/docs.git
Merge pull request #234 from aluzzardi/store
Add a simple key<->Container persistent store.
This commit is contained in:
commit
3f55099c06
|
|
@ -0,0 +1,10 @@
|
||||||
|
package state
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/samalba/dockerclient"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RequestedState struct {
|
||||||
|
Name string
|
||||||
|
Config *dockerclient.ContainerConfig
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,187 @@
|
||||||
|
package state
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
log "github.com/Sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNotFound = errors.New("not found")
|
||||||
|
ErrAlreadyExists = errors.New("already exists")
|
||||||
|
ErrInvalidKey = errors.New("invalid key")
|
||||||
|
)
|
||||||
|
|
||||||
|
// A simple key<->RequestedState store.
|
||||||
|
type Store struct {
|
||||||
|
RootDir string
|
||||||
|
values map[string]*RequestedState
|
||||||
|
|
||||||
|
sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStore(rootdir string) *Store {
|
||||||
|
return &Store{
|
||||||
|
RootDir: rootdir,
|
||||||
|
values: make(map[string]*RequestedState),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize must be called before performing any operation on the store. It
|
||||||
|
// will attempt to restore the data from disk.
|
||||||
|
func (s *Store) Initialize() error {
|
||||||
|
s.Lock()
|
||||||
|
defer s.Unlock()
|
||||||
|
|
||||||
|
if err := os.MkdirAll(s.RootDir, 0700); err != nil && !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.restore(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) path(key string) string {
|
||||||
|
return path.Join(s.RootDir, key+".json")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) restore() error {
|
||||||
|
files, err := ioutil.ReadDir(s.RootDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, fileinfo := range files {
|
||||||
|
file := fileinfo.Name()
|
||||||
|
|
||||||
|
// Verify the file extension.
|
||||||
|
extension := filepath.Ext(file)
|
||||||
|
if extension != ".json" {
|
||||||
|
log.Errorf("invalid file extension for filename %s (%s)", file, extension)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the object back.
|
||||||
|
value, err := s.load(path.Join(s.RootDir, file))
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the key.
|
||||||
|
key := file[0 : len(file)-len(extension)]
|
||||||
|
if len(key) == 0 {
|
||||||
|
log.Errorf("invalid filename %s", file)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store it back.
|
||||||
|
s.values[key] = value
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) load(file string) (*RequestedState, error) {
|
||||||
|
data, err := ioutil.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to load %s: %v", file, err)
|
||||||
|
}
|
||||||
|
value := &RequestedState{}
|
||||||
|
if err := json.Unmarshal(data, value); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieves an object from the store keyed by `key`.
|
||||||
|
func (s *Store) Get(key string) (*RequestedState, error) {
|
||||||
|
s.RLock()
|
||||||
|
defer s.RUnlock()
|
||||||
|
|
||||||
|
if value, ok := s.values[key]; ok {
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return all objects of the store.
|
||||||
|
func (s *Store) All() []*RequestedState {
|
||||||
|
s.RLock()
|
||||||
|
defer s.RUnlock()
|
||||||
|
|
||||||
|
states := make([]*RequestedState, len(s.values))
|
||||||
|
i := 0
|
||||||
|
for _, state := range s.values {
|
||||||
|
states[i] = state
|
||||||
|
i = i + 1
|
||||||
|
}
|
||||||
|
return states
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) set(key string, value *RequestedState) error {
|
||||||
|
if len(key) == 0 {
|
||||||
|
return ErrInvalidKey
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(value, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ioutil.WriteFile(s.path(key), data, 0600); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.values[key] = value
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a new object on the store. `key` must be unique.
|
||||||
|
func (s *Store) Add(key string, value *RequestedState) error {
|
||||||
|
s.Lock()
|
||||||
|
defer s.Unlock()
|
||||||
|
|
||||||
|
if _, exists := s.values[key]; exists {
|
||||||
|
return ErrAlreadyExists
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replaces an already existing object from the store.
|
||||||
|
func (s *Store) Replace(key string, value *RequestedState) error {
|
||||||
|
s.Lock()
|
||||||
|
defer s.Unlock()
|
||||||
|
|
||||||
|
if _, exists := s.values[key]; !exists {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove `key` from the store.
|
||||||
|
func (s *Store) Remove(key string) error {
|
||||||
|
s.Lock()
|
||||||
|
defer s.Unlock()
|
||||||
|
|
||||||
|
if _, exists := s.values[key]; !exists {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Remove(s.path(key)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(s.values, key)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
package state
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStore(t *testing.T) {
|
||||||
|
dir, err := ioutil.TempDir("", "store-test")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
store := NewStore(dir)
|
||||||
|
assert.NoError(t, store.Initialize())
|
||||||
|
|
||||||
|
c1 := &RequestedState{}
|
||||||
|
c1.Name = "foo"
|
||||||
|
c2 := &RequestedState{}
|
||||||
|
c2.Name = "bar"
|
||||||
|
|
||||||
|
var ret *RequestedState
|
||||||
|
|
||||||
|
// Add an invalid key
|
||||||
|
assert.EqualError(t, store.Add("", c1), ErrInvalidKey.Error())
|
||||||
|
|
||||||
|
// Add "foo" into the store.
|
||||||
|
assert.NoError(t, store.Add("foo", c1))
|
||||||
|
|
||||||
|
// Retrieve "foo" from the store.
|
||||||
|
ret, err = store.Get("foo")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, c1.Name, ret.Name)
|
||||||
|
|
||||||
|
// Try to add "foo" again.
|
||||||
|
assert.EqualError(t, store.Add("foo", c1), ErrAlreadyExists.Error())
|
||||||
|
|
||||||
|
// Replace "foo" with c2.
|
||||||
|
assert.NoError(t, store.Replace("foo", c2))
|
||||||
|
ret, err = store.Get("foo")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, c2.Name, ret.Name)
|
||||||
|
|
||||||
|
// Initialize a brand new store and retrieve "foo" again.
|
||||||
|
// This is to ensure data load on initialization works correctly.
|
||||||
|
store = NewStore(dir)
|
||||||
|
assert.NoError(t, store.Initialize())
|
||||||
|
ret, err = store.Get("foo")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, c2.Name, ret.Name)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue