introduce storage.Updater with tests

This commit is contained in:
fibonacci1729 2018-01-23 12:56:51 -07:00
Родитель 13687151bf
Коммит 1a52724812
7 изменённых файлов: 179 добавлений и 30 удалений

29
pkg/storage/errors.go Normal file
Просмотреть файл

@ -0,0 +1,29 @@
package storage
import (
"fmt"
)
// NewErrAppStorageNotFound returns a formatted error specifying the storage
// for application specified by appName does not exist.
func NewErrAppStorageNotFound(appName string) error {
return fmt.Errorf("application storage for %q not found", appName)
}
// NewErrAppStorageExists returns a formatted error specifying the storage
// for application specified by appName already exists.
func NewErrAppStorageExists(appName string) error {
return fmt.Errorf("application storage for %q already exists", appName)
}
// NewErrAppBuildNotFound returns a formatted error specifying the storage
// object for build with buildID does not exist.
func NewErrAppBuildNotFound(appName, buildID string) error {
return fmt.Errorf("application %q build storage with ID %q not found", appName, buildID)
}
// NewErrAppBuildExists returns a formatted error specifying the storage
// object for build with buildID already exists.
func NewErrAppBuildExists(appName, buildID string) error {
return fmt.Errorf("application %q build storage with ID %q already exists", appName, buildID)
}

Просмотреть файл

