diff --git a/state/state.go b/state/state.go new file mode 100644 index 0000000000..10173255e5 --- /dev/null +++ b/state/state.go @@ -0,0 +1,10 @@ +package state + +import ( + "github.com/samalba/dockerclient" +) + +type RequestedState struct { + Name string + Config *dockerclient.ContainerConfig +} diff --git a/state/store.go b/state/store.go new file mode 100644 index 0000000000..224d501330 --- /dev/null +++ b/state/store.go @@ -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 +} diff --git a/state/store_test.go b/state/store_test.go new file mode 100644 index 0000000000..7eb379cb91 --- /dev/null +++ b/state/store_test.go @@ -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) +}