diff --git a/pkg/collections/collection.go b/pkg/collections/collection.go index dfe5d66ed4a5b96f4f99118a6842488a2bc94735..ddb73ecad0151b3f185f069ed503f0b4ba4d9165 100644 --- a/pkg/collections/collection.go +++ b/pkg/collections/collection.go @@ -176,6 +176,10 @@ func (c Collection) IsNoData() bool { return c.NoData != nil && *c.NoData } +func (c Collection) IsSystem() bool { + return c.System != nil && *c.System +} + func (c Collection) IsView() bool { return c.View != nil } diff --git a/pkg/extension/extension.go b/pkg/extension/extension.go index 26d9b6d51171307161e68ceef0830857fbb569a8..fb8044c7a2f4c76b775840c5628319196a0493ae 100644 --- a/pkg/extension/extension.go +++ b/pkg/extension/extension.go @@ -21,6 +21,8 @@ const ( StateInstalled = pb.SpaceExtensions_INSTALLED StateInProgress = pb.SpaceExtensions_IN_PROGRESS StateFail = pb.SpaceExtensions_FAIL + + ExtensionMetadataKey = "extension" ) type ( diff --git a/pkg/schema/schema.go b/pkg/schema/schema.go index e0eca6b6ffc02511acbe3712b2f2bf83fc5bc05e..bac95d05de2e863e5540c02f8553e431b2f602c7 100644 --- a/pkg/schema/schema.go +++ b/pkg/schema/schema.go @@ -12,7 +12,8 @@ import ( type Schema struct { field.Field - Loaded bool `json:"loaded"` + Loaded bool `json:"loaded"` + Metadata map[string]string `json:"metadata"` } func New(kv ...interface{}) *Schema { @@ -33,8 +34,9 @@ var ( func (s *Schema) Clone(reset bool) *Schema { return &Schema{ - Field: *s.Field.Clone(reset), - Loaded: s.Loaded, + Field: *s.Field.Clone(reset), + Loaded: s.Loaded, + Metadata: s.Metadata, } } @@ -43,6 +45,21 @@ func (s Schema) WithIncludes(includes ...interface{}) *Schema { return &s } +func (s *Schema) WithMetadata(kv ...string) *Schema { + if s.Metadata == nil { + s.Metadata = make(map[string]string, len(s.Metadata)) + } + for i := 0; i < len(kv); i += 2 { + s.Metadata[kv[i]] = kv[i+1] + } + return s +} + +func (s Schema) SetMetadata(md map[string]string) *Schema { + s.Metadata = md + return &s +} + func (s *Schema) Load(ctx context.Context) error { if s.Loaded { return nil diff --git a/pkg/schema/schema_json.go b/pkg/schema/schema_json.go index 906acb5d0d361611d01f43d10bd0f80377ccb4a7..e8710f76dfb8a5a81da279e4ba6d46f77a1bbdb8 100644 --- a/pkg/schema/schema_json.go +++ b/pkg/schema/schema_json.go @@ -6,8 +6,8 @@ import ( ) type jsonSchema struct { - //Field json.RawMessage `json:"field,inline"` - Loaded bool `json:"loaded"` + Loaded bool `json:"loaded"` + Metadata map[string]string `json:"metadata"` } func (s *Schema) UnmarshalJSON(b []byte) error { @@ -17,6 +17,7 @@ func (s *Schema) UnmarshalJSON(b []byte) error { return errors.Wrapf(err, "error unmarshal json into field") } s.Loaded = j.Loaded + s.Metadata = j.Metadata if err := s.Field.UnmarshalJSON(b); err != nil { return err @@ -42,8 +43,8 @@ func (s *Schema) MarshalJSON() ([]byte, error) { } jsonSch, err := jsoniter.Marshal(jsonSchema{ - //Field: b, - Loaded: s.Loaded, + Loaded: s.Loaded, + Metadata: s.Metadata, }) if err != nil { return nil, err diff --git a/pkg/setup/collection.go b/pkg/setup/collection.go index 11274063f34d35e095380d27c94387f242339160..f8d119db2b54bc8a9c44c389aafc3e041aafcd1d 100644 --- a/pkg/setup/collection.go +++ b/pkg/setup/collection.go @@ -6,8 +6,10 @@ import ( "strings" "git.perx.ru/perxis/perxis-go/pkg/collections" + "git.perx.ru/perxis/perxis-go/pkg/data" "git.perx.ru/perxis/perxis-go/pkg/environments" "git.perx.ru/perxis/perxis-go/pkg/errors" + "git.perx.ru/perxis/perxis-go/pkg/extension" "go.uber.org/zap" ) @@ -18,8 +20,8 @@ var ( ) type CollectionsOption func(c *CollectionConfig) -type UpdateCollectionFn func(s *Setup, exist, new *collections.Collection) (coll *collections.Collection, upd bool, setSchema bool) -type DeleteCollectionFn func(s *Setup, col *collections.Collection) bool +type UpdateCollectionFn func(s *Setup, exist, new *collections.Collection) (coll *collections.Collection, upd bool, setSchema bool, err error) +type DeleteCollectionFn func(s *Setup, col *collections.Collection) (bool, error) type CollectionConfig struct { collection *collections.Collection @@ -30,7 +32,7 @@ type CollectionConfig struct { func NewCollectionConfig(collection *collections.Collection, opt ...CollectionsOption) CollectionConfig { c := CollectionConfig{collection: collection} - UpdateExistingCollection()(&c) + DefaultUpdateCollectionStrategy()(&c) DeleteCollectionIfRemove()(&c) for _, o := range opt { @@ -42,40 +44,55 @@ func NewCollectionConfig(collection *collections.Collection, opt ...CollectionsO func OverwriteCollection() CollectionsOption { return func(c *CollectionConfig) { - c.UpdateFn = func(s *Setup, old, new *collections.Collection) (*collections.Collection, bool, bool) { - return new, true, true + c.UpdateFn = func(s *Setup, old, new *collections.Collection) (*collections.Collection, bool, bool, error) { + return new, true, true, nil } } } func KeepExistingCollection() CollectionsOption { return func(c *CollectionConfig) { - c.UpdateFn = func(s *Setup, old, new *collections.Collection) (*collections.Collection, bool, bool) { - return old, false, false + c.UpdateFn = func(s *Setup, old, new *collections.Collection) (*collections.Collection, bool, bool, error) { + return old, false, false, nil } } } func DeleteCollection() CollectionsOption { return func(c *CollectionConfig) { - c.DeleteFn = func(s *Setup, collection *collections.Collection) bool { return true } + c.DeleteFn = func(s *Setup, collection *collections.Collection) (bool, error) { return true, nil } } } func DeleteCollectionIfRemove() CollectionsOption { return func(c *CollectionConfig) { - c.DeleteFn = func(s *Setup, collection *collections.Collection) bool { return s.IsRemove() } + c.DeleteFn = func(s *Setup, collection *collections.Collection) (bool, error) { return s.IsRemove(), nil } } } -func UpdateExistingCollection() CollectionsOption { +func DefaultUpdateCollectionStrategy() CollectionsOption { return func(c *CollectionConfig) { - c.UpdateFn = func(s *Setup, exist, collection *collections.Collection) (*collections.Collection, bool, bool) { + c.UpdateFn = func(s *Setup, exist, collection *collections.Collection) (*collections.Collection, bool, bool, error) { + if !s.IsForce() && !collection.IsView() && !exist.IsView() && collection.Schema.Metadata != nil && collection.Schema.Metadata[extension.ExtensionMetadataKey] != "" { + if exist.Schema.Metadata == nil || exist.Schema.Metadata[extension.ExtensionMetadataKey] != collection.Schema.Metadata[extension.ExtensionMetadataKey] { + return nil, false, false, collections.ErrAlreadyExists + } + } + if len(exist.Tags) > 0 { - collection.Tags = append(exist.Tags, collection.Tags...) + collection.Tags = data.SetFromSlice(append(exist.Tags, collection.Tags...)) + } + + var update, setSchema bool + update = collection.Name != exist.Name || collection.IsSingle() != exist.IsSingle() || collection.IsSystem() != exist.IsSystem() || + collection.IsNoData() != exist.IsNoData() || collection.Hidden != exist.Hidden || collection.IsView() != exist.IsView() && data.ElementsMatch(exist.Tags, collection.Tags) + + if exist.View != nil && collection.View != nil { + update = update && *exist.View == *collection.View } + setSchema = !collection.IsView() && !reflect.DeepEqual(exist.Schema, collection.Schema) - return collection, true, !collection.IsView() && !reflect.DeepEqual(exist.Schema, collection.Schema) + return collection, update, setSchema, nil } } } @@ -133,13 +150,16 @@ func (s *Setup) InstallCollection(ctx context.Context, c CollectionConfig) (setS if exist == nil { setSchema = !collection.IsView() - exist, err = s.content.Collections.Create(ctx, collection) + _, err = s.content.Collections.Create(ctx, collection) if err != nil { return false, err } } else { var upd bool - collection, upd, setSchema = c.UpdateFn(s, exist, c.collection) + collection, upd, setSchema, err = c.UpdateFn(s, exist, c.collection) + if err != nil { + return false, err + } if upd { if err = s.content.Collections.Update(ctx, collection); err != nil { return false, err @@ -205,8 +225,12 @@ func (s *Setup) UninstallCollections(ctx context.Context) error { } func (s *Setup) UninstallCollection(ctx context.Context, c CollectionConfig) error { - if c.DeleteFn(s, c.collection) { - if err := s.content.Collections.Delete(ctx, s.SpaceID, s.EnvironmentID, c.collection.ID); err != nil && !strings.Contains(err.Error(), collections.ErrNotFound.Error()) { + ok, err := c.DeleteFn(s, c.collection) + if err != nil { + return err + } + if ok { + if err = s.content.Collections.Delete(ctx, s.SpaceID, s.EnvironmentID, c.collection.ID); err != nil && !strings.Contains(err.Error(), collections.ErrNotFound.Error()) { return err } s.removeItems(c.collection.ID) // после удаления коллекции нет смысла удалять ее элементы diff --git a/pkg/setup/collection_test.go b/pkg/setup/collection_test.go index 313b04bb3e272b3ee0181a38da41f0cd1106ca30..b249048970b153d08f30f8aecf6f732c827cf47e 100644 --- a/pkg/setup/collection_test.go +++ b/pkg/setup/collection_test.go @@ -10,6 +10,7 @@ import ( "git.perx.ru/perxis/perxis-go/pkg/environments" envmocks "git.perx.ru/perxis/perxis-go/pkg/environments/mocks" "git.perx.ru/perxis/perxis-go/pkg/errors" + "git.perx.ru/perxis/perxis-go/pkg/extension" "git.perx.ru/perxis/perxis-go/pkg/schema" "git.perx.ru/perxis/perxis-go/pkg/schema/field" "github.com/stretchr/testify/assert" @@ -22,6 +23,7 @@ func TestSetup_InstallCollections(t *testing.T) { collections []*collections.Collection collectionsCall func(svc *mockscollections.Collections) envsCall func(svc *envmocks.Environments) + force bool wantErr func(t *testing.T, err error) }{ { @@ -91,6 +93,77 @@ func TestSetup_InstallCollections(t *testing.T) { assert.Contains(t, errors.GetDetail(err), "Возникла ошибка при настройке коллекции space(1)") }, }, + { + name: "Update extension collection with metadata", + collections: []*collections.Collection{{ID: "1", SpaceID: "sp", EnvID: "env", Schema: schema.New("name", field.String()).WithMetadata(extension.ExtensionMetadataKey, "test-extension")}}, + collectionsCall: func(svc *mockscollections.Collections) { + svc.On("Get", mock.Anything, "sp", "env", "1").Return(&collections.Collection{ID: "1", SpaceID: "sp", EnvID: "env", Schema: schema.New("name", field.String()).WithMetadata(extension.ExtensionMetadataKey, "test-extension")}, nil).Once() + svc.On("Update", mock.Anything, &collections.Collection{ID: "1", SpaceID: "sp", EnvID: "env", Schema: schema.New("name", field.String()).WithMetadata(extension.ExtensionMetadataKey, "test-extension")}).Return(nil).Once() + svc.On("SetSchema", mock.Anything, "sp", "env", "1", schema.New("name", field.String())).Return(nil).Once() + }, + envsCall: func(svc *envmocks.Environments) { + svc.On("Migrate", mock.Anything, "sp", "env", &environments.MigrateOptions{Wait: true}).Return(nil).Once() + }, + wantErr: func(t *testing.T, err error) { + assert.NoError(t, err) + }, + }, + { + name: "Fail to update user collection with same id as in extensions collection", + collections: []*collections.Collection{{ID: "1", SpaceID: "sp", EnvID: "env", Schema: schema.New("name", field.String()).WithMetadata(extension.ExtensionMetadataKey, "test-extension")}}, + collectionsCall: func(svc *mockscollections.Collections) { + svc.On("Get", mock.Anything, "sp", "env", "1").Return(&collections.Collection{ID: "1", SpaceID: "sp", EnvID: "env", Schema: schema.New("name", field.String())}, nil).Once() + }, + wantErr: func(t *testing.T, err error) { + assert.Error(t, err) + assert.ErrorIs(t, err, collections.ErrAlreadyExists) + }, + }, + { + name: "Update user collection with same id as in extensions collection with force", + collections: []*collections.Collection{{ID: "1", SpaceID: "sp", EnvID: "env", Schema: schema.New("name", field.String()).WithMetadata(extension.ExtensionMetadataKey, "test-extension")}}, + collectionsCall: func(svc *mockscollections.Collections) { + svc.On("Get", mock.Anything, "sp", "env", "1").Return(&collections.Collection{ID: "1", SpaceID: "sp", EnvID: "env", Schema: schema.New("name", field.String())}, nil).Once() + svc.On("Update", mock.Anything, &collections.Collection{ID: "1", SpaceID: "sp", EnvID: "env", Schema: schema.New("name", field.String()).WithMetadata(extension.ExtensionMetadataKey, "test-extension")}).Return(nil).Once() + svc.On("SetSchema", mock.Anything, "sp", "env", "1", schema.New("name", field.String()).WithMetadata(extension.ExtensionMetadataKey, "test-extension")).Return(nil).Once() + }, + envsCall: func(svc *envmocks.Environments) { + svc.On("Migrate", mock.Anything, "sp", "env", &environments.MigrateOptions{Wait: true}).Return(nil).Once() + }, + wantErr: func(t *testing.T, err error) { + assert.NoError(t, err) + }, + force: true, + }, + { + name: "Update exist view collection with the same id", + collections: []*collections.Collection{{ID: "1", SpaceID: "sp", EnvID: "env", Schema: schema.New("name", field.String()).WithMetadata(extension.ExtensionMetadataKey, "test-extension")}}, + collectionsCall: func(svc *mockscollections.Collections) { + svc.On("Get", mock.Anything, "sp", "env", "1").Return(&collections.Collection{ID: "1", SpaceID: "sp", EnvID: "env", View: &collections.View{SpaceID: "sp2", EnvID: environments.DefaultEnvironment, CollectionID: "2"}}, nil).Once() + svc.On("Update", mock.Anything, &collections.Collection{ID: "1", SpaceID: "sp", EnvID: "env", Schema: schema.New("name", field.String()).WithMetadata(extension.ExtensionMetadataKey, "test-extension")}).Return(nil).Once() + svc.On("SetSchema", mock.Anything, "sp", "env", "1", schema.New("name", field.String()).WithMetadata(extension.ExtensionMetadataKey, "test-extension")).Return(nil).Once() + }, + envsCall: func(svc *envmocks.Environments) { + svc.On("Migrate", mock.Anything, "sp", "env", &environments.MigrateOptions{Wait: true}).Return(nil).Once() + }, + wantErr: func(t *testing.T, err error) { + assert.NoError(t, err) + }, + }, + { + name: "Update view collection with the same id to new view collection", + collections: []*collections.Collection{{ID: "1", SpaceID: "sp", EnvID: "env", View: &collections.View{SpaceID: "sp3", EnvID: environments.DefaultEnvironment, CollectionID: "3"}}}, + collectionsCall: func(svc *mockscollections.Collections) { + svc.On("Get", mock.Anything, "sp", "env", "1").Return(&collections.Collection{ID: "1", SpaceID: "sp", EnvID: "env", View: &collections.View{SpaceID: "sp2", EnvID: environments.DefaultEnvironment, CollectionID: "2"}}, nil).Once() + svc.On("Update", mock.Anything, &collections.Collection{ID: "1", SpaceID: "sp", EnvID: "env", View: &collections.View{SpaceID: "sp3", EnvID: environments.DefaultEnvironment, CollectionID: "3"}}).Return(nil).Once() + }, + envsCall: func(svc *envmocks.Environments) { + svc.On("Migrate", mock.Anything, "sp", "env", &environments.MigrateOptions{Wait: true}).Return(nil).Once() + }, + wantErr: func(t *testing.T, err error) { + assert.NoError(t, err) + }, + }, { name: "Fail to install collection on migrate", collections: []*collections.Collection{{ID: "1", SpaceID: "sp", Name: "space", EnvID: "env", Schema: schema.New("name", field.String())}}, @@ -121,7 +194,7 @@ func TestSetup_InstallCollections(t *testing.T) { tt.envsCall(e) } - s := NewSetup(&content.Content{Collections: c, Environments: e}, "sp", "env", nil) + s := NewSetup(&content.Content{Collections: c, Environments: e}, "sp", "env", nil).WithForce(tt.force) s.AddCollections(tt.collections) tt.wantErr(t, s.InstallCollections(context.Background())) }) diff --git a/pkg/setup/setup_test.go b/pkg/setup/setup_test.go index 72773e29fa9faeb7c474d270081aee54e3dd03b7..7c2059732443bd697f4e9b618f36569e45626fa9 100644 --- a/pkg/setup/setup_test.go +++ b/pkg/setup/setup_test.go @@ -181,15 +181,9 @@ func TestSetupInstall(t *testing.T) { envMocks := &environmentMock.Environments{} collsMock := &collectionMock.Collections{} - for _, collection := range getCollections() { - collsMock.On("Get", mock.Anything, spaceID, envID, collection.ID). - Return(collection, nil). - Once() - - collsMock.On("Update", mock.Anything, collection). - Return(nil). - Once() - } + collsMock.On("Get", mock.Anything, spaceID, envID, "coll1"). + Return(&collections.Collection{ID: "coll1", SpaceID: spaceID, EnvID: envID, Schema: schema.New(), Name: "Коллекция старая"}, nil).Once() + collsMock.On("Update", mock.Anything, &collections.Collection{ID: "coll1", SpaceID: spaceID, EnvID: envID, Schema: schema.New(), Name: "Коллекция"}).Return(nil).Once() rMock := &rolesMock.Roles{} for _, role := range getRoles() { @@ -247,15 +241,9 @@ func TestSetupInstall(t *testing.T) { t.Run("Success, with force", func(t *testing.T) { collsMock := &collectionMock.Collections{} - for _, collection := range getCollections() { - collsMock.On("Get", mock.Anything, spaceID, envID, collection.ID). - Return(collection, nil). - Once() - - collsMock.On("Update", mock.Anything, collection). - Return(nil). - Once() - } + collsMock.On("Get", mock.Anything, spaceID, envID, "coll1"). + Return(&collections.Collection{ID: "coll1", SpaceID: spaceID, EnvID: envID, Schema: schema.New(), Name: "Коллекция старая"}, nil).Once() + collsMock.On("Update", mock.Anything, &collections.Collection{ID: "coll1", SpaceID: spaceID, EnvID: envID, Schema: schema.New(), Name: "Коллекция"}).Return(nil).Once() rMock := &rolesMock.Roles{} for _, role := range getRoles() { @@ -453,15 +441,8 @@ func TestSetupInstall(t *testing.T) { t.Run("Can't update collection, storage returns error", func(t *testing.T) { collsMock := &collectionMock.Collections{} - for _, collection := range getCollections() { - collsMock.On("Get", mock.Anything, spaceID, envID, collection.ID). - Return(collection, nil). - Once() - - collsMock.On("Update", mock.Anything, collection). - Return(errors.New("can't update collection")). - Once() - } + collsMock.On("Get", mock.Anything, spaceID, envID, "coll1").Return(&collections.Collection{ID: "coll1", SpaceID: spaceID, EnvID: envID, Schema: schema.New(), Name: "Коллекция 00"}, nil).Once() + collsMock.On("Update", mock.Anything, &collections.Collection{ID: "coll1", SpaceID: spaceID, EnvID: envID, Schema: schema.New(), Name: "Коллекция"}).Return(errors.New("can't update collection")).Once() rMock := &rolesMock.Roles{} for _, role := range getRoles() {