@ -2,8 +2,6 @@ package inprocess
import (
"context"
"fmt"
"github.com/Azure/draft/pkg/storage"
)
@ -13,29 +11,33 @@ type Store struct {
builds map[string][]*storage.Object
}
// compile-time guarantee that Store implements storage.Store
// compile-time guarantee that *Store implements storage.Store
var _ storage.Store = (*Store)(nil)
// NewStore returns a new *inprocess.Store.
// NewStore returns a new inprocess memory Store for storing draft application context.
func NewStore() *Store {
return &Store{builds: make(map[string][]*storage.Object)}
}
// DeleteBuilds deletes all draft builds for the application specified by appName.
//
// DeleteBuilds implements storage.Deleter.
func (s *Store) DeleteBuilds(ctx context.Context, appName string) ([]*storage.Object, error) {
h, ok := s.builds[appName]
if !ok {
return nil, fmt.Errorf("storage history for %q not found", appName)
return nil, storage.NewErrAppStorageNotFound(appName)
}
delete(s.builds, appName)
return h, nil
}
// DeleteBuild deletes the draft build given by buildID for the application specified by appName.
//
// DeleteBuild implements storage.Deleter.
func (s *Store) DeleteBuild(ctx context.Context, appName, buildID string) (*storage.Object, error) {
h, ok := s.builds[appName]
if !ok {
return nil, fmt.Errorf("storage history for %q not found", appName)
return nil, storage.NewErrAppStorageNotFound(appName)
}
for i, o := range h {
if buildID == o.BuildID {
@ -43,38 +45,60 @@ func (s *Store) DeleteBuild(ctx context.Context, appName, buildID string) (*stor
return o, nil
}
}
return nil, fmt.Errorf("application %q storage object %q not found", appName, buildID)
return nil, storage.NewErrAppBuildNotFound(appName, buildID)
}
// CreateBuild stores a draft.Build for the application specified by appName.
// CreateBuild creates new storage for the application specified by appName to include build.
//
// If storage already exists for the application, ErrAppStorageExists is returned.
//
// CreateBuild implements storage.Creater.
func (s *Store) CreateBuild(ctx context.Context, appName string, build *storage.Object) error {
if _, ok := s.builds[appName]; ok {
s.builds[appName] = append(s.builds[appName], build)
return nil
return storage.NewErrAppStorageExists(appName)
}
s.builds[appName] = []*storage.Object{build}
return nil
}
// UpdateBuild updates the application storage specified by appName to include build.
//
// If build does not exist, a new storage entry is created. Otherwise the existing storage
// is updated.
//
// UpdateBuild implements storage.Updater.
func (s *Store) UpdateBuild(ctx context.Context, appName string, build *storage.Object) error {
if _, ok := s.builds[appName]; !ok {
return s.CreateBuild(ctx, appName, build)
}
s.builds[appName] = append(s.builds[appName], build)
// TODO(fibonacci1729): deduplication of builds.
return nil
}
// GetBuilds returns a slice of builds for the given app name.
//
// GetBuilds implements storage.Getter.
func (s *Store) GetBuilds(ctx context.Context, appName string) ([]*storage.Object, error) {
h, ok := s.builds[appName]
if !ok {
return nil, fmt.Errorf("storage history for %q not found", appName)
return nil, storage.NewErrAppStorageNotFound(appName)
}
return h, nil
}
// GetBuild returns the build associated with buildID for the specified app name.
//
// GetBuild implements storage.Getter.
func (s *Store) GetBuild(ctx context.Context, appName, buildID string) (*storage.Object, error) {
h, ok := s.builds[appName]
if !ok {
return nil, fmt.Errorf("storage history for %q not found", appName)
return nil, storage.NewErrAppStorageNotFound(appName)
}
for _, o := range h {
if buildID == o.BuildID {
return o, nil
}
}
return nil, fmt.Errorf("application %q storage object %q not found", appName, buildID)
return nil, storage.NewErrAppBuildNotFound(appName, buildID)
}

Просмотреть файл

@ -53,6 +53,27 @@ func TestStoreCreateBuild(t *testing.T) {
t.Fatalf("failed to get build entry: %v", err)
}
assertEqual(t, "CreateBuild", build, alt)
// try creating a second time; this should fail with ErrAppStorageExists.
if err := store.CreateBuild(ctx, "app2", build); err == nil {
t.Fatalf("expected second CreateBuild to fail")
}
}
func TestStoreUpdateBuild(t *testing.T) {
var (
build = &storage.Object{BuildID: "foo", Release: "bar", ContextID: []byte("foobar")}
store = NewStoreWithMocks()
ctx = context.TODO()
)
if err := store.UpdateBuild(ctx, "app2", build); err != nil {
t.Fatalf("failed to update storage entry: %v", err)
}
alt, err := store.GetBuild(ctx, "app2", build.BuildID)
if err != nil {
t.Fatalf("failed to get build entry: %v", err)
}
assertEqual(t, "UpdateBuild", build, alt)
}
func TestStoreGetBuilds(t *testing.T) {

Просмотреть файл

@ -81,9 +81,6 @@ func (mock *MockConfigMaps) Create(cfgmap *v1.ConfigMap) (*v1.ConfigMap, error)
// Update updates a ConfigMap.
func (mock *MockConfigMaps) Update(cfgmap *v1.ConfigMap) (*v1.ConfigMap, error) {
name := cfgmap.ObjectMeta.Name
if _, ok := mock.cfgmaps[name]; !ok {
return nil, apierrors.NewNotFound(api.Resource("tests"), name)
}
mock.cfgmaps[name] = cfgmap
return cfgmap, nil
}

Просмотреть файл

@ -2,13 +2,12 @@ package kube
import (
"context"
"fmt"
"github.com/Azure/draft/pkg/storage"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/pkg/api/v1"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/pkg/api/v1"
)
// ConfigMaps represents a Kubernetes configmap storage engine for a storage.Object .
@ -16,13 +15,18 @@ type ConfigMaps struct {
impl corev1.ConfigMapInterface
}
// compile-time guarantee that *ConfigMaps implements storage.Store
var _ storage.Store = (*ConfigMaps)(nil)
// NewConfigMaps returns an implementation of storage.Store backed by kubernetes
// ConfigMap objects to store draft application build context.
func NewConfigMaps(impl corev1.ConfigMapInterface) *ConfigMaps {
return &ConfigMaps{impl}
}
// DeleteBuilds deletes all draft builds for the application specified by appName.
//
// DeleteBuilds implements storage.Deleter.
func (this *ConfigMaps) DeleteBuilds(ctx context.Context, appName string) ([]*storage.Object, error) {
builds, err := this.GetBuilds(ctx, appName)
if err != nil {
@ -33,9 +37,14 @@ func (this *ConfigMaps) DeleteBuilds(ctx context.Context, appName string) ([]*st
}
// DeleteBuild deletes the draft build given by buildID for the application specified by appName.
//
// DeleteBuild implements storage.Deleter.
func (this *ConfigMaps) DeleteBuild(ctx context.Context, appName, buildID string) (obj *storage.Object, err error) {
var cfgmap *v1.ConfigMap
if cfgmap, err = this.impl.Get(appName, metav1.GetOptions{}); err != nil {
if apierrors.IsNotFound(err) {
return nil, storage.NewErrAppStorageNotFound(appName)
}
return nil, err
}
if build, ok := cfgmap.Data[buildID]; ok {
@ -46,23 +55,63 @@ func (this *ConfigMaps) DeleteBuild(ctx context.Context, appName, buildID string
_, err = this.impl.Update(cfgmap)
return obj, err
}
return nil, fmt.Errorf("application %q storage object %q not found", appName, buildID)
return nil, storage.NewErrAppBuildNotFound(appName, buildID)
}
// CreateBuild stores a draft.Build for the application specified by appName.
// CreateBuild creates new storage for the application specified by appName to include build.
//
// If the configmap storage already exists for the application, ErrAppStorageExists is returned.
//
// CreateBuild implements storage.Creater.
func (this *ConfigMaps) CreateBuild(ctx context.Context, appName string, build *storage.Object) error {
cfgmap, err := newConfigMap(appName, build)
if err != nil {
return err
}
_, err = this.impl.Create(cfgmap)
if _, err = this.impl.Create(cfgmap); err != nil {
if apierrors.IsAlreadyExists(err) {
return storage.NewErrAppStorageExists(appName)
}
return err
}
return nil
}
// UpdateBuild updates the application configmap storage specified by appName to include build.
//
// If build does not exist, a new storage entry is created. Otherwise the existing storage
// is updated.
//
// UpdateBuild implements storage.Updater.
func (this *ConfigMaps) UpdateBuild(ctx context.Context, appName string, build *storage.Object) (err error) {
var cfgmap *v1.ConfigMap
if cfgmap, err = this.impl.Get(appName, metav1.GetOptions{}); err != nil {
if apierrors.IsNotFound(err) {
return this.CreateBuild(ctx, appName, build)
}
return err
}
if _, ok := cfgmap.Data[build.BuildID]; ok {
return storage.NewErrAppBuildExists(appName, build.BuildID)
}
content, err := storage.EncodeToString(build)
if err != nil {
return err
}
cfgmap.Data[build.BuildID] = content
_, err = this.impl.Update(cfgmap)
return err
}
// GetBuilds returns a slice of builds for the given app name.
//
// GetBuilds implements storage.Getter.
func (this *ConfigMaps) GetBuilds(ctx context.Context, appName string) (builds []*storage.Object, err error) {
var cfgmap *v1.ConfigMap
if cfgmap, err = this.impl.Get(appName, metav1.GetOptions{}); err != nil {
if apierrors.IsNotFound(err) {
return nil, storage.NewErrAppStorageNotFound(appName)
}
return nil, err
}
for _, obj := range cfgmap.Data {
@ -76,9 +125,14 @@ func (this *ConfigMaps) GetBuilds(ctx context.Context, appName string) (builds [
}
// GetBuild returns the build associated with buildID for the specified app name.
//
// GetBuild implements storage.Getter.
func (this *ConfigMaps) GetBuild(ctx context.Context, appName, buildID string) (obj *storage.Object, err error) {
var cfgmap *v1.ConfigMap
if cfgmap, err = this.impl.Get(appName, metav1.GetOptions{}); err != nil {
if apierrors.IsNotFound(err) {
return nil, storage.NewErrAppStorageNotFound(appName)
}
return nil, err
}
if data, ok := cfgmap.Data[buildID]; ok {
@ -87,7 +141,7 @@ func (this *ConfigMaps) GetBuild(ctx context.Context, appName, buildID string) (
}
return obj, nil
}
return nil, fmt.Errorf("application %q storage object %q not found", appName, buildID)
return nil, storage.NewErrAppBuildNotFound(appName, buildID)
}
// newConfigMap constructs a kubernetes ConfigMap object to store a build.

Просмотреть файл

@ -41,7 +41,7 @@ func TestStoreCreateBuild(t *testing.T) {
obj := objectStub("foo1", "bar1", []byte("foobar1"))
err := store.CreateBuild(ctx, "app2", obj)
if err != nil {
t.Fatalf("failed to created build: %v", err)
t.Fatalf("failed to create build: %v", err)
}
got, err := store.GetBuild(ctx, "app2", "foo1")
if err != nil {
@ -50,6 +50,23 @@ func TestStoreCreateBuild(t *testing.T) {
assertEqual(t, "CreateBuild", got, obj)
}
func TestStoreUpdateBuild(t *testing.T) {
var (
store = newMockConfigMapsTestFixture(t)
ctx = context.Background()
)
obj := objectStub("foo1", "bar1", []byte("foobar1"))
err := store.UpdateBuild(ctx, "app2", obj)
if err != nil {
t.Fatalf("failed to update build: %v", err)
}
got, err := store.GetBuild(ctx, "app2", "foo1")
if err != nil {
t.Fatalf("failed to get storage object: %v", err)
}
assertEqual(t, "UpdateBuild", got, obj)
}
func TestStoreGetBuilds(t *testing.T) {
var (
store = newMockConfigMapsTestFixture(t)

Просмотреть файл

@ -12,20 +12,26 @@ import (
"github.com/golang/protobuf/proto"
)
// Deletor represents the delete APIs of the storage engine.
type Deletor interface {
// Deleter represents the delete APIs of the storage engine.
type Deleter interface {
// DeleteBuilds deletes all draft builds for the application specified by appName.
DeleteBuilds(ctx context.Context, appName string) ([]*Object, error)
// DeleteBuild deletes the draft build given by buildID for the application specified by appName.
DeleteBuild(ctx context.Context, appName, buildID string) (*Object, error)
}
// Creator represents the create APIs of the storage engine.
type Creator interface {
// Creater represents the create APIs of the storage engine.
type Creater interface {
// CreateBuild creates and stores a new build.
CreateBuild(ctx context.Context, appName string, build *Object) error
}
// Updater represents the update APIs of the storage engine.
type Updater interface {
// UpdateBuild creates and stores a new build.
UpdateBuild(ctx context.Context, appName string, build *Object) error
}
// Getter represents the retrieval APIs of the storage engine.
type Getter interface {
// GetBuilds retrieves all draft builds from storage.
@ -36,8 +42,9 @@ type Getter interface {
// Store represents a storage engine for application state stored by Draftd.
type Store interface {
Creator
Deletor
Creater
Deleter
Updater
Getter
}