зеркало из https://github.com/Azure/draft-classic.git
introduce storage.Updater with tests
This commit is contained in:
Родитель
13687151bf
Коммит
1a52724812
|
@ -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
|
||||
}
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче