diff --git a/pkg/extension/action.go b/pkg/extension/action.go new file mode 100644 index 0000000000000000000000000000000000000000..43e4341bd3b1de027acc4b91a9d94aabbb84aba1 --- /dev/null +++ b/pkg/extension/action.go @@ -0,0 +1,72 @@ +package extension + +import ( + "git.perx.ru/perxis/perxis-go/pkg/references" + pb "git.perx.ru/perxis/perxis-go/proto/extensions" + "github.com/mitchellh/mapstructure" +) + +const ActionsCollectionID = "space_actions" + +type ( + ActionKind = pb.Action_Kind + //ActionRequest = pb.ActionRequest + //ActionResponse = pb.ActionResponse +) + +const ( + ResponseDone = pb.ActionResponse_DONE + ResponseError = pb.ActionResponse_ERROR + ResponsePending = pb.ActionResponse_PENDING + ResponseInProgress = pb.ActionResponse_IN_PROGRESS + ResponseParametersRequired = pb.ActionResponse_PARAMETERS_REQUIRED +) + +type Action struct { + Extension string `mapstructure:"extension,omitempty"` // Расширение + Action string `mapstructure:"action,omitempty"` // Рдентификатор действия + Name string `mapstructure:"name,omitempty"` // Название действия для отображения РІ интерфейсе (РїСѓРЅРєС‚ меню, РєРЅРѕРїРєР°). + Description string `mapstructure:"description,omitempty"` // Описание действия для отображения РІ интерфейсе + Icon string `mapstructure:"icon,omitempty"` // Название РёРєРѕРЅРєРё для отображения действия РІ интерфейсе + Image *references.Reference `mapstructure:"image,omitempty"` // Рзображение для отображения РІ действия РІ интерфейсе + Groups []string `mapstructure:"groups,omitempty"` // Группы отображения действия РІ интерфейсе + Kind ActionKind `mapstructure:"kind,omitempty"` // Указывает РЅР° что направлено действие + Classes []string `mapstructure:"classes,omitempty"` // Классы данных Рє которым применимо действие (название коллекций или специальных РіСЂСѓРїРї РІ рамках которых данное действие применимо) + Refs []*references.Reference `mapstructure:"refs,omitempty"` // Ссылки РЅР° записи используемые для выполнения действия (назначение ссылок зависит РѕС‚ действия Рё расширения) + ParamsCollection string `mapstructure:"params_collection,omitempty"` + Request *ActionRequest `mapstructure:"request,omitempty"` // Параметры запроса (используется РІ случае `ActionResponse.next`) + NavigationAction bool `mapstructure:"navigation_action,omitempty"` // Флаг указывающий что действие переносить пользователя РІ РґСЂСѓРіСѓСЋ часть интерфейса, Р° РЅРµ отправляет запрос РЅР° сервер + NavigationRoute string `mapstructure:"navigation_route,omitempty"` +} + +func ActionToMap(action *Action) map[string]interface{} { + res := make(map[string]interface{}) + mapstructure.Decode(action, &res) + res["kind"] = int64(action.Kind.Number()) + return res +} + +func ActionFromMap(d map[string]interface{}) (*Action, error) { + var action Action + err := mapstructure.Decode(d, &action) + return &action, err +} + +func ActionFromPB(a *pb.Action) *Action { + return &Action{ + Extension: a.Extension, + Action: a.Action, + Name: a.Name, + Description: a.Description, + Icon: a.Icon, + Image: references.ReferenceFromPB(a.Image), + Groups: a.Groups, + Kind: a.Kind, + Classes: a.Classes, + Refs: references.ReferenceListFromPB(a.Refs), + ParamsCollection: a.ParamsCollection, + Request: a.Request, + NavigationAction: a.NavigationAction, + NavigationRoute: a.NavigationRoute, + } +} diff --git a/pkg/extension/client.go b/pkg/extension/client.go new file mode 100644 index 0000000000000000000000000000000000000000..e9e5dff05f807b16b5700d71c962d39d84f625d6 --- /dev/null +++ b/pkg/extension/client.go @@ -0,0 +1,77 @@ +package extension + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/errors" + pb "git.perx.ru/perxis/perxis-go/proto/extensions" + "google.golang.org/grpc" +) + +type Client struct { + client pb.ExtensionClient +} + +func NewClient(conn *grpc.ClientConn) *Client { + return &Client{ + client: pb.NewExtensionClient(conn), + } +} + +func (c Client) GetDescriptor() *ExtensionDescriptor { + return nil +} + +type results interface { + GetResults() []*RequestResult +} + +func getErrors(out results, err, wrapErr error) error { + if err != nil { + return err + } + + res := out.GetResults() + + var errs []error + for _, r := range res { + if r.State == RequestError { + err := errors.New(r.GetError()) + if msg := r.GetMsg(); msg != "" { + err = errors.WithDetail(err, msg) + } + errs = append(errs, ExtensionError(err, r.Extension)) + } + } + + if len(errs) > 0 { + return errors.WithErrors(wrapErr, errs...) + } + + return nil +} + +func (c Client) Install(ctx context.Context, in *InstallRequest) error { + out, err := c.client.Install(ctx, in) + return getErrors(out, err, ErrInstall) +} + +func (c Client) Check(ctx context.Context, in *CheckRequest) error { + out, err := c.client.Check(ctx, in) + return getErrors(out, err, ErrCheck) +} + +func (c Client) Update(ctx context.Context, in *UpdateRequest) error { + out, err := c.client.Update(ctx, in) + return getErrors(out, err, ErrUpdate) +} + +func (c Client) Uninstall(ctx context.Context, in *UninstallRequest) error { + out, err := c.client.Uninstall(ctx, in) + return getErrors(out, err, ErrUninstall) +} + +func (c Client) Action(ctx context.Context, in *ActionRequest) (*ActionResponse, error) { + out, err := c.client.Action(ctx, in) + return out, err +} diff --git a/pkg/extension/client_test.go b/pkg/extension/client_test.go new file mode 100644 index 0000000000000000000000000000000000000000..d00032a5cb5f944a8dc4a3b6114d1fffaf14dbf7 --- /dev/null +++ b/pkg/extension/client_test.go @@ -0,0 +1,83 @@ +package extension + +import ( + "testing" + + "git.perx.ru/perxis/perxis-go/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetErrors(t *testing.T) { + + getOut := func(results ...*RequestResult) results { return &InstallResponse{Results: results} } + + tests := []struct { + name string + out results + err error + wrapErr error + checkErr func(t *testing.T, err error) + }{ + { + name: "no errors", + out: getOut(&RequestResult{State: RequestOK, Extension: "a"}), + }, + { + name: "no error", + out: getOut(&RequestResult{State: RequestOK, Extension: "a"}), + wrapErr: ErrInstall, + }, + { + name: "one errored result", + out: getOut(&RequestResult{State: RequestError, Extension: "a", Error: "some err", Msg: "Ошибка"}), + wrapErr: ErrInstall, + checkErr: func(t *testing.T, err error) { + assert.ErrorIs(t, err, ErrInstall) + errs := errors.GetErrors(err) + require.Len(t, errs, 1) + + extErr := errs[0] + assert.Equal(t, "a", ExtensionFromError(extErr)) + assert.Equal(t, "some err", extErr.Error()) + assert.Equal(t, "Ошибка", errors.GetDetail(extErr)) + }, + }, + { + name: "multiple results, some of them errored", + out: getOut( + &RequestResult{State: RequestError, Extension: "a", Error: "some err a", Msg: "Ошибка Рђ"}, + &RequestResult{State: RequestOK, Extension: "b"}, + &RequestResult{State: RequestError, Extension: "c", Error: "some err c", Msg: "Ошибка РЎ"}, + &RequestResult{State: RequestOK, Extension: "d"}, + ), + wrapErr: ErrInstall, + checkErr: func(t *testing.T, err error) { + assert.ErrorIs(t, err, ErrInstall) + errs := errors.GetErrors(err) + require.Len(t, errs, 2) + + extErr1 := errs[0] + assert.Equal(t, "a", ExtensionFromError(extErr1)) + assert.Equal(t, "some err a", extErr1.Error()) + assert.Equal(t, "Ошибка Рђ", errors.GetDetail(extErr1)) + + extErr2 := errs[1] + assert.Equal(t, "c", ExtensionFromError(extErr2)) + assert.Equal(t, "some err c", extErr2.Error()) + assert.Equal(t, "Ошибка РЎ", errors.GetDetail(extErr2)) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := getErrors(tt.out, tt.err, tt.wrapErr) + if tt.checkErr != nil { + require.Error(t, err) + tt.checkErr(t, err) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/pkg/extension/extension.go b/pkg/extension/extension.go new file mode 100644 index 0000000000000000000000000000000000000000..63691c62c050d109daea0c68c003f4fba1d9ca0e --- /dev/null +++ b/pkg/extension/extension.go @@ -0,0 +1,109 @@ +package extension + +import ( + "context" + "fmt" + + "git.perx.ru/perxis/perxis-go/pkg/content" + "git.perx.ru/perxis/perxis-go/pkg/errors" + "git.perx.ru/perxis/perxis-go/pkg/items" + pb "git.perx.ru/perxis/perxis-go/proto/extensions" +) + +const ( + RequestOK = pb.ExtensionRequestResult_OK + RequestError = pb.ExtensionRequestResult_ERROR + RequestPending = pb.ExtensionRequestResult_PENDING + RequestInProgress = pb.ExtensionRequestResult_IN_PROGRESS + RequestParametersRequires = pb.ExtensionRequestResult_PARAMETERS_REQUIRED + + ExtensionsCollectionID = "space_extensions" + + StatePending = pb.SpaceExtensions_PENDING + StateInstalled = pb.SpaceExtensions_INSTALLED + StateInProgress = pb.SpaceExtensions_IN_PROGRESS + StateFail = pb.SpaceExtensions_FAIL +) + +type ( + InstallRequest = pb.InstallRequest + InstallResponse = pb.InstallResponse + CheckRequest = pb.CheckRequest + CheckResponse = pb.CheckResponse + UpdateRequest = pb.UpdateRequest + UpdateResponse = pb.UpdateResponse + UninstallRequest = pb.UninstallRequest + UninstallResponse = pb.UninstallResponse + RequestResult = pb.ExtensionRequestResult + + ActionRequest = pb.ActionRequest + ActionResponse = pb.ActionResponse + + ExtensionDescriptor = pb.ExtensionDescriptor +) + +var ( + ErrStart = errors.New("start failed") + ErrStop = errors.New("stop failed") + + ErrInstall = errors.New("install failed") + ErrUpdate = errors.New("update failed") + ErrCheck = errors.New("check failed") + ErrUninstall = errors.New("uninstall failed") + + ErrUpdateAvailable = errors.New("update available") + ErrNotInstalled = errors.New("not installed") + ErrUnknownExtension = errors.New("unknown extension") +) + +// Runnable описывает интерфейс сервиса СЃ запуском Рё остановкой. Вызывается сервером расширений +type Runnable interface { + Start() error + Stop() error +} + +// Extension описывает интерфейс расширения Perxis +type Extension interface { + // GetDescriptor возвращает описание расширения + GetDescriptor() *ExtensionDescriptor + + // Install вызывается РїСЂРё установке расширения РІ пространство + Install(ctx context.Context, in *InstallRequest) error + + // Check вызывается для проверки состояния расширения + Check(ctx context.Context, in *CheckRequest) error + + // Update вызывается для обновления вресии расширения + Update(ctx context.Context, in *UpdateRequest) error + + // Uninstall вызывается для удаления расширения РёР· пространства + Uninstall(ctx context.Context, in *UninstallRequest) error + + // Action вызывается для выполнения расширением действия + Action(ctx context.Context, in *ActionRequest) (*ActionResponse, error) +} + +func CheckInstalled(ctx context.Context, content *content.Content, spaceID, envID, extension string) (bool, error) { + res, _, err := content.Items.Find(ctx, spaceID, envID, ExtensionsCollectionID, + &items.Filter{Q: []string{fmt.Sprintf("extension == '%s'", extension)}}) + + if err != nil { + return false, err + } + + if len(res) == 0 || res[0].Data["extension_state"] != int64(StateInstalled) { + return false, nil + } + + return true, nil +} + +func ExtensionError(err error, ext string) error { + return errors.WithContext(err, "extension", ext) +} + +func ExtensionFromError(err error) string { + v, _ := errors.ContextKey(err, "extension") + ext, _ := v.(string) + return ext +} diff --git a/pkg/extension/key.go b/pkg/extension/key.go new file mode 100644 index 0000000000000000000000000000000000000000..a93bafe2fc72fd39e80d617fc548512f6f61e663 --- /dev/null +++ b/pkg/extension/key.go @@ -0,0 +1,55 @@ +package extension + +import ( + "crypto/sha512" + "encoding/base64" + "strings" +) + +// KeyFunc функция получения ключа доступа Рє указанному пространству +type KeyFunc func(spaceID string) string +type SignatureFunc func(spaceID string) string + +func NamedSignedKey(name string, signature any) KeyFunc { + switch v := signature.(type) { + case string: + return func(spaceID string) string { + return Sign(name, spaceID, v) + } + case SignatureFunc: + return func(spaceID string) string { + return Sign(name, spaceID, v(spaceID)) + } + } + panic("incorrect signature") +} + +func ExtensionSignedKey(ext, signature any) KeyFunc { + var desc *ExtensionDescriptor + + switch v := ext.(type) { + case Extension: + desc = v.GetDescriptor() + case *ExtensionDescriptor: + desc = v + } + + switch v := signature.(type) { + case string: + return func(spaceID string) string { + return Sign(desc.Extension, spaceID, v) + } + case SignatureFunc: + return func(spaceID string) string { + return Sign(desc.Extension, spaceID, v(spaceID)) + } + } + panic("incorrect signature") +} + +func Sign(vals ...string) string { + value := strings.Join(vals, ".") + hash := sha512.Sum512([]byte(value)) + encoded := base64.StdEncoding.EncodeToString(hash[:]) + return encoded +} diff --git a/pkg/extension/mocks/Extension.go b/pkg/extension/mocks/Extension.go new file mode 100644 index 0000000000000000000000000000000000000000..87710ccff1d98aa5e0922d7e1563caad8c9f21f1 --- /dev/null +++ b/pkg/extension/mocks/Extension.go @@ -0,0 +1,126 @@ +// Code generated by mockery v2.14.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + extensions "git.perx.ru/perxis/perxis-go/proto/extensions" + + mock "github.com/stretchr/testify/mock" +) + +// Extension is an autogenerated mock type for the Extension type +type Extension struct { + mock.Mock +} + +// Action provides a mock function with given fields: ctx, in +func (_m *Extension) Action(ctx context.Context, in *extensions.ActionRequest) (*extensions.ActionResponse, error) { + ret := _m.Called(ctx, in) + + var r0 *extensions.ActionResponse + if rf, ok := ret.Get(0).(func(context.Context, *extensions.ActionRequest) *extensions.ActionResponse); ok { + r0 = rf(ctx, in) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*extensions.ActionResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *extensions.ActionRequest) error); ok { + r1 = rf(ctx, in) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Check provides a mock function with given fields: ctx, in +func (_m *Extension) Check(ctx context.Context, in *extensions.CheckRequest) error { + ret := _m.Called(ctx, in) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *extensions.CheckRequest) error); ok { + r0 = rf(ctx, in) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetDescriptor provides a mock function with given fields: +func (_m *Extension) GetDescriptor() *extensions.ExtensionDescriptor { + ret := _m.Called() + + var r0 *extensions.ExtensionDescriptor + if rf, ok := ret.Get(0).(func() *extensions.ExtensionDescriptor); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*extensions.ExtensionDescriptor) + } + } + + return r0 +} + +// Install provides a mock function with given fields: ctx, in +func (_m *Extension) Install(ctx context.Context, in *extensions.InstallRequest) error { + ret := _m.Called(ctx, in) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *extensions.InstallRequest) error); ok { + r0 = rf(ctx, in) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Uninstall provides a mock function with given fields: ctx, in +func (_m *Extension) Uninstall(ctx context.Context, in *extensions.UninstallRequest) error { + ret := _m.Called(ctx, in) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *extensions.UninstallRequest) error); ok { + r0 = rf(ctx, in) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Update provides a mock function with given fields: ctx, in +func (_m *Extension) Update(ctx context.Context, in *extensions.UpdateRequest) error { + ret := _m.Called(ctx, in) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *extensions.UpdateRequest) error); ok { + r0 = rf(ctx, in) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +type mockConstructorTestingTNewExtension interface { + mock.TestingT + Cleanup(func()) +} + +// NewExtension creates a new instance of Extension. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewExtension(t mockConstructorTestingTNewExtension) *Extension { + mock := &Extension{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/extension/server.go b/pkg/extension/server.go new file mode 100644 index 0000000000000000000000000000000000000000..19fcdf55b4d9cf27e878d6e89abee09cabd2352e --- /dev/null +++ b/pkg/extension/server.go @@ -0,0 +1,136 @@ +package extension + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/errors" + pb "git.perx.ru/perxis/perxis-go/proto/extensions" +) + +type Server struct { + services map[string]Extension + pb.UnimplementedExtensionServer +} + +func NewServer(svc ...Extension) *Server { + srv := &Server{ + services: make(map[string]Extension, len(svc)), + } + for _, s := range svc { + srv.services[s.GetDescriptor().Extension] = s + } + return srv +} + +func getResult(ext string, err error) *RequestResult { + res := new(RequestResult) + res.Extension = ext + res.State = RequestOK + + if err != nil { + res.State = RequestError + res.Error = err.Error() + errs := errors.GetErrors(err) + if errs == nil { + errs = append(errs, err) + } + for _, e := range errs { + res.Msg += errors.GetDetail(e) + "\n" + } + } + return res +} + +func (srv *Server) getResults(extensions []string, fn func(svc Extension) error) []*RequestResult { + + var results []*RequestResult + + for _, e := range extensions { + svc, ok := srv.services[e] + if !ok { + results = append(results, getResult(e, ErrUnknownExtension)) + continue + } + + err := fn(svc) + results = append(results, getResult(e, err)) + } + + return results +} + +func (srv *Server) Install(ctx context.Context, request *InstallRequest) (*InstallResponse, error) { + res := srv.getResults(request.Extensions, func(svc Extension) error { return svc.Install(ctx, request) }) + return &InstallResponse{Results: res}, nil +} + +func (srv *Server) Check(ctx context.Context, request *CheckRequest) (*CheckResponse, error) { + res := srv.getResults(request.Extensions, func(svc Extension) error { return svc.Check(ctx, request) }) + return &CheckResponse{Results: res}, nil +} + +func (srv *Server) Uninstall(ctx context.Context, request *UninstallRequest) (*UninstallResponse, error) { + res := srv.getResults(request.Extensions, func(svc Extension) error { return svc.Uninstall(ctx, request) }) + return &UninstallResponse{Results: res}, nil +} + +func (srv *Server) Update(ctx context.Context, request *UpdateRequest) (*UpdateResponse, error) { + res := srv.getResults(request.Extensions, func(svc Extension) error { return svc.Update(ctx, request) }) + return &UpdateResponse{Results: res}, nil +} + +func (srv *Server) Action(ctx context.Context, in *ActionRequest) (*ActionResponse, error) { + + svc, ok := srv.services[in.Extension] + if !ok { + return nil, ErrUnknownExtension + } + + out, err := svc.Action(ctx, in) + + if out == nil { + out = &ActionResponse{} + } + + if err != nil { + out.State = ResponseError + out.Error = err.Error() + out.Msg += errors.GetDetail(err) + } + + return out, nil +} + +func (srv *Server) Start() error { + var errs []error + for _, svc := range srv.services { + if r, ok := svc.(Runnable); ok { + if err := r.Start(); err != nil { + errs = append(errs, err) + } + } + } + + if len(errs) > 0 { + return errors.WithErrors(ErrStart, errs...) + } + + return nil +} + +func (srv *Server) Stop() error { + var errs []error + for _, svc := range srv.services { + if r, ok := svc.(Runnable); ok { + if err := r.Stop(); err != nil { + errs = append(errs, err) + } + } + } + + if len(errs) > 0 { + return errors.WithErrors(ErrStop, errs...) + } + + return nil +} diff --git a/pkg/extension/server_test.go b/pkg/extension/server_test.go new file mode 100644 index 0000000000000000000000000000000000000000..98d307d198a71541dd779231edc09c5b0467f84c --- /dev/null +++ b/pkg/extension/server_test.go @@ -0,0 +1,90 @@ +package extension + +import ( + "reflect" + "strings" + "testing" + + "git.perx.ru/perxis/perxis-go/pkg/errors" + "git.perx.ru/perxis/perxis-go/pkg/extension/mocks" + "github.com/stretchr/testify/mock" +) + +func TestGetResults(t *testing.T) { + + getMockExtension := func(name string, wantErr ...bool) Extension { + ext := &mocks.Extension{} + ext.On("GetDescriptor").Return(&ExtensionDescriptor{ + Extension: name, + Title: strings.ToTitle(name), + Description: "test extension", + Version: "0.0.0", + }) + + var err error + if len(wantErr) > 0 { + err = errors.WithDetail(errors.New("some err"), "Ошибка") + } + + ext.On("Install", mock.Anything, mock.Anything).Return(err) + return ext + } + + tests := []struct { + name string + services []Extension + extensions []string + fn func(svc Extension) error + want []*RequestResult + }{ + { + name: "one extension without errors", + services: []Extension{getMockExtension("a"), getMockExtension("b")}, + extensions: []string{"a"}, + fn: func(svc Extension) error { return nil }, + want: []*RequestResult{ + {Extension: "a", State: RequestOK}, + }, + }, + { + name: "multiple extensions without errors", + services: []Extension{getMockExtension("a"), getMockExtension("b"), getMockExtension("c")}, + extensions: []string{"a", "c"}, + fn: func(svc Extension) error { return nil }, + want: []*RequestResult{ + {Extension: "a", State: RequestOK}, + {Extension: "c", State: RequestOK}, + }, + }, + { + name: "multiple extensions, one returns error", + services: []Extension{getMockExtension("a"), getMockExtension("b"), getMockExtension("c", true)}, + extensions: []string{"a", "c"}, + fn: func(svc Extension) error { return svc.Install(nil, nil) }, + want: []*RequestResult{ + {Extension: "a", State: RequestOK}, + {Extension: "c", State: RequestError, Error: "some err", Msg: "Ошибка\n"}, + }, + }, + { + name: "multiple extensions, all return error", + services: []Extension{getMockExtension("a", true), getMockExtension("b", true), getMockExtension("c", true)}, + extensions: []string{"a", "b", "c"}, + fn: func(svc Extension) error { return svc.Install(nil, nil) }, + want: []*RequestResult{ + {Extension: "a", State: RequestError, Error: "some err", Msg: "Ошибка\n"}, + {Extension: "b", State: RequestError, Error: "some err", Msg: "Ошибка\n"}, + {Extension: "c", State: RequestError, Error: "some err", Msg: "Ошибка\n"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + srv := NewServer(tt.services...) + if got := srv.getResults(tt.extensions, tt.fn); !reflect.DeepEqual(got, tt.want) { + t.Errorf("getResults() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/setup/client.go b/pkg/setup/client.go new file mode 100644 index 0000000000000000000000000000000000000000..35f303a1c4734be22144cd26c77f0047348ccb4c --- /dev/null +++ b/pkg/setup/client.go @@ -0,0 +1,199 @@ +package setup + +import ( + "context" + "strings" + + "git.perx.ru/perxis/perxis-go/pkg/clients" + "git.perx.ru/perxis/perxis-go/pkg/errors" + "go.uber.org/zap" +) + +var ( + ErrCheckClients = errors.New("clients check error") + ErrInstallClients = errors.New("failed to install clients") + ErrUninstallClients = errors.New("failed to uninstall clients") +) + +type ClientsOption func(c *ClientConfig) +type UpdateClientFn func(s *Setup, exist, new *clients.Client) (*clients.Client, bool) +type DeleteClientFn func(s *Setup, client *clients.Client) bool + +type ClientConfig struct { + client *clients.Client + UpdateFn UpdateClientFn + DeleteFn DeleteClientFn +} + +func NewClientConfig(client *clients.Client, opt ...ClientsOption) ClientConfig { + c := ClientConfig{client: client} + + UpdateExistingClient()(&c) + DeleteClientIfRemove()(&c) + + for _, o := range opt { + o(&c) + } + + return c +} + +func OverwriteClient() ClientsOption { + return func(c *ClientConfig) { + c.UpdateFn = func(s *Setup, old, new *clients.Client) (*clients.Client, bool) { return new, true } + } +} + +func KeepExistingClient() ClientsOption { + return func(c *ClientConfig) { + c.UpdateFn = func(s *Setup, old, new *clients.Client) (*clients.Client, bool) { return old, false } + } +} + +func DeleteClient() ClientsOption { + return func(c *ClientConfig) { + c.DeleteFn = func(s *Setup, client *clients.Client) bool { return true } + } +} + +func DeleteClientIfRemove() ClientsOption { + return func(c *ClientConfig) { + c.DeleteFn = func(s *Setup, client *clients.Client) bool { return s.IsRemove() } + } +} + +func UpdateExistingClient() ClientsOption { + return func(c *ClientConfig) { + c.UpdateFn = func(s *Setup, exist, client *clients.Client) (*clients.Client, bool) { + if exist.Name == "" { + exist.Name = client.Name + } + + if exist.Description == "" { + exist.Description = client.Description + } + + if exist.OAuth == nil { + exist.OAuth = client.OAuth + } + + if exist.TLS == nil { + exist.TLS = client.TLS + } + + if exist.APIKey == nil { + exist.APIKey = client.APIKey + } + + exist.Disabled = client.Disabled + exist.RoleID = client.RoleID + + return exist, true + } + } +} + +func (s *Setup) InstallClients(ctx context.Context) error { + if len(s.Clients) == 0 { + return nil + } + + s.logger.Debug("Install clients", zap.String("Space ID", s.SpaceID)) + + var errs []error + for _, c := range s.Clients { + err := s.InstallClient(ctx, c) + if err != nil { + errs = append(errs, errors.WithDetailf(err, "Возникла ошибка РїСЂРё настройке клиента %s(%s)", c.client.Name, c.client.ID)) + s.logger.Error("Failed to install client", zap.String("Client ID", c.client.ID), zap.String("Client Name", c.client.Name), zap.Error(err)) + } + } + + if len(errs) > 0 { + return errors.WithErrors(ErrInstallClients, errs...) + } + + return nil +} + +func (s *Setup) InstallClient(ctx context.Context, c ClientConfig) error { + client := c.client + client.SpaceID = s.SpaceID + + if s.IsForce() { + s.content.Clients.Delete(ctx, s.SpaceID, c.client.ID) + _, err := s.content.Clients.Create(ctx, c.client) + return err + } + + exist, err := s.content.Clients.Get(ctx, s.SpaceID, c.client.ID) + if err != nil { + if !strings.Contains(err.Error(), clients.ErrNotFound.Error()) { + return err + } + + _, err = s.content.Clients.Create(ctx, c.client) + return err + } + + if client, upd := c.UpdateFn(s, exist, c.client); upd { + return s.content.Clients.Update(ctx, client) + } + + return nil +} + +func (s *Setup) CheckClients(ctx context.Context) (err error) { + if len(s.Clients) == 0 { + return nil + } + + var errs []error + s.logger.Debug("Check clients", zap.String("Space ID", s.SpaceID)) + for _, c := range s.Clients { + err := s.CheckClient(ctx, c.client) + if err != nil { + errs = append(errs, errors.WithDetailf(err, "РќРµ найден клиент %s(%s)", c.client.Name, c.client.ID)) + } + } + + if len(errs) > 0 { + return errors.WithErrors(ErrCheckClients, errs...) + } + + return nil +} + +func (s *Setup) CheckClient(ctx context.Context, client *clients.Client) error { + _, err := s.content.Clients.Get(ctx, s.SpaceID, client.ID) + return err +} + +func (s *Setup) UninstallClients(ctx context.Context) error { + if len(s.Clients) == 0 { + return nil + } + + s.logger.Debug("Uninstall clients", zap.String("Space ID", s.SpaceID)) + + var errs []error + for _, c := range s.Clients { + if err := s.UninstallClient(ctx, c); err != nil { + errs = append(errs, errors.WithDetailf(err, "Возникла ошибка РїСЂРё удалении клиента %s(%s)", c.client.Name, c.client.ID)) + s.logger.Error("Failed to uninstall client", zap.String("Client ID", c.client.ID), zap.String("Client Name", c.client.Name), zap.Error(err)) + } + } + + if len(errs) > 0 { + return errors.WithErrors(ErrUninstallClients, errs...) + } + + return nil +} + +func (s *Setup) UninstallClient(ctx context.Context, c ClientConfig) error { + if c.DeleteFn(s, c.client) { + return s.content.Clients.Delete(ctx, s.SpaceID, c.client.ID) + } + return nil +} diff --git a/pkg/setup/collection.go b/pkg/setup/collection.go new file mode 100644 index 0000000000000000000000000000000000000000..176fddc59a959e023e5756d7bd4962aaed6a83fc --- /dev/null +++ b/pkg/setup/collection.go @@ -0,0 +1,225 @@ +package setup + +import ( + "context" + "reflect" + "strings" + + "git.perx.ru/perxis/perxis-go/pkg/collections" + "git.perx.ru/perxis/perxis-go/pkg/environments" + "git.perx.ru/perxis/perxis-go/pkg/errors" + "go.uber.org/zap" +) + +var ( + ErrCheckCollections = errors.New("collections check error") + ErrInstallCollections = errors.New("failed to install collections") + ErrUninstallCollections = errors.New("failed to uninstall collections") +) + +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 CollectionConfig struct { + collection *collections.Collection + UpdateFn UpdateCollectionFn + DeleteFn DeleteCollectionFn +} + +func NewCollectionConfig(collection *collections.Collection, opt ...CollectionsOption) CollectionConfig { + c := CollectionConfig{collection: collection} + + UpdateExistingCollection()(&c) + DeleteCollectionIfRemove()(&c) + + for _, o := range opt { + o(&c) + } + + return c +} + +func OverwriteCollection() CollectionsOption { + return func(c *CollectionConfig) { + c.UpdateFn = func(s *Setup, old, new *collections.Collection) (*collections.Collection, bool, bool) { + return new, true, true + } + } +} + +func KeepExistingCollection() CollectionsOption { + return func(c *CollectionConfig) { + c.UpdateFn = func(s *Setup, old, new *collections.Collection) (*collections.Collection, bool, bool) { + return old, false, false + } + } +} + +func DeleteCollection() CollectionsOption { + return func(c *CollectionConfig) { + c.DeleteFn = func(s *Setup, collection *collections.Collection) bool { return true } + } +} + +func DeleteCollectionIfRemove() CollectionsOption { + return func(c *CollectionConfig) { + c.DeleteFn = func(s *Setup, collection *collections.Collection) bool { return s.IsRemove() } + } +} + +func UpdateExistingCollection() CollectionsOption { + return func(c *CollectionConfig) { + c.UpdateFn = func(s *Setup, exist, collection *collections.Collection) (*collections.Collection, bool, bool) { + if len(exist.Tags) > 0 { + collection.Tags = append(exist.Tags, collection.Tags...) + } + + return collection, true, !collection.IsView() && !reflect.DeepEqual(exist.Schema, collection.Schema) + } + } +} + +func (s *Setup) InstallCollections(ctx context.Context) (err error) { + if len(s.Collections) == 0 { + return nil + } + + s.logger.Debug("Install collections", zap.Int("Collections", len(s.Collections))) + + var errs []error + var migrate, setSchema bool + + for _, c := range s.Collections { + setSchema, err = s.InstallCollection(ctx, c) + if err != nil { + errs = append(errs, errors.WithDetailf(err, "Возникла ошибка РїСЂРё настройке коллекции %s(%s)", c.collection.Name, c.collection.ID)) + s.logger.Error("Failed to install collection", + zap.String("Collection ID", c.collection.ID), + zap.String("Collection Name", c.collection.Name), + zap.Error(err), + ) + } + if setSchema { + migrate = true + } + } + + if len(errs) > 0 { + return errors.WithErrors(ErrInstallCollections, errs...) + } + + if migrate { + if err = s.content.Environments.Migrate(ctx, s.SpaceID, s.EnvironmentID, &environments.MigrateOptions{Wait: true}); err != nil { + s.logger.Error( + "Failed to migrate environment", + zap.String("Space ID", s.SpaceID), + zap.String("Environment ID", s.EnvironmentID), + zap.Error(err), + ) + + return errors.WithErrors(ErrInstallCollections, err) + } + } + + return nil +} + +func (s *Setup) InstallCollection(ctx context.Context, c CollectionConfig) (setSchema bool, err error) { + collection := c.collection + collection.SpaceID, collection.EnvID = s.SpaceID, s.EnvironmentID + + var exist *collections.Collection + // isForce - РЅРµ удалять коллекцию, если РѕРЅР° СѓР¶Рµ существует + exist, err = s.content.Collections.Get(ctx, collection.SpaceID, collection.EnvID, collection.ID) + if err != nil && !strings.Contains(err.Error(), collections.ErrNotFound.Error()) { + return false, err + } + + if exist == nil { + setSchema = !collection.IsView() + exist, 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) + if upd { + if err = s.content.Collections.Update(ctx, collection); err != nil { + return false, err + } + } + } + + if setSchema { + err = s.content.Collections.SetSchema(ctx, collection.SpaceID, collection.EnvID, collection.ID, collection.Schema) + if err != nil { + return false, err + } + } + + return setSchema, nil +} + +func (s *Setup) CheckCollections(ctx context.Context) error { + if len(s.Collections) == 0 { + return nil + } + + s.logger.Debug("Check collections", zap.Int("Collections", len(s.Collections))) + + var errs []error + for _, c := range s.Collections { + if err := s.CheckCollection(ctx, c); err != nil { + errs = append(errs, errors.WithDetailf(err, "РќРµ найдена коллекция %s(%s)", c.collection.ID, c.collection.Name)) + } + } + + if len(errs) > 0 { + return errors.WithErrors(ErrCheckCollections, errs...) + } + + return nil +} + +func (s *Setup) CheckCollection(ctx context.Context, c CollectionConfig) (err error) { + _, err = s.content.Collections.Get(ctx, s.SpaceID, s.EnvironmentID, c.collection.ID) + return err +} + +func (s *Setup) UninstallCollections(ctx context.Context) error { + if len(s.Collections) == 0 { + return nil + } + + s.logger.Debug("Uninstall collections", zap.Int("Collections", len(s.Collections))) + + var errs []error + for _, c := range s.Collections { + if err := s.UninstallCollection(ctx, c); err != nil { + errs = append(errs, errors.WithDetailf(err, "Возникла ошибка РїСЂРё удалении коллекции %s(%s)", c.collection.Name, c.collection.ID)) + s.logger.Error("Failed to uninstall collection", + zap.String("Collection ID", c.collection.ID), + zap.String("Collection Name", c.collection.Name), + zap.Error(err), + ) + } + } + + if len(errs) > 0 { + return errors.WithErrors(ErrUninstallCollections, errs...) + } + + return nil +} + +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()) { + return err + } + s.removeItems(c.collection.ID) // после удаления коллекции нет смысла удалять ее элементы + } + return nil +} diff --git a/pkg/setup/item.go b/pkg/setup/item.go new file mode 100644 index 0000000000000000000000000000000000000000..79aa76ac1589f3c5b85e05784b048a4e3436dce7 --- /dev/null +++ b/pkg/setup/item.go @@ -0,0 +1,203 @@ +package setup + +import ( + "context" + "strings" + + "git.perx.ru/perxis/perxis-go/pkg/errors" + "git.perx.ru/perxis/perxis-go/pkg/items" + "go.uber.org/zap" +) + +var ( + ErrCheckItems = errors.New("items check error") + ErrInstallItems = errors.New("failed to install items") + ErrUninstallItems = errors.New("failed to uninstall items") + ErrItemsNotFound = errors.New("item not found") +) + +type ItemsOption func(c *ItemConfig) +type UpdateItemFn func(s *Setup, exist, new *items.Item) (*items.Item, bool) +type DeleteItemFn func(s *Setup, col *items.Item) bool + +type ItemConfig struct { + item *items.Item + UpdateFn UpdateItemFn + DeleteFn DeleteItemFn +} + +func NewItemConfig(item *items.Item, opt ...ItemsOption) ItemConfig { + c := ItemConfig{item: item} + + KeepExistingItem()(&c) + DeleteItemIfRemove()(&c) + + for _, o := range opt { + o(&c) + } + + return c +} + +func OverwriteItem() ItemsOption { + return func(c *ItemConfig) { + c.UpdateFn = func(s *Setup, old, new *items.Item) (*items.Item, bool) { return new, true } + } +} + +func KeepExistingItem() ItemsOption { + return func(c *ItemConfig) { + c.UpdateFn = func(s *Setup, old, new *items.Item) (*items.Item, bool) { return old, false } + } +} + +func DeleteItem() ItemsOption { + return func(c *ItemConfig) { + c.DeleteFn = func(s *Setup, item *items.Item) bool { return true } + } +} + +func DeleteItemIfRemove() ItemsOption { + return func(c *ItemConfig) { + c.DeleteFn = func(s *Setup, item *items.Item) bool { return s.IsRemove() } + } +} + +func (s *Setup) InstallItems(ctx context.Context) error { + if len(s.Items) == 0 { + return nil + } + + s.logger.Debug("Install items", zap.Int("Items", len(s.Items))) + + var errs []error + for _, c := range s.Items { + if err := s.InstallItem(ctx, c); err != nil { + errs = append(errs, errors.WithDetailf(err, "Возникла ошибка РїСЂРё добавлении элемента %s(%s)", c.item.ID, c.item.CollectionID)) + s.logger.Error("Failed to install item", + zap.String("Item", c.item.ID), + zap.String("Item", c.item.CollectionID), + zap.Error(err), + ) + } + } + + if len(errs) > 0 { + return errors.WithErrors(ErrInstallItems, errs...) + } + + return nil +} + +func (s *Setup) InstallItem(ctx context.Context, c ItemConfig) error { + item := c.item + item.SpaceID, item.EnvID = s.SpaceID, s.EnvironmentID + + exist, err := s.content.Items.Get(ctx, item.SpaceID, item.EnvID, item.CollectionID, item.ID) + if err != nil && !strings.Contains(err.Error(), items.ErrNotFound.Error()) { + return err + } + if exist == nil { + return s.CreateAndPublishItem(ctx, item) + } + + if item, changed := c.UpdateFn(s, exist, c.item); changed { + return s.UpdateAndPublishItem(ctx, item) + } + + return nil +} + +func (s *Setup) UninstallItems(ctx context.Context) error { + if len(s.Items) == 0 { + return nil + } + + s.logger.Debug("Uninstall items", zap.Int("Items", len(s.Items))) + + var errs []error + for _, c := range s.Items { + if err := s.UninstallItem(ctx, c); err != nil { + errs = append(errs, errors.WithDetailf(err, "Возникла ошибка РїСЂРё удалении элемента %s(%s)", c.item.ID, c.item.CollectionID)) + s.logger.Error("Failed to uninstall item", + zap.String("Item", c.item.ID), + zap.String("Item", c.item.CollectionID), + zap.Error(err), + ) + } + } + + if len(errs) > 0 { + return errors.WithErrors(ErrUninstallItems, errs...) + } + + return nil +} + +func (s *Setup) UninstallItem(ctx context.Context, c ItemConfig) error { + if c.DeleteFn(s, c.item) { + err := s.content.Items.Delete(ctx, s.SpaceID, s.EnvironmentID, c.item.CollectionID, c.item.ID) + if err != nil && !strings.Contains(err.Error(), items.ErrNotFound.Error()) { + return err + } + } + return nil +} + +func (s *Setup) CheckItems(ctx context.Context) error { + if len(s.Items) == 0 { + return nil + } + + var errs []error + s.logger.Debug("Check items", zap.Int("Items", len(s.Items))) + + for _, c := range s.Items { + if err := s.CheckItem(ctx, c); err != nil { + errs = append(errs, errors.WithDetailf(err, "РќРµ найден элемент %s(%s)", c.item.ID, c.item.CollectionID)) + } + } + + if len(errs) > 0 { + return errors.WithErrors(ErrCheckItems, errs...) + } + + return nil +} + +func (s *Setup) CheckItem(ctx context.Context, c ItemConfig) (err error) { + _, err = s.content.Items.Get(ctx, s.SpaceID, s.EnvironmentID, c.item.CollectionID, c.item.ID) + return err +} + +func (s *Setup) CreateAndPublishItem(ctx context.Context, item *items.Item) error { + var err error + if item, err = s.content.Items.Create(ctx, item); err != nil { + return errors.Wrap(err, "create item") + } + if err = s.content.Items.Publish(ctx, item); err != nil { + return errors.Wrap(err, "publish item") + } + return nil +} + +func (s *Setup) UpdateAndPublishItem(ctx context.Context, item *items.Item) error { + var err error + if err = s.content.Items.Update(ctx, item); err != nil { + return errors.Wrap(err, "update item") + } + if err = s.content.Items.Publish(ctx, item); err != nil { + return errors.Wrap(err, "publish item") + } + return nil +} + +func (s *Setup) removeItems(collID string) { + itms := make([]ItemConfig, 0, len(s.Items)) + for _, i := range s.Items { + if i.item.CollectionID != collID { + itms = append(itms, i) + } + } + s.Items = itms +} diff --git a/pkg/setup/role.go b/pkg/setup/role.go new file mode 100644 index 0000000000000000000000000000000000000000..081696373517eb726a809c3fb509e46525a628c8 --- /dev/null +++ b/pkg/setup/role.go @@ -0,0 +1,211 @@ +package setup + +import ( + "context" + "strings" + + "git.perx.ru/perxis/perxis-go/pkg/data" + "git.perx.ru/perxis/perxis-go/pkg/errors" + "git.perx.ru/perxis/perxis-go/pkg/permission" + "git.perx.ru/perxis/perxis-go/pkg/roles" + "go.uber.org/zap" +) + +var ( + ErrCheckRoles = errors.New("role check error") + ErrInstallRoles = errors.New("failed to install role") + ErrUninstallRoles = errors.New("failed to uninstall role") +) + +type RolesOption func(c *RoleConfig) +type UpdateRoleFn func(s *Setup, exist, new *roles.Role) (*roles.Role, bool) +type DeleteRoleFn func(s *Setup, role *roles.Role) bool + +type RoleConfig struct { + role *roles.Role + UpdateFn UpdateRoleFn + DeleteFn DeleteRoleFn +} + +func NewRoleConfig(role *roles.Role, opt ...RolesOption) RoleConfig { + c := RoleConfig{role: role} + + UpdateExistingRole()(&c) + DeleteRoleIfRemove()(&c) + + for _, o := range opt { + o(&c) + } + return c +} + +func OverwriteRole() RolesOption { + return func(c *RoleConfig) { + c.UpdateFn = func(s *Setup, old, new *roles.Role) (*roles.Role, bool) { return new, true } + } +} + +//func OverwriteRoleIfChanged() RolesOption { +// return func(c *RoleConfig) { +// c.UpdateFn = func(s *Setup, old, new *roles.Role) (*roles.Role, bool) { +// changed := old.Description != new.Description || old.AllowManagement != new.AllowManagement || +// !data.ElementsMatch(old.Environments, new.Environments) +// return new, changed +// } +// } +//} + +func KeepExistingRole() RolesOption { + return func(c *RoleConfig) { + c.UpdateFn = func(s *Setup, old, new *roles.Role) (*roles.Role, bool) { return old, false } + } +} + +func DeleteRole() RolesOption { + return func(c *RoleConfig) { + c.DeleteFn = func(s *Setup, role *roles.Role) bool { return true } + } +} + +func DeleteRoleIfRemove() RolesOption { + return func(c *RoleConfig) { + c.DeleteFn = func(s *Setup, role *roles.Role) bool { return s.IsRemove() } + } +} + +func UpdateExistingRole() RolesOption { + return func(c *RoleConfig) { + c.UpdateFn = func(s *Setup, exist, new *roles.Role) (*roles.Role, bool) { + + // если передан флаг force, то обновляем РІСЃРµ поля роли РЅР° переданные + if s.IsForce() { + return new, true + } + + if exist.Description == "" { + exist.Description = new.Description + } + + if len(exist.Environments) == 0 { + exist.Environments = new.Environments + } + + if !data.Contains(s.EnvironmentID, exist.Environments) { + exist.Environments = append(exist.Environments, s.EnvironmentID) + } + + exist.Rules = permission.MergeRules(exist.Rules, new.Rules) + + if !exist.AllowManagement { + exist.AllowManagement = new.AllowManagement + } + + return exist, true + } + } +} + +func (s *Setup) InstallRoles(ctx context.Context) error { + if len(s.Roles) == 0 { + return nil + } + + s.logger.Debug("Install role", zap.String("Space ID", s.SpaceID), zap.Int("Roles", len(s.Roles))) + + var errs []error + for _, c := range s.Roles { + if err := s.InstallRole(ctx, c); err != nil { + errs = append(errs, errors.WithDetailf(err, "Возникла ошибка РїСЂРё настройке роли %s(%s)", c.role.ID, c.role.Description)) + s.logger.Error("Failed to install role", zap.String("Role ID", c.role.ID), zap.Error(err)) + } + } + + if len(errs) > 0 { + return errors.WithErrors(ErrInstallRoles, errs...) + } + + return nil +} + +func (s *Setup) InstallRole(ctx context.Context, c RoleConfig) error { + role := c.role + role.SpaceID = s.SpaceID + + if !data.Contains(s.EnvironmentID, c.role.Environments) { + role.Environments = append(role.Environments, s.EnvironmentID) + } + + exist, err := s.content.Roles.Get(ctx, s.SpaceID, role.ID) + if err != nil { + if !strings.Contains(err.Error(), roles.ErrNotFound.Error()) { + return err + } + + _, err = s.content.Roles.Create(ctx, role) + return err + } + + if r, upd := c.UpdateFn(s, exist, role); upd { + return s.content.Roles.Update(ctx, r) + } + + return nil +} + +func (s *Setup) UninstallRoles(ctx context.Context) error { + if len(s.Roles) == 0 { + return nil + } + + s.logger.Debug("Uninstall role", zap.String("Space ID", s.SpaceID), zap.Int("Roles", len(s.Roles))) + + var errs []error + for _, c := range s.Roles { + if err := s.UninstallRole(ctx, c); err != nil { + errs = append(errs, errors.WithDetailf(err, "Возникла ошибка РїСЂРё удалении роли %s(%s)", c.role.ID, c.role.Description)) + s.logger.Error("Failed to uninstall role", zap.String("Role ID", c.role.ID), zap.Error(err)) + } + } + + if len(errs) > 0 { + return errors.WithErrors(ErrUninstallRoles, errs...) + } + + return nil +} + +func (s *Setup) UninstallRole(ctx context.Context, c RoleConfig) error { + if c.DeleteFn(s, c.role) { + err := s.content.Roles.Delete(ctx, s.SpaceID, c.role.ID) + if err != nil && !strings.Contains(err.Error(), roles.ErrNotFound.Error()) { + return err + } + } + return nil +} + +func (s *Setup) CheckRoles(ctx context.Context) error { + if len(s.Roles) == 0 { + return nil + } + + s.logger.Debug("Check role", zap.String("Space ID", s.SpaceID)) + + var errs []error + for _, c := range s.Roles { + if err := s.CheckRole(ctx, c.role); err != nil { + errs = append(errs, errors.WithDetailf(err, "РќРµ найдена роль %s(%s)", c.role.ID, c.role.Description)) + } + } + + if len(errs) > 0 { + return errors.WithErrors(ErrCheckRoles, errs...) + } + + return nil +} + +func (s *Setup) CheckRole(ctx context.Context, role *roles.Role) error { + _, err := s.content.Roles.Get(ctx, s.SpaceID, role.ID) + return err +} diff --git a/pkg/setup/setup.go b/pkg/setup/setup.go new file mode 100644 index 0000000000000000000000000000000000000000..8b7097a282d0ec73462ae8175a28f8013f4207f0 --- /dev/null +++ b/pkg/setup/setup.go @@ -0,0 +1,184 @@ +package setup + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/clients" + "git.perx.ru/perxis/perxis-go/pkg/collections" + "git.perx.ru/perxis/perxis-go/pkg/content" + "git.perx.ru/perxis/perxis-go/pkg/items" + "git.perx.ru/perxis/perxis-go/pkg/roles" + "go.uber.org/zap" +) + +// Setup реализует процесс настройки пространства. Указав необходимые требования Рє конфигурации пространства РјРѕР¶РЅРѕ +// выполнить процесс установки, проверки Рё удаления требований. +type Setup struct { + SpaceID string + EnvironmentID string + + Roles []RoleConfig + Clients []ClientConfig + Collections []CollectionConfig + Items []ItemConfig + + content *content.Content + + force bool + remove bool + + errors []error + logger *zap.Logger +} + +func NewSetup(content *content.Content, spaceID, environmentID string, logger *zap.Logger) *Setup { + //logger = logger.With(zap.String("Space", spaceID), zap.String("Environment", environmentID)) + + return &Setup{ + SpaceID: spaceID, + EnvironmentID: environmentID, + content: content, + logger: logger, + } +} + +func (s *Setup) WithForce(force bool) *Setup { + setup := *s + setup.force = force + return &setup +} + +func (s *Setup) IsForce() bool { + return s.force +} + +func (s *Setup) WithRemove(remove bool) *Setup { + setup := *s + setup.remove = remove + return &setup +} + +func (s *Setup) IsRemove() bool { + return s.remove +} + +func (s *Setup) HasErrors() bool { + return len(s.errors) > 0 +} + +func (s *Setup) AddError(err error) { + s.errors = append(s.errors, err) +} + +// AddRoles добавляет требования Рє настройке ролей РІ пространстве +func (s *Setup) AddRoles(roles []*roles.Role, opt ...RolesOption) *Setup { + for _, role := range roles { + s.AddRole(role, opt...) + } + return s +} + +func (s *Setup) AddRole(role *roles.Role, opt ...RolesOption) *Setup { + s.Roles = append(s.Roles, NewRoleConfig(role, opt...)) + return s +} + +// AddClients добавляет требования Рє настройке приложений РІ пространстве +func (s *Setup) AddClients(clients []*clients.Client, opt ...ClientsOption) *Setup { + for _, client := range clients { + s.AddClient(client, opt...) + } + return s +} + +func (s *Setup) AddClient(client *clients.Client, opt ...ClientsOption) *Setup { + s.Clients = append(s.Clients, NewClientConfig(client, opt...)) + return s +} + +// AddCollections добавляет требования Рє настройке коллекций РІ пространстве +func (s *Setup) AddCollections(collections []*collections.Collection, opt ...CollectionsOption) *Setup { + for _, col := range collections { + s.AddCollection(col, opt...) + } + return s +} + +func (s *Setup) AddCollection(collection *collections.Collection, opt ...CollectionsOption) *Setup { + s.Collections = append(s.Collections, NewCollectionConfig(collection, opt...)) + return s +} + +// AddItems добавляет требования Рє настройке элементов РІ пространстве +func (s *Setup) AddItems(items []*items.Item, opt ...ItemsOption) *Setup { + for _, item := range items { + s.AddItem(item, opt...) + } + return s +} + +func (s *Setup) AddItem(item *items.Item, opt ...ItemsOption) *Setup { + s.Items = append(s.Items, NewItemConfig(item, opt...)) + return s +} + +// Install выполняет установку необходимых требований +func (s *Setup) Install(ctx context.Context) error { + s.logger = s.logger.With(zap.String("Space", s.SpaceID), zap.String("Environment", s.EnvironmentID)) + + if err := s.InstallRoles(ctx); err != nil { + return err + } + if err := s.InstallClients(ctx); err != nil { + return err + } + if err := s.InstallCollections(ctx); err != nil { + return err + } + if err := s.InstallItems(ctx); err != nil { + return err + } + + return nil +} + +// Check выполняет проверку требований +func (s *Setup) Check(ctx context.Context) error { + s.logger = s.logger.With(zap.String("Space", s.SpaceID), zap.String("Environment", s.EnvironmentID)) + + if err := s.CheckRoles(ctx); err != nil { + return err + } + if err := s.CheckClients(ctx); err != nil { + return err + } + if err := s.CheckCollections(ctx); err != nil { + return err + } + if err := s.CheckItems(ctx); err != nil { + return err + } + + return nil +} + +// Uninstall выполняет удаление установленных раннее требований +func (s *Setup) Uninstall(ctx context.Context) error { + s.logger = s.logger.With(zap.String("Space", s.SpaceID), zap.String("Environment", s.EnvironmentID)) + + // Р’ случае если необходимо удалить данные удаляем РІСЃРµ что создано РїСЂРё установке расширения + if err := s.UninstallClients(ctx); err != nil { + return err + } + if err := s.UninstallRoles(ctx); err != nil { + return err + } + if err := s.UninstallCollections(ctx); err != nil { + return err + } + if err := s.UninstallItems(ctx); err != nil { + return err + } + + return nil +} diff --git a/pkg/setup/setup_test.go b/pkg/setup/setup_test.go new file mode 100644 index 0000000000000000000000000000000000000000..d3c3c363e9ec048b53704829413238b84528a24c --- /dev/null +++ b/pkg/setup/setup_test.go @@ -0,0 +1,1210 @@ +package setup + +import ( + "context" + "errors" + "testing" + + "git.perx.ru/perxis/perxis-go/pkg/clients" + clientsMock "git.perx.ru/perxis/perxis-go/pkg/clients/mocks" + "git.perx.ru/perxis/perxis-go/pkg/collections" + collectionMock "git.perx.ru/perxis/perxis-go/pkg/collections/mocks" + "git.perx.ru/perxis/perxis-go/pkg/content" + "git.perx.ru/perxis/perxis-go/pkg/data" + "git.perx.ru/perxis/perxis-go/pkg/environments" + environmentMock "git.perx.ru/perxis/perxis-go/pkg/environments/mocks" + "git.perx.ru/perxis/perxis-go/pkg/extension" + "git.perx.ru/perxis/perxis-go/pkg/items" + itemsMock "git.perx.ru/perxis/perxis-go/pkg/items/mocks" + "git.perx.ru/perxis/perxis-go/pkg/roles" + rolesMock "git.perx.ru/perxis/perxis-go/pkg/roles/mocks" + "git.perx.ru/perxis/perxis-go/pkg/schema" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" +) + +const ( + spaceID = "sp" + envID = "env" +) + +func getCollections() []*collections.Collection { + return []*collections.Collection{ + { + ID: "coll1", + SpaceID: spaceID, + EnvID: envID, + Schema: schema.New(), + Name: "Коллекция", + }, + } +} + +func getRoles() []*roles.Role { + return []*roles.Role{ + { + ID: "role", + SpaceID: spaceID, + }, + } +} + +func getClients() []*clients.Client { + return []*clients.Client{ + { + ID: "client", + SpaceID: spaceID, + RoleID: "role", + }, + } +} + +func getActions() []*items.Item { + return []*items.Item{ + { + ID: "act", + SpaceID: spaceID, + EnvID: envID, + CollectionID: extension.ActionsCollectionID, + Data: map[string]interface{}{ + "action": "act", + "name": "Action", + "extension": "ext", + }, + }, + } +} + +func newSetup(content *content.Content, t *testing.T) *Setup { + logger := zaptest.NewLogger(t, zaptest.WrapOptions()) + + setup := NewSetup(content, spaceID, envID, logger) + setup.AddCollections(getCollections()) + setup.AddRoles(getRoles()) + setup.AddClients(getClients()) + setup.AddItems(getActions(), OverwriteItem()) + + return setup +} + +func TestSetupInstall(t *testing.T) { + t.Run("Success, nothing to install", func(t *testing.T) { + logger := zaptest.NewLogger(t, zaptest.WrapOptions()) + + setup := NewSetup(nil, spaceID, envID, logger) + + err := setup.Install(context.Background()) + + require.NoError(t, err) + }) + + t.Run("Success, no force", func(t *testing.T) { + envMocks := &environmentMock.Environments{} + envMocks.On("Migrate", mock.Anything, spaceID, envID, &environments.MigrateOptions{Wait: true}). + Return(nil). + Once() + + collsMock := &collectionMock.Collections{} + for _, collection := range getCollections() { + collsMock.On("Get", mock.Anything, spaceID, envID, collection.ID). + Return(nil, collections.ErrNotFound). + Once() + + collsMock.On("Create", mock.Anything, collection). + Return(collection, nil). + Once() + + collsMock.On("SetSchema", mock.Anything, spaceID, envID, collection.ID, collection.Schema). + Return(nil). + Once() + } + + rMock := &rolesMock.Roles{} + for _, role := range getRoles() { + rMock.On("Get", mock.Anything, spaceID, role.ID). + Return(nil, roles.ErrNotFound). + Once() + + rMock.On("Create", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + create := args[1].(*roles.Role) + require.True(t, data.Contains(envID, create.Environments)) + }).Return(role, nil).Once() + } + + clMock := &clientsMock.Clients{} + for _, client := range getClients() { + clMock.On("Get", mock.Anything, spaceID, client.ID). + Return(nil, clients.ErrNotFound). + Once() + + clMock.On("Create", mock.Anything, client). + Return(client, nil). + Once() + } + + itmMock := &itemsMock.Items{} + for _, act := range getActions() { + itmMock.On("Get", mock.Anything, spaceID, envID, extension.ActionsCollectionID, act.ID). + Return(nil, items.ErrNotFound). + Once() + + itmMock.On("Create", mock.Anything, act). + Return(act, nil). + Once() + itmMock.On("Publish", mock.Anything, act). + Return(nil). + Once() + } + + setup := newSetup(&content.Content{ + Collections: collsMock, + Clients: clMock, + Roles: rMock, + Items: itmMock, + Environments: envMocks, + }, t) + + err := setup.Install(context.Background()) + + require.NoError(t, err) + require.False(t, setup.HasErrors()) + + rMock.AssertExpectations(t) + collsMock.AssertExpectations(t) + clMock.AssertExpectations(t) + itmMock.AssertExpectations(t) + envMocks.AssertExpectations(t) + }) + + t.Run("Success, update existing records", func(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() + } + + rMock := &rolesMock.Roles{} + for _, role := range getRoles() { + rMock.On("Get", mock.Anything, spaceID, role.ID). + Return(role, nil). + Once() + + rMock.On("Update", mock.Anything, role). + Return(nil). + Once() + } + + clMock := &clientsMock.Clients{} + for _, client := range getClients() { + clMock.On("Get", mock.Anything, spaceID, client.ID). + Return(client, nil). + Once() + + clMock.On("Update", mock.Anything, client). + Return(nil). + Once() + } + + itmMock := &itemsMock.Items{} + for _, act := range getActions() { + itmMock.On("Get", mock.Anything, spaceID, envID, extension.ActionsCollectionID, act.ID). + Return(act, nil). + Once() + itmMock.On("Update", mock.Anything, mock.Anything). + Return(nil). + Once() + itmMock.On("Publish", mock.Anything, mock.Anything). + Return(nil). + Once() + } + + setup := newSetup(&content.Content{ + Collections: collsMock, + Clients: clMock, + Roles: rMock, + Items: itmMock, + Environments: envMocks, + }, t) + + err := setup.Install(context.Background()) + + require.NoError(t, err) + require.False(t, setup.HasErrors()) + + rMock.AssertExpectations(t) + collsMock.AssertExpectations(t) + clMock.AssertExpectations(t) + itmMock.AssertExpectations(t) + envMocks.AssertExpectations(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() + } + + rMock := &rolesMock.Roles{} + for _, role := range getRoles() { + rMock.On("Get", mock.Anything, spaceID, role.ID).Return(role, nil).Once() + rMock.On("Update", mock.Anything, &roles.Role{ID: "role", SpaceID: "sp", Environments: []string{"env"}}).Return(nil).Once() + } + + clMock := &clientsMock.Clients{} + for _, client := range getClients() { + clMock.On("Delete", mock.Anything, spaceID, client.ID).Return(nil).Once() + + clMock.On("Create", mock.Anything, client). + Return(client, nil). + Once() + } + + itmMock := &itemsMock.Items{} + for _, act := range getActions() { + itmMock.On("Get", mock.Anything, spaceID, envID, extension.ActionsCollectionID, act.ID). + Return(nil, items.ErrNotFound). + Once() + + itmMock.On("Create", mock.Anything, act). + Return(act, nil). + Once() + itmMock.On("Publish", mock.Anything, act). + Return(nil). + Once() + } + + setup := newSetup(&content.Content{ + Collections: collsMock, + Clients: clMock, + Roles: rMock, + Items: itmMock, + }, t) + setup = setup.WithForce(true) + + err := setup.Install(context.Background()) + + require.NoError(t, err) + require.False(t, setup.HasErrors()) + + rMock.AssertExpectations(t) + collsMock.AssertExpectations(t) + clMock.AssertExpectations(t) + itmMock.AssertExpectations(t) + }) + + t.Run("Can't install role, storage returns error", func(t *testing.T) { + rMock := &rolesMock.Roles{} + rMock.On("Get", mock.Anything, spaceID, mock.Anything). + Return(nil, errors.New("can't get role")). + Once() + + setup := newSetup(&content.Content{ + Roles: rMock, + }, t) + + err := setup.Install(context.Background()) + + require.Error(t, err) + assert.ErrorContains(t, err, "failed to install role") + + rMock.AssertExpectations(t) + }) + + t.Run("Can't install client, storage returns error", func(t *testing.T) { + rMock := &rolesMock.Roles{} + for _, role := range getRoles() { + rMock.On("Get", mock.Anything, spaceID, role.ID). + Return(nil, roles.ErrNotFound). + Once() + + rMock.On("Create", mock.Anything, mock.Anything). + Return(role, nil). + Once() + } + + clMock := &clientsMock.Clients{} + for _, client := range getClients() { + clMock.On("Get", mock.Anything, spaceID, client.ID). + Return(nil, errors.New("can't get client")). + Once() + } + + setup := newSetup(&content.Content{ + Clients: clMock, + Roles: rMock, + }, t) + + err := setup.Install(context.Background()) + + require.Error(t, err) + assert.ErrorContains(t, err, "failed to install clients") + + rMock.AssertExpectations(t) + clMock.AssertExpectations(t) + }) + + t.Run("Can't get 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(nil, errors.New("can't get collection")). + Once() + } + + rMock := &rolesMock.Roles{} + for _, role := range getRoles() { + rMock.On("Get", mock.Anything, spaceID, role.ID). + Return(nil, roles.ErrNotFound). + Once() + + rMock.On("Create", mock.Anything, mock.Anything). + Return(role, nil). + Once() + } + + clMock := &clientsMock.Clients{} + for _, client := range getClients() { + clMock.On("Get", mock.Anything, spaceID, client.ID). + Return(nil, clients.ErrNotFound). + Once() + + clMock.On("Create", mock.Anything, client). + Return(client, nil). + Once() + } + + setup := newSetup(&content.Content{ + Collections: collsMock, + Clients: clMock, + Roles: rMock, + }, t) + + err := setup.Install(context.Background()) + + require.Error(t, err) + assert.ErrorContains(t, err, "failed to install collections") + + rMock.AssertExpectations(t) + collsMock.AssertExpectations(t) + clMock.AssertExpectations(t) + }) + + t.Run("Can't create 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(nil, collections.ErrNotFound). + Once() + + collsMock.On("Create", mock.Anything, collection). + Return(nil, errors.New("can't create collection")). + Once() + } + + rMock := &rolesMock.Roles{} + for _, role := range getRoles() { + rMock.On("Get", mock.Anything, spaceID, role.ID). + Return(nil, roles.ErrNotFound). + Once() + + rMock.On("Create", mock.Anything, mock.Anything). + Return(role, nil). + Once() + } + + clMock := &clientsMock.Clients{} + for _, client := range getClients() { + clMock.On("Get", mock.Anything, spaceID, client.ID). + Return(nil, clients.ErrNotFound). + Once() + + clMock.On("Create", mock.Anything, client). + Return(client, nil). + Once() + } + + setup := newSetup(&content.Content{ + Collections: collsMock, + Clients: clMock, + Roles: rMock, + }, t) + + err := setup.Install(context.Background()) + + require.Error(t, err) + assert.ErrorContains(t, err, "failed to install collections") + + rMock.AssertExpectations(t) + collsMock.AssertExpectations(t) + clMock.AssertExpectations(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() + } + + rMock := &rolesMock.Roles{} + for _, role := range getRoles() { + rMock.On("Get", mock.Anything, spaceID, role.ID). + Return(nil, roles.ErrNotFound). + Once() + + rMock.On("Create", mock.Anything, mock.Anything). + Return(role, nil). + Once() + } + + clMock := &clientsMock.Clients{} + for _, client := range getClients() { + clMock.On("Get", mock.Anything, spaceID, client.ID). + Return(nil, clients.ErrNotFound). + Once() + + clMock.On("Create", mock.Anything, client). + Return(client, nil). + Once() + } + + setup := newSetup(&content.Content{ + Collections: collsMock, + Clients: clMock, + Roles: rMock, + }, t) + + err := setup.Install(context.Background()) + + require.Error(t, err) + assert.ErrorContains(t, err, "failed to install collections") + + rMock.AssertExpectations(t) + collsMock.AssertExpectations(t) + clMock.AssertExpectations(t) + }) + + t.Run("Can't set schema, storage returns error", func(t *testing.T) { + collsMock := &collectionMock.Collections{} + for _, collection := range getCollections() { + collsMock.On("Get", mock.Anything, spaceID, envID, collection.ID). + Return(nil, collections.ErrNotFound). + Once() + + collsMock.On("Create", mock.Anything, collection). + Return(collection, nil). + Once() + + collsMock.On("SetSchema", mock.Anything, spaceID, envID, collection.ID, collection.Schema). + Return(errors.New("can't set schema")). + Once() + } + + rMock := &rolesMock.Roles{} + for _, role := range getRoles() { + rMock.On("Get", mock.Anything, spaceID, role.ID). + Return(nil, roles.ErrNotFound). + Once() + + rMock.On("Create", mock.Anything, mock.Anything). + Return(role, nil). + Once() + } + + clMock := &clientsMock.Clients{} + for _, client := range getClients() { + clMock.On("Get", mock.Anything, spaceID, client.ID). + Return(nil, clients.ErrNotFound). + Once() + + clMock.On("Create", mock.Anything, client). + Return(client, nil). + Once() + } + + setup := newSetup(&content.Content{ + Collections: collsMock, + Clients: clMock, + Roles: rMock, + }, t) + + err := setup.Install(context.Background()) + + require.Error(t, err) + assert.ErrorContains(t, err, "failed to install collections") + + rMock.AssertExpectations(t) + collsMock.AssertExpectations(t) + clMock.AssertExpectations(t) + }) + + t.Run("Can't migrate, storage returns error", func(t *testing.T) { + envMocks := &environmentMock.Environments{} + envMocks.On("Migrate", mock.Anything, spaceID, envID, &environments.MigrateOptions{Wait: true}). + Return(errors.New("can't migrate")). + Once() + + collsMock := &collectionMock.Collections{} + for _, collection := range getCollections() { + collsMock.On("Get", mock.Anything, spaceID, envID, collection.ID). + Return(nil, collections.ErrNotFound). + Once() + + collsMock.On("Create", mock.Anything, collection). + Return(collection, nil). + Once() + + collsMock.On("SetSchema", mock.Anything, spaceID, envID, collection.ID, collection.Schema). + Return(nil). + Once() + } + + rMock := &rolesMock.Roles{} + for _, role := range getRoles() { + rMock.On("Get", mock.Anything, spaceID, role.ID). + Return(nil, roles.ErrNotFound). + Once() + + rMock.On("Create", mock.Anything, mock.Anything). + Return(role, nil). + Once() + } + + clMock := &clientsMock.Clients{} + for _, client := range getClients() { + clMock.On("Get", mock.Anything, spaceID, client.ID). + Return(nil, clients.ErrNotFound). + Once() + + clMock.On("Create", mock.Anything, client). + Return(client, nil). + Once() + } + + setup := newSetup(&content.Content{ + Collections: collsMock, + Clients: clMock, + Roles: rMock, + Environments: envMocks, + }, t) + + err := setup.Install(context.Background()) + + require.Error(t, err) + assert.ErrorContains(t, err, "failed to install collections") + + rMock.AssertExpectations(t) + collsMock.AssertExpectations(t) + clMock.AssertExpectations(t) + }) + + t.Run("Can't find action, storage returns error", func(t *testing.T) { + envMocks := &environmentMock.Environments{} + envMocks.On("Migrate", mock.Anything, spaceID, envID, &environments.MigrateOptions{Wait: true}). + Return(nil). + Once() + + collsMock := &collectionMock.Collections{} + for _, collection := range getCollections() { + collsMock.On("Get", mock.Anything, spaceID, envID, collection.ID). + Return(nil, collections.ErrNotFound). + Once() + + collsMock.On("Create", mock.Anything, collection). + Return(collection, nil). + Once() + + collsMock.On("SetSchema", mock.Anything, spaceID, envID, collection.ID, collection.Schema). + Return(nil). + Once() + } + + rMock := &rolesMock.Roles{} + for _, role := range getRoles() { + rMock.On("Get", mock.Anything, spaceID, role.ID).Return(role, nil).Once() + rMock.On("Update", mock.Anything, &roles.Role{ID: "role", SpaceID: "sp", Environments: []string{"env"}}).Return(nil).Once() + } + + clMock := &clientsMock.Clients{} + for _, client := range getClients() { + clMock.On("Delete", mock.Anything, spaceID, client.ID).Return(nil).Once() + + clMock.On("Create", mock.Anything, client). + Return(client, nil). + Once() + } + + itmMock := &itemsMock.Items{} + itmMock.On( + "Get", mock.Anything, spaceID, envID, extension.ActionsCollectionID, mock.Anything, + ). + Return(nil, items.ErrNotFound). + Once() + + itmMock.On("Create", mock.Anything, mock.Anything). + Return(nil, errors.New("can't create item")). + Once() + + setup := newSetup(&content.Content{ + Collections: collsMock, + Clients: clMock, + Roles: rMock, + Items: itmMock, + Environments: envMocks, + }, t) + setup = setup.WithForce(true) + + err := setup.Install(context.Background()) + + require.Error(t, err) + assert.ErrorContains(t, err, "failed to install items") + + rMock.AssertExpectations(t) + collsMock.AssertExpectations(t) + clMock.AssertExpectations(t) + itmMock.AssertExpectations(t) + envMocks.AssertExpectations(t) + }) + + //t.Run("Can't find task configs, storage returns error", func(t *testing.T) { + // envMocks := &environmentMock.Environments{} + // envMocks.On("Migrate", mock.Anything, spaceID, envID). + // Return(nil). + // Once() + // + // collsMock := &collectionMock.Collections{} + // for _, collection := range getCollections() { + // collsMock.On("Get", mock.Anything, spaceID, envID, collection.ID). + // Return(nil, collections.ErrNotFound). + // Once() + // + // collsMock.On("Create", mock.Anything, collection). + // Return(collection, nil). + // Once() + // + // collsMock.On("SetSchema", mock.Anything, spaceID, envID, collection.ID, collection.Schema). + // Return(nil). + // Once() + // } + // + // rMock := &rolesMock.Roles{} + // for _, role := range getRoles() { + // rMock.On("Get", mock.Anything, spaceID, role.ID). + // Return(nil, roles.ErrNotFound). + // Once() + // + // rMock.On("Create", mock.Anything, mock.Anything). + // Return(role, nil). + // Once() + // } + // + // clMock := &clientsMock.Clients{} + // for _, client := range getClients() { + // clMock.On("Get", mock.Anything, spaceID, client.ID). + // Return(nil, clients.ErrNotFound). + // Once() + // + // clMock.On("Create", mock.Anything, client). + // Return(client, nil). + // Once() + // } + // + // itmMock := &itemsMock.Items{} + // for _, act := range getActions() { + // itmMock.On("Get", mock.Anything, spaceID, envID, extension.ActionsCollectionID, act.ID). + // Return(nil, items.ErrNotFound). + // Once() + // + // itmMock.On("Create", mock.Anything, act). + // Return(act, nil). + // Once() + // itmMock.On("Publish", mock.Anything, act). + // Return(nil). + // Once() + // } + // + // setup := newSetup(&content.Content{ + // Collections: collsMock, + // Clients: clMock, + // Roles: rMock, + // Items: itmMock, + // Environments: envMocks, + // }, t) + // + // err := setup.Install(context.Background()) + // + // require.Error(t, err) + // assert.ErrorContains(t, err, "failed to install task configs") + // + // rMock.AssertExpectations(t) + // collsMock.AssertExpectations(t) + // clMock.AssertExpectations(t) + // itmMock.AssertExpectations(t) + // envMocks.AssertExpectations(t) + //}) +} + +func TestSetupUninstall(t *testing.T) { + t.Run("Success, nothing to uninstall", func(t *testing.T) { + logger := zaptest.NewLogger(t, zaptest.WrapOptions()) + + setup := NewSetup(nil, spaceID, envID, logger) + + err := setup.Uninstall(context.Background()) + + require.NoError(t, err) + }) + + t.Run("Remove", func(t *testing.T) { + collsMock := &collectionMock.Collections{} + for _, collection := range getCollections() { + collsMock.On("Delete", mock.Anything, spaceID, envID, collection.ID). + Return(nil). + Once() + } + + rMock := &rolesMock.Roles{} + for _, role := range getRoles() { + rMock.On("Delete", mock.Anything, spaceID, role.ID). + Return(nil). + Once() + } + + clMock := &clientsMock.Clients{} + for _, client := range getClients() { + clMock.On("Delete", mock.Anything, spaceID, client.ID).Return(nil).Once() + } + + itmMock := &itemsMock.Items{} + for _, act := range getActions() { + itmMock.On("Delete", mock.Anything, spaceID, envID, extension.ActionsCollectionID, act.ID). + Return(nil). + Once() + } + + setup := newSetup(&content.Content{ + Collections: collsMock, + Clients: clMock, + Roles: rMock, + Items: itmMock, + }, t) + + setup = setup.WithRemove(true) + err := setup.Uninstall(context.Background()) + + require.NoError(t, err) + require.False(t, setup.HasErrors()) + + rMock.AssertExpectations(t) + collsMock.AssertExpectations(t) + clMock.AssertExpectations(t) + itmMock.AssertExpectations(t) + }) + + t.Run("Can't uninstall clients, storage returns error", func(t *testing.T) { + rMock := &rolesMock.Roles{} + + clMock := &clientsMock.Clients{} + clMock.On("Delete", mock.Anything, spaceID, mock.Anything). + Return(errors.New("can't delete client")). + Once() + + itmMock := &itemsMock.Items{} + for _, act := range getActions() { + itmMock.On("Delete", mock.Anything, spaceID, envID, extension.ActionsCollectionID, act.ID). + Return(nil). + Once() + } + + setup := newSetup(&content.Content{ + Clients: clMock, + Roles: rMock, + Items: itmMock, + }, t) + + setup = setup.WithRemove(true) + err := setup.Uninstall(context.Background()) + + require.Error(t, err) + assert.ErrorContains(t, err, "failed to uninstall clients") + + rMock.AssertExpectations(t) + clMock.AssertExpectations(t) + }) + + t.Run("Can't uninstall role, storage returns error", func(t *testing.T) { + rMock := &rolesMock.Roles{} + rMock.On("Delete", mock.Anything, spaceID, mock.Anything). + Return(errors.New("can't delete role")). + Once() + + clMock := &clientsMock.Clients{} + for _, client := range getClients() { + clMock.On("Delete", mock.Anything, spaceID, client.ID).Return(nil).Once() + } + + itmMock := &itemsMock.Items{} + for _, act := range getActions() { + itmMock.On("Delete", mock.Anything, spaceID, envID, extension.ActionsCollectionID, act.ID). + Return(nil). + Once() + } + + setup := newSetup(&content.Content{ + Roles: rMock, + Clients: clMock, + Items: itmMock, + }, t) + + setup = setup.WithRemove(true) + err := setup.Uninstall(context.Background()) + + require.Error(t, err) + assert.ErrorContains(t, err, "failed to uninstall role") + + rMock.AssertExpectations(t) + }) + + t.Run("Can't uninstall collections, storage returns error", func(t *testing.T) { + collsMock := &collectionMock.Collections{} + collsMock.On("Delete", mock.Anything, spaceID, envID, mock.Anything). + Return(errors.New("can't delete collection")). + Once() + + rMock := &rolesMock.Roles{} + for _, role := range getRoles() { + rMock.On("Delete", mock.Anything, spaceID, role.ID). + Return(nil). + Once() + } + + clMock := &clientsMock.Clients{} + for _, client := range getClients() { + clMock.On("Delete", mock.Anything, spaceID, client.ID).Return(nil).Once() + } + + itmMock := &itemsMock.Items{} + for _, act := range getActions() { + itmMock.On("Delete", mock.Anything, spaceID, envID, extension.ActionsCollectionID, act.ID). + Return(nil). + Once() + } + + setup := newSetup(&content.Content{ + Collections: collsMock, + Clients: clMock, + Roles: rMock, + Items: itmMock, + }, t) + + setup = setup.WithRemove(true) + err := setup.Uninstall(context.Background()) + + require.Error(t, err) + assert.ErrorContains(t, err, "failed to uninstall collections") + + rMock.AssertExpectations(t) + collsMock.AssertExpectations(t) + clMock.AssertExpectations(t) + }) + + t.Run("Can't uninstall actions, storage returns error", func(t *testing.T) { + collsMock := &collectionMock.Collections{} + collsMock.On("Delete", mock.Anything, spaceID, envID, mock.Anything). + Return(nil). + Once() + + rMock := &rolesMock.Roles{} + for _, role := range getRoles() { + rMock.On("Delete", mock.Anything, spaceID, role.ID). + Return(nil). + Once() + } + + clMock := &clientsMock.Clients{} + for _, client := range getClients() { + clMock.On("Delete", mock.Anything, spaceID, client.ID).Return(nil).Once() + } + + itmMock := &itemsMock.Items{} + for _, act := range getActions() { + itmMock.On("Delete", mock.Anything, spaceID, envID, extension.ActionsCollectionID, act.ID). + Return(errors.New("can't delete item")). + Once() + } + + setup := newSetup(&content.Content{ + Collections: collsMock, + Clients: clMock, + Roles: rMock, + Items: itmMock, + }, t) + + setup = setup.WithRemove(true) + err := setup.Uninstall(context.Background()) + + require.Error(t, err) + assert.ErrorContains(t, err, "failed to uninstall items") + + itmMock.AssertExpectations(t) + }) +} + +func TestSetupCheck(t *testing.T) { + t.Run("Success, nothing to check", func(t *testing.T) { + logger := zaptest.NewLogger(t, zaptest.WrapOptions()) + + setup := NewSetup(nil, spaceID, envID, logger) + + err := setup.Check(context.Background()) + + require.NoError(t, err) + }) + + t.Run("Success", func(t *testing.T) { + collsMock := &collectionMock.Collections{} + for _, collection := range getCollections() { + collsMock.On("Get", mock.Anything, spaceID, envID, collection.ID). + Return(collection, nil). + Once() + } + + rMock := &rolesMock.Roles{} + for _, role := range getRoles() { + rMock.On("Get", mock.Anything, spaceID, role.ID). + Return(role, nil). + Once() + } + + clMock := &clientsMock.Clients{} + for _, client := range getClients() { + clMock.On("Get", mock.Anything, spaceID, client.ID). + Return(client, nil). + Once() + } + + itmMock := &itemsMock.Items{} + for _, act := range getActions() { + itmMock.On("Get", mock.Anything, spaceID, envID, extension.ActionsCollectionID, act.ID). + Return(act, nil). + Once() + } + + setup := newSetup(&content.Content{ + Collections: collsMock, + Clients: clMock, + Roles: rMock, + Items: itmMock, + }, t) + + err := setup.Check(context.Background()) + + require.NoError(t, err) + require.False(t, setup.HasErrors()) + + rMock.AssertExpectations(t) + collsMock.AssertExpectations(t) + clMock.AssertExpectations(t) + itmMock.AssertExpectations(t) + }) + + t.Run("Can't get role, storage returns error", func(t *testing.T) { + rMock := &rolesMock.Roles{} + rMock.On("Get", mock.Anything, spaceID, mock.Anything). + Return(nil, errors.New("can't get role")). + Once() + + setup := newSetup(&content.Content{ + Roles: rMock, + }, t) + + err := setup.Check(context.Background()) + + require.Error(t, err) + assert.ErrorContains(t, err, "role check error") + + rMock.AssertExpectations(t) + }) + + t.Run("Can't get client, storage returns error", func(t *testing.T) { + rMock := &rolesMock.Roles{} + for _, role := range getRoles() { + rMock.On("Get", mock.Anything, spaceID, role.ID). + Return(role, nil). + Once() + } + + clMock := &clientsMock.Clients{} + clMock.On("Get", mock.Anything, spaceID, mock.Anything). + Return(nil, errors.New("can't get client")). + Once() + + setup := newSetup(&content.Content{ + Roles: rMock, + Clients: clMock, + }, t) + + err := setup.Check(context.Background()) + + require.Error(t, err) + assert.ErrorContains(t, err, "clients check error") + + rMock.AssertExpectations(t) + clMock.AssertExpectations(t) + }) + + t.Run("Can't get collection, storage returns error", func(t *testing.T) { + collsMock := &collectionMock.Collections{} + collsMock.On("Get", mock.Anything, spaceID, envID, mock.Anything). + Return(nil, errors.New("can't get collection")). + Once() + + rMock := &rolesMock.Roles{} + for _, role := range getRoles() { + rMock.On("Get", mock.Anything, spaceID, role.ID). + Return(role, nil). + Once() + } + + clMock := &clientsMock.Clients{} + for _, client := range getClients() { + clMock.On("Get", mock.Anything, spaceID, client.ID). + Return(client, nil). + Once() + } + + setup := newSetup(&content.Content{ + Roles: rMock, + Clients: clMock, + Collections: collsMock, + }, t) + + err := setup.Check(context.Background()) + + require.Error(t, err) + assert.ErrorContains(t, err, "collections check error") + + rMock.AssertExpectations(t) + clMock.AssertExpectations(t) + collsMock.AssertExpectations(t) + }) + + t.Run("Can't get action, 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() + } + + rMock := &rolesMock.Roles{} + for _, role := range getRoles() { + rMock.On("Get", mock.Anything, spaceID, role.ID). + Return(role, nil). + Once() + } + + clMock := &clientsMock.Clients{} + for _, client := range getClients() { + clMock.On("Get", mock.Anything, spaceID, client.ID). + Return(client, nil). + Once() + } + + itmMock := &itemsMock.Items{} + itmMock.On( + "Get", mock.Anything, spaceID, envID, extension.ActionsCollectionID, mock.Anything, + ). + Return(nil, errors.New("can't find item")). + Once() + + setup := newSetup(&content.Content{ + Roles: rMock, + Clients: clMock, + Collections: collsMock, + Items: itmMock, + }, t) + + err := setup.Check(context.Background()) + + require.Error(t, err) + assert.ErrorContains(t, err, "items check error") + + rMock.AssertExpectations(t) + clMock.AssertExpectations(t) + collsMock.AssertExpectations(t) + }) + + //t.Run("Can't get task config, 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() + // } + // + // rMock := &rolesMock.Roles{} + // for _, role := range getRoles() { + // rMock.On("Get", mock.Anything, spaceID, role.ID). + // Return(role, nil). + // Once() + // } + // + // clMock := &clientsMock.Clients{} + // for _, client := range getClients() { + // clMock.On("Get", mock.Anything, spaceID, client.ID). + // Return(client, nil). + // Once() + // } + // + // itmMock := &itemsMock.Items{} + // for _, act := range getActions() { + // itmMock.On("Get", mock.Anything, spaceID, envID, extension.ActionsCollectionID, act.ID). + // Return(act, nil). + // Once() + // } + // + // itmMock.On( + // "Find", mock.Anything, spaceID, envID, tasks.TaskConfigCollection, + // mock.Anything, + // ). + // Return(nil, 0, errors.New("can't get task configs")). + // Once() + // + // setup := newSetup(&content.Content{ + // Roles: rMock, + // Clients: clMock, + // Collections: collsMock, + // Items: itmMock, + // }, t) + // + // err := setup.Check(context.Background()) + // + // require.Error(t, err) + // assert.ErrorContains(t, err, "task configs check error") + // + // rMock.AssertExpectations(t) + // clMock.AssertExpectations(t) + // collsMock.AssertExpectations(t) + //}) +}