diff --git a/go.mod b/go.mod index 0efbbd17a96ea8fb3071e5b27820d68e4f2d80d3..64610c41c917cc60ebec0ceed850595620e3859c 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.18 require ( github.com/antonmedv/expr v1.9.0 + github.com/avast/retry-go v3.0.0+incompatible github.com/go-kit/kit v0.12.0 github.com/golang/protobuf v1.5.2 github.com/gosimple/slug v1.13.1 diff --git a/go.sum b/go.sum index e83ee408a6b4fea9e636d7028d7d8f0bf69f82f2..c23afb4718c63e0424aaa4805c160dd724735e56 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,8 @@ cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2Aawl github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/antonmedv/expr v1.9.0 h1:j4HI3NHEdgDnN9p6oI6Ndr0G5QryMY0FNxT4ONrFDGU= github.com/antonmedv/expr v1.9.0/go.mod h1:5qsM3oLGDND7sDmQGDXHkYfkjYMUX14qsgqmHhwGEk8= +github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= +github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= diff --git a/pkg/environments/transport/grpc/protobuf_type_converters.microgen.go b/pkg/environments/transport/grpc/protobuf_type_converters.microgen.go index 5402c513bc7ecbf3a7dc3b4a291282eed03f554e..cb9ee820f529f6ae1389415735dccbcdcd1bfee5 100644 --- a/pkg/environments/transport/grpc/protobuf_type_converters.microgen.go +++ b/pkg/environments/transport/grpc/protobuf_type_converters.microgen.go @@ -5,8 +5,8 @@ package transportgrpc import ( - pb "git.perx.ru/perxis/perxis-go/proto/environments" service "git.perx.ru/perxis/perxis-go/pkg/environments" + pb "git.perx.ru/perxis/perxis-go/proto/environments" "github.com/golang/protobuf/ptypes" ) diff --git a/pkg/environments/transport/server.microgen.go b/pkg/environments/transport/server.microgen.go index 3a0b20a55cdf3aff82beb803efb59c5833e5afd7..d1b8264ad2a38989b549b3b30a8cd58e7c860554 100644 --- a/pkg/environments/transport/server.microgen.go +++ b/pkg/environments/transport/server.microgen.go @@ -3,8 +3,7 @@ package transport import ( - -"context" + "context" "git.perx.ru/perxis/perxis-go/pkg/environments" endpoint "github.com/go-kit/kit/endpoint" diff --git a/pkg/extension/action.go b/pkg/extension/action.go new file mode 100644 index 0000000000000000000000000000000000000000..3f090dffc541dcdee9f3dba7b0b8e6adc30931df --- /dev/null +++ b/pkg/extension/action.go @@ -0,0 +1,213 @@ +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" + ActionsCollectionName = "Настройки/Действия" +) + +type ( + ActionKind = pb.Action_Kind + ResponseState = pb.ActionResponse_State + + ActionRequest = pb.ActionRequest + ActionResponse = pb.ActionResponse + + //ActionRequest struct { + // Extension string + // Action string + // SpaceID string + // EnvID string + // CollectionID string + // ItemID string + // ItemIDs []string + // Fields []string + // Metadata map[string]string + // Refs []*references.Reference + // Params *references.Reference + //} + // + //ActionResponse struct { + // State ResponseState + // Msg string + // Error string + // Next []*Action + // Metadata map[string]string + // Refs []*references.Reference + //} +) + +const ( + ResponseDone = pb.ActionResponse_DONE + ResponseError = pb.ActionResponse_ERROR + ResponsePending = pb.ActionResponse_PENDING + ResponseInProgress = pb.ActionResponse_IN_PROGRESS + ResponseParametersRequired = pb.ActionResponse_PARAMETERS_REQUIRED + + ActionKindSpace = pb.Action_SPACE + ActionKindEnvironment = pb.Action_ENVIRONMENT + ActionKindCollection = pb.Action_COLLECTION + ActionKindItem = pb.Action_ITEM + ActionKindItems = pb.Action_ITEMS + ActionKindRevision = pb.Action_REVISION + ActionKindCreate = pb.Action_CREATE +) + +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 ActionRequestToPB(req *ActionRequest) *pb.ActionRequest { +// if req == nil { +// return nil +// } +// return &pb.ActionRequest{ +// Extension: req.Extension, +// Action: req.Action, +// SpaceId: req.SpaceID, +// EnvId: req.EnvID, +// CollectionId: req.CollectionID, +// ItemId: req.ItemID, +// ItemIds: req.ItemIDs, +// Fields: req.Fields, +// Metadata: req.Metadata, +// Refs: references.ReferenceListToPB(req.Refs), +// Params: references.ReferenceToPB(req.Params), +// } +//} +// +//func ActionRequestFromPB(req *pb.ActionRequest) *ActionRequest { +// if req == nil { +// return nil +// } +// return &ActionRequest{ +// Extension: req.Extension, +// Action: req.Action, +// SpaceID: req.SpaceId, +// EnvID: req.EnvId, +// CollectionID: req.CollectionId, +// ItemID: req.ItemId, +// ItemIDs: req.ItemIds, +// Fields: req.Fields, +// Metadata: req.Metadata, +// Refs: references.ReferenceListFromPB(req.Refs), +// Params: references.ReferenceFromPB(req.Params), +// } +//} +//func ActionResponseToPB(out *ActionResponse) *pb.ActionResponse { +// if out == nil { +// return nil +// } +// +// var next []*pb.Action +// for _, a := range out.Next { +// next = append(next, ActionToPB(a)) +// } +// +// return &pb.ActionResponse{ +// State: out.State, +// Msg: out.Msg, +// Error: out.Error, +// Next: next, +// Metadata: out.Metadata, +// Refs: references.ReferenceListToPB(out.Refs), +// } +//} +// +//func ActionResponseFromPB(out *pb.ActionResponse) *ActionResponse { +// if out == nil { +// return nil +// } +// +// var next []*Action +// for _, a := range out.Next { +// next = append(next, ActionFromPB(a)) +// } +// +// return &ActionResponse{ +// State: out.State, +// Msg: out.Msg, +// Error: out.Error, +// Next: next, +// Metadata: out.Metadata, +// Refs: references.ReferenceListFromPB(out.Refs), +// } +//} + +func ActionFromPB(a *pb.Action) *Action { + if a == nil { + return nil + } + 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, + //Request: ActionRequestFromPB(a.Request), + NavigationAction: a.NavigationAction, + NavigationRoute: a.NavigationRoute, + } +} + +// +//func ActionToPB(a *Action) *pb.Action { +// if a == nil { +// return nil +// } +// return &pb.Action{ +// Extension: a.Extension, +// Action: a.Action, +// Name: a.Name, +// Description: a.Description, +// Icon: a.Icon, +// Image: references.ReferenceToPB(a.Image), +// Groups: a.Groups, +// Kind: a.Kind, +// Classes: a.Classes, +// Refs: references.ReferenceListToPB(a.Refs), +// ParamsCollection: a.ParamsCollection, +// Request: a.Request, +// //Request: ActionRequestToPB(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..fc7276552f11ff086934e8d524541c77249d6d22 --- /dev/null +++ b/pkg/extension/client.go @@ -0,0 +1,82 @@ +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 r.UpdateAvailable { + errs = append(errs, ExtensionError(ErrUpdateAvailable, 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 + //return ActionResponseFromPB(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..26d9b6d51171307161e68ceef0830857fbb569a8 --- /dev/null +++ b/pkg/extension/extension.go @@ -0,0 +1,105 @@ +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 + + ExtensionsCollectionID = "space_extensions" + ExtensionsCollectionName = "Настройки/Расширения" + + 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 + + ExtensionDescriptor = pb.ExtensionDescriptor + SpaceExtensionsState = pb.SpaceExtensions_State +) + +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/manager.go b/pkg/extension/manager.go new file mode 100644 index 0000000000000000000000000000000000000000..958ac1788315fdaf4f8f4776099888a5af7b1b1d --- /dev/null +++ b/pkg/extension/manager.go @@ -0,0 +1,81 @@ +package extension + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/auth" + "git.perx.ru/perxis/perxis-go/pkg/errors" + pb "git.perx.ru/perxis/perxis-go/proto/extensions" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +type Manager interface { + Extension + RegisterExtensions(ctx context.Context, ext ...*ExtensionConnector) error + UnregisterExtensions(ctx context.Context, ext ...*ExtensionConnector) error + ListExtensions(ctx context.Context, filter *ListExtensionsFilter) ([]*ExtensionConnector, error) +} + +type ( + ListExtensionsFilter = pb.ListExtensionsFilter +) + +type ExtensionConnector struct { + Extension + Connection *grpc.ClientConn + Descriptor *ExtensionDescriptor +} + +func (e *ExtensionConnector) Connect() error { + // РЈР¶Рµ есть экземпляр расширение или клиент расширения + if e.Extension != nil { + return nil + } + + // РЈР¶Рµ есть соединение, создаем клиента расширения + if e.Connection != nil { + if e.Extension == nil { + e.Extension = NewClient(e.Connection) + return nil + } + return nil + } + + // Нет дескриптора или Url для соединения СЃ расширением + if e.Descriptor == nil || e.Descriptor.Url == "" { + return nil + } + + conn, err := grpc.Dial( + e.Descriptor.Url, + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithChainUnaryInterceptor( + auth.PrincipalClientInterceptor(), + ), + ) + if err != nil { + return err + } + + e.Connection = conn + e.Extension = NewClient(conn) + return nil +} + +func (e *ExtensionConnector) IsConnected() bool { + if e != nil && e.Connection != nil { + return true + } + return false +} + +func (e *ExtensionConnector) Close() error { + if e.IsConnected() { + if err := e.Connection.Close(); err != nil { + return errors.Wrapf(err, "fail to close connection, extension %s (%s)", e.Extension, e.Descriptor.Url) + } + e.Connection = nil + } + return nil +} diff --git a/pkg/extension/manager_client.go b/pkg/extension/manager_client.go new file mode 100644 index 0000000000000000000000000000000000000000..99057c3e75f22cb623d7aeff83ac0f9016d9383e --- /dev/null +++ b/pkg/extension/manager_client.go @@ -0,0 +1,72 @@ +package extension + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/auth" + pb "git.perx.ru/perxis/perxis-go/proto/extensions" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +type ManagerClient struct { + *Client + manager pb.ExtensionManagerClient + conn *grpc.ClientConn +} + +func NewManagerClientWithAddr(addr string) (*ManagerClient, error) { + cc, err := grpc.Dial(addr, + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithUnaryInterceptor(auth.PrincipalClientInterceptor()), + ) + if err != nil { + return nil, err + } + + return NewManagerClient(cc), nil +} + +func NewManagerClient(cc *grpc.ClientConn) *ManagerClient { + return &ManagerClient{ + manager: pb.NewExtensionManagerClient(cc), + conn: cc, + Client: NewClient(cc), + } +} + +func (c *ManagerClient) Close() error { + return c.conn.Close() +} + +func (c *ManagerClient) RegisterExtensions(ctx context.Context, exts ...*ExtensionConnector) error { + var descs []*pb.ExtensionDescriptor + for _, e := range exts { + descs = append(descs, e.Descriptor) + } + _, err := c.manager.RegisterExtensions(ctx, &pb.RegisterExtensionsRequest{Extensions: descs}, grpc.WaitForReady(true)) + return err +} + +func (c *ManagerClient) UnregisterExtensions(ctx context.Context, exts ...*ExtensionConnector) error { + var descs []*pb.ExtensionDescriptor + for _, e := range exts { + descs = append(descs, e.Descriptor) + } + _, err := c.manager.UnregisterExtensions(ctx, &pb.UnregisterExtensionsRequest{Extensions: descs}, grpc.WaitForReady(true)) + return err +} + +func (c *ManagerClient) ListExtensions(ctx context.Context, filter *ListExtensionsFilter) ([]*ExtensionConnector, error) { + resp, err := c.manager.ListExtensions(ctx, &pb.ListExtensionsRequest{Filter: filter}, grpc.WaitForReady(true)) + if err != nil { + return nil, err + } + + var exts []*ExtensionConnector + for _, desc := range resp.Extensions { + exts = append(exts, &ExtensionConnector{Descriptor: desc}) + } + + return exts, nil +} 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/mocks/Manager.go b/pkg/extension/mocks/Manager.go new file mode 100644 index 0000000000000000000000000000000000000000..9dd78407842bb31086066b1ebbee6fb2de180484 --- /dev/null +++ b/pkg/extension/mocks/Manager.go @@ -0,0 +1,192 @@ +// Code generated by mockery v2.14.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + extension "git.perx.ru/perxis/perxis-go/pkg/extension" + extensions "git.perx.ru/perxis/perxis-go/proto/extensions" + + mock "github.com/stretchr/testify/mock" +) + +// Manager is an autogenerated mock type for the Manager type +type Manager struct { + mock.Mock +} + +// Action provides a mock function with given fields: ctx, in +func (_m *Manager) 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 *Manager) 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 *Manager) 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 *Manager) 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 +} + +// ListExtensions provides a mock function with given fields: ctx, filter +func (_m *Manager) ListExtensions(ctx context.Context, filter *extensions.ListExtensionsFilter) ([]*extension.ExtensionConnector, error) { + ret := _m.Called(ctx, filter) + + var r0 []*extension.ExtensionConnector + if rf, ok := ret.Get(0).(func(context.Context, *extensions.ListExtensionsFilter) []*extension.ExtensionConnector); ok { + r0 = rf(ctx, filter) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*extension.ExtensionConnector) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *extensions.ListExtensionsFilter) error); ok { + r1 = rf(ctx, filter) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RegisterExtensions provides a mock function with given fields: ctx, ext +func (_m *Manager) RegisterExtensions(ctx context.Context, ext ...*extension.ExtensionConnector) error { + _va := make([]interface{}, len(ext)) + for _i := range ext { + _va[_i] = ext[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, ...*extension.ExtensionConnector) error); ok { + r0 = rf(ctx, ext...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Uninstall provides a mock function with given fields: ctx, in +func (_m *Manager) 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 +} + +// UnregisterExtensions provides a mock function with given fields: ctx, ext +func (_m *Manager) UnregisterExtensions(ctx context.Context, ext ...*extension.ExtensionConnector) error { + _va := make([]interface{}, len(ext)) + for _i := range ext { + _va[_i] = ext[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, ...*extension.ExtensionConnector) error); ok { + r0 = rf(ctx, ext...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Update provides a mock function with given fields: ctx, in +func (_m *Manager) 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 mockConstructorTestingTNewManager interface { + mock.TestingT + Cleanup(func()) +} + +// NewManager creates a new instance of Manager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewManager(t mockConstructorTestingTNewManager) *Manager { + mock := &Manager{} + 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..f683d0914d897de21dc052f93734c6604e69ced9 --- /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 *pb.ActionRequest) (*pb.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..5400c8c4786cbe09bf2c56fdfb0611d65048d31d --- /dev/null +++ b/pkg/extension/server_test.go @@ -0,0 +1,104 @@ +package extension + +import ( + "context" + "reflect" + "strings" + "testing" + + "git.perx.ru/perxis/perxis-go/pkg/errors" +) + +func TestGetResults(t *testing.T) { + + getDummyExtension := func(name string, wantErr ...bool) Extension { + ext := &testServerExtension{name: name} + + if len(wantErr) > 0 { + ext.err = errors.WithDetail(errors.New("some 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{getDummyExtension("a"), getDummyExtension("b")}, + extensions: []string{"a"}, + fn: func(svc Extension) error { return nil }, + want: []*RequestResult{ + {Extension: "a", State: RequestOK}, + }, + }, + { + name: "multiple extensions without errors", + services: []Extension{getDummyExtension("a"), getDummyExtension("b"), getDummyExtension("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{getDummyExtension("a"), getDummyExtension("b"), getDummyExtension("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{getDummyExtension("a", true), getDummyExtension("b", true), getDummyExtension("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) + } + }) + } +} + +// РЅРµ РїРѕРґС…РѕРґРёС‚ использование mock.Extension РёР·-Р·Р° возникающих циклических импортов +type testServerExtension struct { + err error + name string +} + +func (t testServerExtension) GetDescriptor() *ExtensionDescriptor { + return &ExtensionDescriptor{ + Extension: t.name, + Title: strings.ToTitle(t.name), + Description: "test extension", + Version: "0.0.0", + } +} + +func (t testServerExtension) Install(ctx context.Context, in *InstallRequest) error { return t.err } +func (t testServerExtension) Check(ctx context.Context, in *CheckRequest) error { return t.err } +func (t testServerExtension) Update(ctx context.Context, in *UpdateRequest) error { return t.err } +func (t testServerExtension) Uninstall(ctx context.Context, in *UninstallRequest) error { return t.err } +func (t testServerExtension) Action(ctx context.Context, in *ActionRequest) (*ActionResponse, error) { + return &ActionResponse{}, t.err +} diff --git a/pkg/extension/service/doc.go b/pkg/extension/service/doc.go new file mode 100644 index 0000000000000000000000000000000000000000..c834c26e1dcffff9ae61aa7fa644c10cccd1a621 --- /dev/null +++ b/pkg/extension/service/doc.go @@ -0,0 +1,20 @@ +package service + +/* + +Пакет содержит инструментарий для построения расширений + + +Для реализации расширения сервисом РЅСѓР¶РЅРѕ включение реализации расширения. + +Для автоматизации регистрации расширений +реализуемых сервисом требуется включение регистратора. + +``` +type Service struct { + *service.Extension + *service.Registrar +} +``` + +*/ diff --git a/pkg/extension/service/extension.go b/pkg/extension/service/extension.go new file mode 100644 index 0000000000000000000000000000000000000000..1ee716d9c1d8066b2e1c5a6adbd082382ae30dc0 --- /dev/null +++ b/pkg/extension/service/extension.go @@ -0,0 +1,167 @@ +package service + +import ( + "context" + "fmt" + + "git.perx.ru/perxis/perxis-go/pkg/clients" + "git.perx.ru/perxis/perxis-go/pkg/content" + "git.perx.ru/perxis/perxis-go/pkg/errors" + "git.perx.ru/perxis/perxis-go/pkg/extension" + "git.perx.ru/perxis/perxis-go/pkg/roles" + "git.perx.ru/perxis/perxis-go/pkg/setup" + "go.uber.org/zap" +) + +const ( + roleID = "%s_extension" + roleDesc = "Роль расширения \"%s\"" + clientID = "%s_extension" + clientName = "Клиент расширения \"%s\"" + clientDesc = "Клиент используется РїСЂРё работе расширения \"%s\"" +) + +type SetupFunc func(spaceID, envID string) *setup.Setup +type SignatureFunc func(spaceID string) string + +// Extension реализация сервиса СЃ РѕРґРЅРёРј расширением +type Extension struct { + desc *extension.ExtensionDescriptor + setupFunc SetupFunc + Content *content.Content + Logger *zap.Logger + manager extension.Manager + + withClient bool + role *roles.Role + client *clients.Client + keyFn extension.KeyFunc +} + +func NewExtension(desc *extension.ExtensionDescriptor, cnt *content.Content, setupFunc SetupFunc, logger *zap.Logger) *Extension { + if logger == nil { + logger = zap.NewNop() + } + return &Extension{ + desc: desc, + setupFunc: setupFunc, + Content: cnt, + Logger: logger, + } +} + +func (s *Extension) GetDescriptor() *extension.ExtensionDescriptor { + return s.desc +} + +func (s *Extension) GetName() string { + return s.desc.Extension +} + +// WithClient добавляет РІ процесс установки расширения создание роли Рё приложения для работы расширения +func (s *Extension) WithClient(role *roles.Role, client *clients.Client, fn extension.KeyFunc) *Extension { + s.withClient = true + s.keyFn = fn + s.role = role + s.client = client + return s +} + +// GetKey Получить ключ для доступа Рє пространству +func (s *Extension) GetKey(spaceID string) string { + if s == nil || s.keyFn == nil { + return "" + } + + return s.keyFn(spaceID) +} + +// setupExtensionClient добавляет роль Рё клиента для авторизации расширения +func (s *Extension) setupExtensionClient(set *setup.Setup, spaceID string) { + if !s.withClient { + return + } + + if s.role == nil { + s.role = &roles.Role{} + } + + if s.role.ID == "" { + s.role.ID = fmt.Sprintf(roleID, s.desc.Extension) + } + + if s.role.Description == "" { + s.role.Description = fmt.Sprintf(roleDesc, s.desc.Title) + } + + role := *s.role + role.SpaceID = spaceID + + set.AddRole(&role, setup.DeleteRoleIfRemove()) + + if s.client == nil { + s.client = &clients.Client{} + } + + if s.client.ID == "" { + s.client.ID = fmt.Sprintf(clientID, s.desc.Extension) + } + if s.client.RoleID == "" { + s.client.RoleID = s.role.ID + } + if s.client.Name == "" { + s.client.Name = fmt.Sprintf(clientName, s.desc.Title) + } + if s.client.Description == "" { + s.client.Description = fmt.Sprintf(clientDesc, s.desc.Description) + } + if s.client.OAuth == nil { + s.client.OAuth = &clients.OAuth{ + ClientID: fmt.Sprintf(clientID, s.desc.Extension), + } + } + + client := *s.client + client.SpaceID = spaceID + if client.APIKey == nil { + client.APIKey = &clients.APIKey{ + Key: s.GetKey(spaceID), + } + } + + set.AddClient(&client, setup.OverwriteClient(), setup.DeleteClientIfRemove()) +} + +func (s *Extension) GetSetup(spaceID, envID string) *setup.Setup { + set := s.setupFunc(spaceID, envID) + s.setupExtensionClient(set, spaceID) + return set +} + +func (s *Extension) Install(ctx context.Context, in *extension.InstallRequest) error { + return s.GetSetup(in.SpaceId, in.EnvId).WithForce(in.Force).Install(ctx) +} + +func (s *Extension) Check(ctx context.Context, in *extension.CheckRequest) error { + return s.GetSetup(in.SpaceId, in.EnvId).Check(ctx) +} + +func (s *Extension) Update(ctx context.Context, in *extension.UpdateRequest) error { + return s.GetSetup(in.SpaceId, in.EnvId).WithForce(in.Force).Install(ctx) +} + +func (s *Extension) Uninstall(ctx context.Context, in *extension.UninstallRequest) error { + return s.GetSetup(in.SpaceId, in.EnvId).WithForce(in.Force).WithRemove(in.Remove).Uninstall(ctx) +} + +func (s *Extension) Action(ctx context.Context, in *extension.ActionRequest) (*extension.ActionResponse, error) { + ok, err := extension.CheckInstalled(ctx, s.Content, in.SpaceId, in.EnvId, in.Extension) + if err != nil { + return nil, errors.Wrap(err, "check extension installed") + } + if !ok { + return nil, errors.New("extension not installed") + } + + return &extension.ActionResponse{}, nil +} diff --git a/pkg/extension/service/registrar.go b/pkg/extension/service/registrar.go new file mode 100644 index 0000000000000000000000000000000000000000..5d505d2667a3b752cd4229348f9bfbbeab9f74fb --- /dev/null +++ b/pkg/extension/service/registrar.go @@ -0,0 +1,98 @@ +package service + +import ( + "context" + "time" + + "git.perx.ru/perxis/perxis-go/pkg/auth" + "git.perx.ru/perxis/perxis-go/pkg/extension" + "github.com/avast/retry-go" + "go.uber.org/zap" + "google.golang.org/grpc" +) + +const RegistrationDelay = time.Minute + +// Registrar выполняет действия РїРѕ регистрации Рё обновления регистрации расширений РІ менеджере расширений. Одновременно +// выполняется регистрация РѕРґРЅРѕРіРѕ или нескольких расширений +type Registrar struct { + addr string + managerConn *grpc.ClientConn + manager extension.Manager + exts []extension.Extension + logger *zap.Logger + stopFn func() error +} + +func NewRegistrar(addr string, man extension.Manager, exts []extension.Extension, logger *zap.Logger) *Registrar { + return &Registrar{ + addr: addr, + manager: man, + exts: exts, + logger: logger, + } +} + +func (reg *Registrar) register(ctx context.Context, manager extension.Manager, desc []*extension.ExtensionConnector) error { + err := retry.Do( + func() error { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + err := manager.RegisterExtensions(auth.WithSystem(ctx), desc...) + return err + }, + retry.RetryIf(func(err error) bool { return err != nil }), + retry.OnRetry(func(n uint, err error) { + reg.logger.Warn("Fail to register extension", zap.Uint("Retry", n), zap.Error(err)) + }), + retry.DelayType(retry.BackOffDelay), + retry.MaxDelay(2*time.Minute), + retry.Attempts(1000), + retry.Context(ctx), + ) + + if err == nil { + reg.logger.Debug("Extensions successful registered") + } + + return err +} + +func (reg *Registrar) Start() error { + registrationDelay := time.Duration(0) + + regCtx, regStop := context.WithCancel(context.Background()) + + reg.stopFn = func() error { + regStop() + return nil + } + + extList := make([]*extension.ExtensionConnector, 0, len(reg.exts)) + for _, v := range reg.exts { + desc := *v.GetDescriptor() + desc.Url = reg.addr + extList = append(extList, &extension.ExtensionConnector{Descriptor: &desc}) + } + + reg.logger.Info("Start registration process") + + go func() { + for { + select { + case <-time.After(registrationDelay): + reg.register(regCtx, reg.manager, extList) + registrationDelay = RegistrationDelay + case <-regCtx.Done(): + reg.logger.Info("Stop registration process") + return + } + } + }() + + return nil +} + +func (reg *Registrar) Stop() error { + return reg.stopFn() +} diff --git a/pkg/items/options.go b/pkg/items/options.go index 3d45a226ac44f858cf12c6df3e1eafdb1a3b38fa..0c6bd9094ff763f0e2df03fcb2525cd5555a44eb 100644 --- a/pkg/items/options.go +++ b/pkg/items/options.go @@ -215,7 +215,7 @@ func MergePublishOptions(opts ...*PublishOptions) *PublishOptions { type UnpublishOptions struct { UpdateAttrs bool - + Options } diff --git a/pkg/items/transport/client.go b/pkg/items/transport/client.go index db1a7a789df8bf7b820d018a219bcd4d3a635153..fd5de3f21b7ad9ec0bb1eed5f0a908216c9531d7 100644 --- a/pkg/items/transport/client.go +++ b/pkg/items/transport/client.go @@ -90,7 +90,7 @@ func (set EndpointsSet) Update(arg0 context.Context, arg1 *items.Item, arg2 ...* } func (set EndpointsSet) Delete(arg0 context.Context, arg1 *items.Item, arg2 ...*items.DeleteOptions) (res0 error) { - request := DeleteRequest{Item: arg1,Options: arg2} + request := DeleteRequest{Item: arg1, Options: arg2} _, res0 = set.DeleteEndpoint(arg0, &request) if res0 != nil { return @@ -98,7 +98,7 @@ func (set EndpointsSet) Delete(arg0 context.Context, arg1 *items.Item, arg2 ...* return res0 } -func (set EndpointsSet) Undelete(arg0 context.Context,arg1 *items.Item, options ...*items.UndeleteOptions) (res0 error) { +func (set EndpointsSet) Undelete(arg0 context.Context, arg1 *items.Item, options ...*items.UndeleteOptions) (res0 error) { request := UndeleteRequest{Item: arg1, Options: options} _, res0 = set.UndeleteEndpoint(arg0, &request) if res0 != nil { diff --git a/pkg/items/transport/grpc/protobuf_type_converters.microgen.go b/pkg/items/transport/grpc/protobuf_type_converters.microgen.go index 63182822f884b61f9e5d9e21f7d73ba1eadf0f69..212b1ecd2bfb27db8b49d0f965b484226265f189 100644 --- a/pkg/items/transport/grpc/protobuf_type_converters.microgen.go +++ b/pkg/items/transport/grpc/protobuf_type_converters.microgen.go @@ -318,7 +318,7 @@ func DeleteOptionsToProto(options []*service.DeleteOptions) (*pb.DeleteOptions, return &pb.DeleteOptions{ UpdateAttrs: opts.UpdateAttrs, - Erase: opts.Erase, + Erase: opts.Erase, }, nil } diff --git a/pkg/organizations/transport/exchanges.microgen.go b/pkg/organizations/transport/exchanges.microgen.go index 7ae098e5b5b555cfe834c55e2fe73c79805fdbfd..480cc3e5b7b3d6ea68bfa2e9bea23f0156d6f3d0 100644 --- a/pkg/organizations/transport/exchanges.microgen.go +++ b/pkg/organizations/transport/exchanges.microgen.go @@ -36,7 +36,7 @@ type ( FindRequest struct { Filter *organizations.Filter `json:"filter"` - Opts *options.FindOptions `json:"opts"` + Opts *options.FindOptions `json:"opts"` } FindResponse struct { Orgs []*organizations.Organization `json:"orgs"` diff --git a/pkg/setup/client.go b/pkg/setup/client.go new file mode 100644 index 0000000000000000000000000000000000000000..98fa6a0e7a0d9e7d7676b89b43e096121f1804fd --- /dev/null +++ b/pkg/setup/client.go @@ -0,0 +1,188 @@ +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)) + + for _, c := range s.Clients { + err := s.InstallClient(ctx, c) + if err != nil { + s.logger.Error("Failed to install client", zap.String("Client ID", c.client.ID), zap.String("Client Name", c.client.Name), zap.Error(err)) + return errors.WithDetailf(errors.Wrap(err, "failed to install client"), "Возникла ошибка РїСЂРё настройке клиента %s(%s)", c.client.Name, c.client.ID) + } + } + 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)) + + for _, c := range s.Clients { + if err := s.UninstallClient(ctx, c); err != nil { + s.logger.Error("Failed to uninstall client", zap.String("Client ID", c.client.ID), zap.String("Client Name", c.client.Name), zap.Error(err)) + return errors.WithDetailf(errors.Wrap(err, "failed to uninstall client"), "Возникла ошибка РїСЂРё удалении клиента %s(%s)", c.client.Name, c.client.ID) + } + } + + 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/client_test.go b/pkg/setup/client_test.go new file mode 100644 index 0000000000000000000000000000000000000000..d345756a2b0b87e7ad10aebc1ee9ce0899df199c --- /dev/null +++ b/pkg/setup/client_test.go @@ -0,0 +1,97 @@ +package setup + +import ( + "context" + "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/content" + "git.perx.ru/perxis/perxis-go/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestSetup_InstallClients(t *testing.T) { + + tests := []struct { + name string + clients []*clients.Client + clientsCall func(svc *clientsMock.Clients) + wantErr func(t *testing.T, err error) + }{ + { + name: "Nil clients", + clients: nil, + clientsCall: func(svc *clientsMock.Clients) {}, + wantErr: func(t *testing.T, err error) { + assert.NoError(t, err) + }, + }, + { + name: "Install one client success", + clients: []*clients.Client{{ID: "1", SpaceID: "sp", Name: "client", RoleID: "role-1"}}, + clientsCall: func(svc *clientsMock.Clients) { + svc.On("Get", mock.Anything, "sp", "1").Return(nil, errors.New("not found")).Once() + svc.On("Create", mock.Anything, &clients.Client{ID: "1", SpaceID: "sp", Name: "client", RoleID: "role-1"}).Return(&clients.Client{ID: "1", SpaceID: "sp", Name: "client", RoleID: "role-1"}, nil).Once() + }, + wantErr: func(t *testing.T, err error) { + assert.NoError(t, err) + }, + }, + { + name: "Install one client fails", + clients: []*clients.Client{{ID: "1", SpaceID: "sp", Name: "client", RoleID: "role-1"}}, + clientsCall: func(svc *clientsMock.Clients) { + svc.On("Get", mock.Anything, "sp", "1").Return(nil, errors.New("not found")).Once() + svc.On("Create", mock.Anything, &clients.Client{ID: "1", SpaceID: "sp", Name: "client", RoleID: "role-1"}).Return(nil, errors.New("some error")).Once() + }, + wantErr: func(t *testing.T, err error) { + assert.Error(t, err) + assert.EqualError(t, err, "failed to install client: some error") + assert.Contains(t, errors.GetDetail(err), "Возникла ошибка РїСЂРё настройке клиента client(1)") + }, + }, + { + name: "Install multiple clients success", + clients: []*clients.Client{{ID: "1", SpaceID: "sp", Name: "client", RoleID: "role-1"}, {ID: "2", SpaceID: "sp", Name: "client", RoleID: "role-2"}}, + clientsCall: func(svc *clientsMock.Clients) { + svc.On("Get", mock.Anything, "sp", "1").Return(nil, errors.New("not found")).Once() + svc.On("Get", mock.Anything, "sp", "2").Return(nil, errors.New("not found")).Once() + svc.On("Create", mock.Anything, &clients.Client{ID: "1", SpaceID: "sp", Name: "client", RoleID: "role-1"}).Return(&clients.Client{ID: "1", SpaceID: "sp", Name: "client", RoleID: "role-1"}, nil).Once() + svc.On("Create", mock.Anything, &clients.Client{ID: "2", SpaceID: "sp", Name: "client", RoleID: "role-2"}).Return(&clients.Client{ID: "2", SpaceID: "sp", Name: "client", RoleID: "role-2"}, nil).Once() + }, + wantErr: func(t *testing.T, err error) { + assert.NoError(t, err) + }, + }, + { + name: "Install multiple clients fails", + clients: []*clients.Client{{ID: "1", SpaceID: "sp", Name: "client", RoleID: "role-1"}, {ID: "2", SpaceID: "sp", Name: "client", RoleID: "role-2"}}, + clientsCall: func(svc *clientsMock.Clients) { + svc.On("Get", mock.Anything, "sp", "1").Return(nil, errors.New("not found")).Once() + svc.On("Get", mock.Anything, "sp", "2").Return(nil, errors.New("not found")).Once() + svc.On("Create", mock.Anything, &clients.Client{ID: "1", SpaceID: "sp", Name: "client", RoleID: "role-1"}).Return(nil, errors.New("some error")).Once() + svc.On("Create", mock.Anything, &clients.Client{ID: "2", SpaceID: "sp", Name: "client", RoleID: "role-2"}).Return(nil, errors.New("some error")).Once() + }, + wantErr: func(t *testing.T, err error) { + assert.Error(t, err) + assert.EqualError(t, err, "failed to install client: some error") + assert.Contains(t, errors.GetDetail(err), "Возникла ошибка РїСЂРё настройке клиента client(1)") + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &clientsMock.Clients{} + if tt.clientsCall != nil { + tt.clientsCall(c) + } + + s := NewSetup(&content.Content{Clients: c}, "sp", "env", nil) + s.AddClients(tt.clients) + tt.wantErr(t, s.InstallClients(context.Background())) + }) + } +} diff --git a/pkg/setup/collection.go b/pkg/setup/collection.go new file mode 100644 index 0000000000000000000000000000000000000000..d61ba14d18f2d8bc93d2a535392b8b608ba6e2d0 --- /dev/null +++ b/pkg/setup/collection.go @@ -0,0 +1,215 @@ +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 migrate, setSchema bool + + for _, c := range s.Collections { + setSchema, err = s.InstallCollection(ctx, c) + if err != nil { + s.logger.Error("Failed to install collection", + zap.String("Collection ID", c.collection.ID), + zap.String("Collection Name", c.collection.Name), + zap.Error(err), + ) + return errors.WithDetailf(errors.Wrap(err, "failed to install collection"), "Возникла ошибка РїСЂРё настройке коллекции %s(%s)", c.collection.Name, c.collection.ID) + } + if setSchema { + migrate = true + } + } + + 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))) + + for _, c := range s.Collections { + if err := s.UninstallCollection(ctx, c); err != nil { + s.logger.Error("Failed to uninstall collection", + zap.String("Collection ID", c.collection.ID), + zap.String("Collection Name", c.collection.Name), + zap.Error(err), + ) + return errors.WithDetailf(errors.Wrap(err, "failed to uninstall collection"), "Возникла ошибка РїСЂРё удалении коллекции %s(%s)", c.collection.Name, c.collection.ID) + } + } + + 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/collection_test.go b/pkg/setup/collection_test.go new file mode 100644 index 0000000000000000000000000000000000000000..433aa7397d450f8226a95af032a7cd177697fbed --- /dev/null +++ b/pkg/setup/collection_test.go @@ -0,0 +1,112 @@ +package setup + +import ( + "context" + "testing" + + "git.perx.ru/perxis/perxis-go/pkg/collections" + mockscollections "git.perx.ru/perxis/perxis-go/pkg/collections/mocks" + "git.perx.ru/perxis/perxis-go/pkg/content" + "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/schema" + "git.perx.ru/perxis/perxis-go/pkg/schema/field" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestSetup_InstallCollections(t *testing.T) { + tests := []struct { + name string + collections []*collections.Collection + collectionsCall func(svc *mockscollections.Collections) + envsCall func(svc *envmocks.Environments) + wantErr func(t *testing.T, err error) + }{ + { + name: "Nil collections", + collections: nil, + collectionsCall: func(svc *mockscollections.Collections) {}, + wantErr: func(t *testing.T, err error) { + assert.NoError(t, err) + }, + }, + { + name: "Install one collection success", + collections: []*collections.Collection{{ID: "1", SpaceID: "sp", Name: "space", EnvID: "env", Schema: schema.New("name", field.String())}}, + collectionsCall: func(svc *mockscollections.Collections) { + svc.On("Get", mock.Anything, "sp", "env", "1").Return(nil, errors.New("not found")).Once() + svc.On("Create", mock.Anything, &collections.Collection{ID: "1", SpaceID: "sp", Name: "space", EnvID: "env", Schema: schema.New("name", field.String())}).Return(&collections.Collection{ID: "1", SpaceID: "sp", Name: "space", EnvID: "env", Schema: schema.New("name", field.String())}, 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: "Install one collection fails", + collections: []*collections.Collection{{ID: "1", SpaceID: "sp", Name: "space", EnvID: "env", Schema: schema.New("name", field.String())}}, + collectionsCall: func(svc *mockscollections.Collections) { + svc.On("Get", mock.Anything, "sp", "env", "1").Return(nil, errors.New("not found")).Once() + svc.On("Create", mock.Anything, &collections.Collection{ID: "1", SpaceID: "sp", Name: "space", EnvID: "env", Schema: schema.New("name", field.String())}).Return(nil, errors.New("some error")).Once() + }, + wantErr: func(t *testing.T, err error) { + assert.Error(t, err) + assert.EqualError(t, err, "failed to install collection: some error") + assert.Contains(t, errors.GetDetail(err), "Возникла ошибка РїСЂРё настройке коллекции space(1)") + }, + }, + { + name: "Install multiple collections success", + collections: []*collections.Collection{{ID: "1", SpaceID: "sp", Name: "space", EnvID: "env", Schema: schema.New("name", field.String())}, {ID: "2", SpaceID: "sp", Name: "space", EnvID: "env", Schema: schema.New("name", field.String())}}, + collectionsCall: func(svc *mockscollections.Collections) { + svc.On("Get", mock.Anything, "sp", "env", "1").Return(nil, errors.New("not found")).Once() + svc.On("Get", mock.Anything, "sp", "env", "2").Return(nil, errors.New("not found")).Once() + svc.On("Create", mock.Anything, &collections.Collection{ID: "1", SpaceID: "sp", Name: "space", EnvID: "env", Schema: schema.New("name", field.String())}).Return(&collections.Collection{ID: "1", SpaceID: "sp", Name: "space", EnvID: "env", Schema: schema.New("name", field.String())}, nil).Once() + svc.On("Create", mock.Anything, &collections.Collection{ID: "2", SpaceID: "sp", Name: "space", EnvID: "env", Schema: schema.New("name", field.String())}).Return(&collections.Collection{ID: "2", SpaceID: "sp", Name: "space", EnvID: "env", Schema: schema.New("name", field.String())}, nil).Once() + svc.On("SetSchema", mock.Anything, "sp", "env", "1", schema.New("name", field.String())).Return(nil).Once() + svc.On("SetSchema", mock.Anything, "sp", "env", "2", 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).Twice() + }, + wantErr: func(t *testing.T, err error) { + assert.NoError(t, err) + }, + }, + { + name: "Install multiple collections fails", + collections: []*collections.Collection{{ID: "1", SpaceID: "sp", Name: "space", EnvID: "env", Schema: schema.New("name", field.String())}, {ID: "2", SpaceID: "sp", Name: "space", EnvID: "env", Schema: schema.New("name", field.String())}}, + collectionsCall: func(svc *mockscollections.Collections) { + svc.On("Get", mock.Anything, "sp", "env", "1").Return(nil, errors.New("not found")).Once() + svc.On("Create", mock.Anything, &collections.Collection{ID: "1", SpaceID: "sp", Name: "space", EnvID: "env", Schema: schema.New("name", field.String())}).Return(nil, errors.New("some error")).Once() + }, + wantErr: func(t *testing.T, err error) { + assert.Error(t, err) + assert.EqualError(t, err, "failed to install collection: some error") + assert.Contains(t, errors.GetDetail(err), "Возникла ошибка РїСЂРё настройке коллекции space(1)") + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &mockscollections.Collections{} + e := &envmocks.Environments{} + if tt.collectionsCall != nil { + tt.collectionsCall(c) + } + if tt.envsCall != nil { + tt.envsCall(e) + } + + s := NewSetup(&content.Content{Collections: c, Environments: e}, "sp", "env", nil) + s.AddCollections(tt.collections) + tt.wantErr(t, s.InstallCollections(context.Background())) + }) + } +} diff --git a/pkg/setup/item.go b/pkg/setup/item.go new file mode 100644 index 0000000000000000000000000000000000000000..d275d0ff38fa33e7c2a77010344b5f6117d2310d --- /dev/null +++ b/pkg/setup/item.go @@ -0,0 +1,278 @@ +package setup + +import ( + "context" + "reflect" + "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 OverwriteFields(fields ...string) ItemsOption { + return func(c *ItemConfig) { + c.UpdateFn = func(s *Setup, old, new *items.Item) (*items.Item, bool) { + + var changed bool + for _, field := range fields { + if items.IsSystemField(field) { + continue + } + + newValue, err := new.Get(field) + if err != nil { + continue + } + + oldValue, err := old.Get(field) + if err != nil || newValue != oldValue { + changed = true + if err = old.Set(field, newValue); err != nil { + return nil, false // РЅРµ обновляем данные если РЅРµ удалось установить значение + } + } + } + + return old, changed + } + } +} + +func KeepFields(fields ...string) ItemsOption { + return func(c *ItemConfig) { + c.UpdateFn = func(s *Setup, old, new *items.Item) (*items.Item, bool) { + + for _, field := range fields { + if items.IsSystemField(field) { + continue + } + + oldValue, err := old.Get(field) + if err != nil { + continue + } + + newValue, err := new.Get(field) + if err != nil || newValue != oldValue { + if err = new.Set(field, oldValue); err != nil { + return nil, false // РЅРµ обновляем данные если РЅРµ удалось установить значение + } + } + } + + return new, !reflect.DeepEqual(old, new) + } + } +} + +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))) + + for col, itms := range s.groupByCollection() { + exists, err := s.getExisting(ctx, col, itms) + if err != nil { + return err + } + for _, c := range itms { + if err := s.InstallItem(ctx, exists, c); err != nil { + s.logger.Error("Failed to install item", + zap.String("ID", c.item.ID), + zap.String("Collection", c.item.CollectionID), + zap.Error(err), + ) + return errors.WithDetailf(errors.Wrap(err, "failed to install item"), "Возникла ошибка РїСЂРё добавлении элемента %s(%s)", c.item.ID, c.item.CollectionID) + } + } + } + + return nil +} + +func (s *Setup) InstallItem(ctx context.Context, exists map[string]*items.Item, c ItemConfig) error { + item := c.item + item.SpaceID, item.EnvID = s.SpaceID, s.EnvironmentID + + exist, ok := exists[item.ID] + if !ok { + return items.CreateAndPublishItem(ctx, s.content.Items, item) + } + + if item, changed := c.UpdateFn(s, exist, c.item); changed { + return items.UpdateAndPublishItem(ctx, s.content.Items, 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))) + + for _, c := range s.Items { + if err := s.UninstallItem(ctx, c); err != nil { + s.logger.Error("Failed to uninstall item", + zap.String("Item", c.item.ID), + zap.String("Item", c.item.CollectionID), + zap.Error(err), + ) + return errors.WithDetailf(errors.Wrap(err, "failed to uninstall item"), "Возникла ошибка РїСЂРё удалении элемента %s(%s)", c.item.ID, c.item.CollectionID) + } + } + + return nil +} + +func (s *Setup) UninstallItem(ctx context.Context, c ItemConfig) error { + if c.DeleteFn(s, c.item) { + err := s.content.Items.Delete(ctx, &items.Item{SpaceID: s.SpaceID, EnvID: s.EnvironmentID, CollectionID: c.item.CollectionID, ID: 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 col, itms := range s.groupByCollection() { + exists, err := s.getExisting(ctx, col, itms) + if err != nil { + return err + } + + for _, c := range itms { + if _, ok := exists[c.item.ID]; !ok { + errs = append(errs, errors.WithDetailf(errors.New("not found"), "РќРµ найден элемент %s(%s)", c.item.ID, c.item.CollectionID)) + } + } + } + + if len(errs) > 0 { + return errors.WithErrors(ErrCheckItems, errs...) + } + + 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 +} + +func (s *Setup) groupByCollection() map[string][]ItemConfig { + itemsByColl := map[string][]ItemConfig{} + for _, i := range s.Items { + cfg, ok := itemsByColl[i.item.CollectionID] + if !ok { + itemsByColl[i.item.CollectionID] = []ItemConfig{i} + continue + } + itemsByColl[i.item.CollectionID] = append(cfg, i) + } + return itemsByColl +} + +func (s *Setup) getExisting(ctx context.Context, collID string, configs []ItemConfig) (map[string]*items.Item, error) { + itms, _, err := s.content.Items.Find( + ctx, + s.SpaceID, + s.EnvironmentID, + collID, + &items.Filter{ID: getItemIds(configs)}, + &items.FindOptions{Regular: true, Hidden: true, Templates: true}, + ) + if err != nil { + s.logger.Error("Failed to find existing items", + zap.String("Collection", collID), + zap.Error(err), + ) + return nil, errors.WithDetailf(errors.Wrap(err, "failed to find existing items"), "Возникла ошибка РїСЂРё РїРѕРёСЃРєРµ элементов РІ коллекции %s", collID) + } + + exists := make(map[string]*items.Item, len(itms)) + for _, itm := range itms { + exists[itm.ID] = itm + } + return exists, nil +} + +func getItemIds(items []ItemConfig) []string { + var ids []string + for _, i := range items { + ids = append(ids, i.item.ID) + } + return ids +} diff --git a/pkg/setup/item_test.go b/pkg/setup/item_test.go new file mode 100644 index 0000000000000000000000000000000000000000..920292f501cf9e469150621b3894bcad41326aa8 --- /dev/null +++ b/pkg/setup/item_test.go @@ -0,0 +1,279 @@ +package setup + +import ( + "context" + "testing" + + "git.perx.ru/perxis/perxis-go/pkg/content" + "git.perx.ru/perxis/perxis-go/pkg/errors" + "git.perx.ru/perxis/perxis-go/pkg/items" + itemsMock "git.perx.ru/perxis/perxis-go/pkg/items/mocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestItem_OverwriteFields(t *testing.T) { + tests := []struct { + name string + fields []string + new *items.Item + old *items.Item + want *items.Item + changed bool + }{ + { + name: "Empty", + fields: []string{}, + new: &items.Item{ID: "id", Data: map[string]interface{}{"key": "value"}}, + old: &items.Item{ID: "id", Data: map[string]interface{}{"key": "value"}}, + changed: false, + }, + { + name: "Not found", + fields: []string{"notfound"}, + new: &items.Item{ID: "id", Data: map[string]interface{}{"key": "value"}}, + old: &items.Item{ID: "id", Data: map[string]interface{}{"key": "value"}}, + changed: false, + }, + { + name: "Equal value", + fields: []string{"key"}, + new: &items.Item{ID: "id", Data: map[string]interface{}{"key": "value"}}, + old: &items.Item{ID: "id", Data: map[string]interface{}{"key": "value"}}, + changed: false, + }, + { + name: "Not Equal value #1", + fields: []string{"key"}, + new: &items.Item{ID: "id", Data: map[string]interface{}{"key": "value1"}}, + old: &items.Item{ID: "id", Data: map[string]interface{}{"key": "value", "key2": "value2"}}, + want: &items.Item{ID: "id", Data: map[string]interface{}{"key": "value1", "key2": "value2"}}, + changed: true, + }, + { + name: "Not Equal value #2", + fields: []string{"key", "key2"}, + new: &items.Item{ID: "id", Data: map[string]interface{}{"key": "value1", "key2": "value2"}}, + old: &items.Item{ID: "id", Data: map[string]interface{}{"key": "value", "key3": "value3"}}, + want: &items.Item{ID: "id", Data: map[string]interface{}{"key": "value1", "key2": "value2", "key3": "value3"}}, + changed: true, + }, + { + name: "Equal nested", + fields: []string{"hoop.exclude"}, + new: &items.Item{ID: "id", Data: map[string]interface{}{"hoop": map[string]interface{}{"exclude": true}}}, + old: &items.Item{ID: "id", Data: map[string]interface{}{"hoop": map[string]interface{}{"exclude": true}, "key": "value"}}, + changed: false, + }, + { + name: "Not Equal nested", + fields: []string{"hoop.exclude"}, + new: &items.Item{ID: "id", Data: map[string]interface{}{"hoop": map[string]interface{}{"exclude": true}}}, + old: &items.Item{ID: "id", Data: map[string]interface{}{"hoop": map[string]interface{}{"exclude": false}, "key": "value"}}, + want: &items.Item{ID: "id", Data: map[string]interface{}{"hoop": map[string]interface{}{"exclude": true}, "key": "value"}}, + changed: true, + }, + { + name: "System", + fields: []string{"id"}, + new: &items.Item{ID: "id", Data: map[string]interface{}{"id": "value"}}, + old: &items.Item{ID: "id", Data: map[string]interface{}{"id": "value"}}, + changed: false, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + c := ItemConfig{item: test.new} + OverwriteFields(test.fields...)(&c) + + got, changed := c.UpdateFn(nil, test.old, test.new) + require.Equal(t, test.changed, changed) + if !test.changed { + return + } + assert.Equal(t, test.want, got) + + }) + } +} + +func TestItem_KeepFields(t *testing.T) { + tests := []struct { + name string + fields []string + new *items.Item + old *items.Item + want *items.Item + changed bool + }{ + { + name: "Empty", + fields: []string{}, + new: &items.Item{ID: "id", Data: map[string]interface{}{"key": "value"}}, + old: &items.Item{ID: "id", Data: map[string]interface{}{"key": "0"}}, + want: &items.Item{ID: "id", Data: map[string]interface{}{"key": "value"}}, + changed: true, + }, + { + name: "Not found", + fields: []string{"notfound"}, + new: &items.Item{ID: "id", Data: map[string]interface{}{"key": "value"}}, + old: &items.Item{ID: "id", Data: map[string]interface{}{"key": "0"}}, + want: &items.Item{ID: "id", Data: map[string]interface{}{"key": "value"}}, + changed: true, + }, + { + name: "Equal value", + fields: []string{"key"}, + new: &items.Item{ID: "id", Data: map[string]interface{}{"key": "value", "key2": "value2"}}, + old: &items.Item{ID: "id", Data: map[string]interface{}{"key": "value"}}, + want: &items.Item{ID: "id", Data: map[string]interface{}{"key": "value", "key2": "value2"}}, + changed: true, + }, + { + name: "Not Equal value #1", + fields: []string{"key"}, + new: &items.Item{ID: "id", Data: map[string]interface{}{"key": "value1", "key3": "value3"}}, + old: &items.Item{ID: "id", Data: map[string]interface{}{"key": "value", "key2": "value2"}}, + want: &items.Item{ID: "id", Data: map[string]interface{}{"key": "value", "key3": "value3"}}, + changed: true, + }, + { + name: "Not Equal value #2", + fields: []string{"key", "key2"}, + new: &items.Item{ID: "id", Data: map[string]interface{}{"key": "value1", "key3": "value3"}}, + old: &items.Item{ID: "id", Data: map[string]interface{}{"key": "value", "key2": "value2"}}, + want: &items.Item{ID: "id", Data: map[string]interface{}{"key": "value", "key2": "value2", "key3": "value3"}}, + changed: true, + }, + { + name: "Equal nested", + fields: []string{"hoop.exclude"}, + new: &items.Item{ID: "id", Data: map[string]interface{}{"hoop": map[string]interface{}{"exclude": true}}}, + old: &items.Item{ID: "id", Data: map[string]interface{}{"hoop": map[string]interface{}{"exclude": true}, "key": "value"}}, + want: &items.Item{ID: "id", Data: map[string]interface{}{"hoop": map[string]interface{}{"exclude": true}}}, + changed: true, + }, + { + name: "Not Equal nested", + fields: []string{"hoop.exclude"}, + new: &items.Item{ID: "id", Data: map[string]interface{}{"hoop": map[string]interface{}{"exclude": true}}}, + old: &items.Item{ID: "id", Data: map[string]interface{}{"hoop": map[string]interface{}{"exclude": false}, "key": "value"}}, + want: &items.Item{ID: "id", Data: map[string]interface{}{"hoop": map[string]interface{}{"exclude": false}}}, + changed: true, + }, + { + name: "System", + fields: []string{"id"}, + new: &items.Item{ID: "id", Data: map[string]interface{}{"id": "value1"}}, + old: &items.Item{ID: "id", Data: map[string]interface{}{"id": "value"}}, + want: &items.Item{ID: "id", Data: map[string]interface{}{"id": "value1"}}, + changed: true, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + c := ItemConfig{item: test.new} + KeepFields(test.fields...)(&c) + + got, changed := c.UpdateFn(nil, test.old, test.new) + require.Equal(t, test.changed, changed) + if !test.changed { + return + } + assert.Equal(t, test.want, got) + + }) + } +} + +func TestSetup_InstallItems(t *testing.T) { + + tests := []struct { + name string + items []*items.Item + itemsCall func(svc *itemsMock.Items) + wantErr func(t *testing.T, err error) + }{ + { + name: "Nil clients", + items: nil, + itemsCall: func(svc *itemsMock.Items) {}, + wantErr: func(t *testing.T, err error) { + assert.NoError(t, err) + }, + }, + { + name: "Install one item success", + items: []*items.Item{{ID: "1", SpaceID: "sp", EnvID: "env", CollectionID: "coll", Data: map[string]interface{}{"text": "test"}}}, + itemsCall: func(svc *itemsMock.Items) { + svc.On("Find", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, 0, nil).Once() + svc.On("Create", mock.Anything, &items.Item{ID: "1", SpaceID: "sp", EnvID: "env", CollectionID: "coll", Data: map[string]interface{}{"text": "test"}}).Return(&items.Item{ID: "1", SpaceID: "sp", EnvID: "env", CollectionID: "coll", Data: map[string]interface{}{"text": "test"}}, nil).Once() + svc.On("Publish", mock.Anything, &items.Item{ID: "1", SpaceID: "sp", EnvID: "env", CollectionID: "coll", Data: map[string]interface{}{"text": "test"}}).Return(nil).Once() + }, + wantErr: func(t *testing.T, err error) { + assert.NoError(t, err) + }, + }, + { + name: "Install one item fails", + items: []*items.Item{{ID: "1", SpaceID: "sp", EnvID: "env", CollectionID: "coll", Data: map[string]interface{}{"text": "test"}}}, + itemsCall: func(svc *itemsMock.Items) { + svc.On("Find", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, 0, nil).Once() + svc.On("Create", mock.Anything, &items.Item{ID: "1", SpaceID: "sp", EnvID: "env", CollectionID: "coll", Data: map[string]interface{}{"text": "test"}}).Return(nil, errors.New("some error")).Once() + }, + wantErr: func(t *testing.T, err error) { + assert.Error(t, err) + assert.EqualError(t, err, "failed to install item: create item: some error") + assert.Contains(t, errors.GetDetail(err), "Возникла ошибка РїСЂРё добавлении элемента 1(coll)") + }, + }, + { + name: "Install multiple items success", + items: []*items.Item{ + {ID: "1", SpaceID: "sp", EnvID: "env", CollectionID: "coll", Data: map[string]interface{}{"text": "test"}}, + {ID: "2", SpaceID: "sp", EnvID: "env", CollectionID: "coll", Data: map[string]interface{}{"text": "test"}}, + }, + itemsCall: func(svc *itemsMock.Items) { + svc.On("Find", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, 0, nil).Once() + svc.On("Create", mock.Anything, &items.Item{ID: "1", SpaceID: "sp", EnvID: "env", CollectionID: "coll", Data: map[string]interface{}{"text": "test"}}).Return(&items.Item{ID: "1", SpaceID: "sp", EnvID: "env", CollectionID: "coll", Data: map[string]interface{}{"text": "test"}}, nil).Once() + svc.On("Create", mock.Anything, &items.Item{ID: "2", SpaceID: "sp", EnvID: "env", CollectionID: "coll", Data: map[string]interface{}{"text": "test"}}).Return(&items.Item{ID: "2", SpaceID: "sp", EnvID: "env", CollectionID: "coll", Data: map[string]interface{}{"text": "test"}}, nil).Once() + svc.On("Publish", mock.Anything, &items.Item{ID: "1", SpaceID: "sp", EnvID: "env", CollectionID: "coll", Data: map[string]interface{}{"text": "test"}}).Return(nil).Once() + svc.On("Publish", mock.Anything, &items.Item{ID: "2", SpaceID: "sp", EnvID: "env", CollectionID: "coll", Data: map[string]interface{}{"text": "test"}}).Return(nil).Once() + }, + wantErr: func(t *testing.T, err error) { + assert.NoError(t, err) + }, + }, + { + name: "Install multiple items fails", + items: []*items.Item{ + {ID: "1", SpaceID: "sp", EnvID: "env", CollectionID: "coll", Data: map[string]interface{}{"text": "test"}}, + {ID: "2", SpaceID: "sp", EnvID: "env", CollectionID: "coll", Data: map[string]interface{}{"text": "test"}}, + }, + itemsCall: func(svc *itemsMock.Items) { + svc.On("Find", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, 0, nil).Once() + svc.On("Create", mock.Anything, &items.Item{ID: "1", SpaceID: "sp", EnvID: "env", CollectionID: "coll", Data: map[string]interface{}{"text": "test"}}).Return(nil, errors.New("some error")).Once() + }, + wantErr: func(t *testing.T, err error) { + assert.Error(t, err) + assert.EqualError(t, err, "failed to install item: create item: some error") + assert.Contains(t, errors.GetDetail(err), "Возникла ошибка РїСЂРё добавлении элемента 1(coll)") + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + i := &itemsMock.Items{} + if tt.itemsCall != nil { + tt.itemsCall(i) + } + + s := NewSetup(&content.Content{Items: i}, "sp", "env", nil) + s.AddItems(tt.items) + tt.wantErr(t, s.InstallItems(context.Background())) + }) + } +} diff --git a/pkg/setup/role.go b/pkg/setup/role.go new file mode 100644 index 0000000000000000000000000000000000000000..ac6e641a97c6160cf29c5353f0176ef110a9bab3 --- /dev/null +++ b/pkg/setup/role.go @@ -0,0 +1,201 @@ +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 || +// !util.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))) + + for _, c := range s.Roles { + if err := s.InstallRole(ctx, c); err != nil { + s.logger.Error("Failed to install role", zap.String("Role ID", c.role.ID), zap.Error(err)) + return errors.Wrap(errors.WithDetailf(err, "Возникла ошибка РїСЂРё настройке роли %s(%s)", c.role.ID, c.role.Description), "failed to install role") + } + } + + 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))) + + for _, c := range s.Roles { + if err := s.UninstallRole(ctx, c); err != nil { + s.logger.Error("Failed to uninstall role", zap.String("Role ID", c.role.ID), zap.Error(err)) + return errors.WithDetailf(errors.Wrap(err, "failed to uninstall role"), "Возникла ошибка РїСЂРё удалении роли %s(%s)", c.role.ID, c.role.Description) + } + } + + 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/role_test.go b/pkg/setup/role_test.go new file mode 100644 index 0000000000000000000000000000000000000000..1c218270b5b50bf2bc8c6cd27364a65cd2d9b60b --- /dev/null +++ b/pkg/setup/role_test.go @@ -0,0 +1,98 @@ +package setup + +import ( + "context" + "testing" + + "git.perx.ru/perxis/perxis-go/pkg/content" + "git.perx.ru/perxis/perxis-go/pkg/errors" + "git.perx.ru/perxis/perxis-go/pkg/roles" + rolesMock "git.perx.ru/perxis/perxis-go/pkg/roles/mocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestSetup_InstallRoles(t *testing.T) { + + tests := []struct { + name string + roles []*roles.Role + rolesCall func(svc *rolesMock.Roles) + wantErr func(t *testing.T, err error) + }{ + { + name: "Nil role", + roles: nil, + rolesCall: func(svc *rolesMock.Roles) {}, + wantErr: func(t *testing.T, err error) { + assert.NoError(t, err) + }, + }, + { + name: "Install one role success", + roles: []*roles.Role{{ID: "1", SpaceID: "sp", Environments: []string{"env"}, Description: "test"}}, + rolesCall: func(svc *rolesMock.Roles) { + svc.On("Get", mock.Anything, "sp", "1").Return(nil, errors.New("not found")).Once() + svc.On("Create", mock.Anything, &roles.Role{ID: "1", SpaceID: "sp", Environments: []string{"env"}, Description: "test"}).Return(&roles.Role{ID: "1", SpaceID: "sp", Environments: []string{"env"}, Description: "test"}, nil).Once() + }, + wantErr: func(t *testing.T, err error) { + assert.NoError(t, err) + }, + }, + { + name: "Install one role fails", + roles: []*roles.Role{{ID: "1", SpaceID: "sp", Environments: []string{"env"}, Description: "test"}}, + rolesCall: func(svc *rolesMock.Roles) { + svc.On("Get", mock.Anything, "sp", "1").Return(nil, errors.New("some error")).Once() + svc.On("Create", mock.Anything, &roles.Role{ID: "1", SpaceID: "sp", Environments: []string{"env"}, Description: "test"}).Return(nil, errors.New("failed to install")).Once() + }, + wantErr: func(t *testing.T, err error) { + assert.Error(t, err) + assert.EqualError(t, err, "failed to install role: some error") + assert.Contains(t, errors.GetDetail(err), "Возникла ошибка РїСЂРё настройке роли 1(test)") + }, + }, + { + name: "Install multiple roles success", + roles: []*roles.Role{ + {ID: "1", SpaceID: "sp", Environments: []string{"env"}, Description: "test"}, + {ID: "2", SpaceID: "sp", Environments: []string{"env"}, Description: "test"}}, + rolesCall: func(svc *rolesMock.Roles) { + svc.On("Get", mock.Anything, "sp", "1").Return(nil, errors.New("not found")).Once() + svc.On("Get", mock.Anything, "sp", "2").Return(nil, errors.New("not found")).Once() + svc.On("Create", mock.Anything, &roles.Role{ID: "1", SpaceID: "sp", Environments: []string{"env"}, Description: "test"}).Return(&roles.Role{ID: "1", SpaceID: "sp", Environments: []string{"env"}, Description: "test"}, nil).Once() + svc.On("Create", mock.Anything, &roles.Role{ID: "2", SpaceID: "sp", Environments: []string{"env"}, Description: "test"}).Return(&roles.Role{ID: "2", SpaceID: "sp", Environments: []string{"env"}, Description: "test"}, nil).Once() + }, + wantErr: func(t *testing.T, err error) { + assert.NoError(t, err) + }, + }, + { + name: "Install multiple roles fails", + roles: []*roles.Role{{ID: "1", SpaceID: "sp", Environments: []string{"env"}, Description: "test"}, {ID: "2", SpaceID: "sp", Environments: []string{"env"}}}, + rolesCall: func(svc *rolesMock.Roles) { + svc.On("Get", mock.Anything, "sp", "1").Return(nil, errors.New("some error")).Once() + svc.On("Get", mock.Anything, "sp", "2").Return(nil, errors.New("some error")).Once() + svc.On("Create", mock.Anything, &roles.Role{ID: "1", SpaceID: "sp", Environments: []string{"env"}, Description: "test"}).Return(nil, errors.New("failed to install")).Once() + }, + wantErr: func(t *testing.T, err error) { + assert.Error(t, err) + assert.EqualError(t, err, "failed to install role: some error") + assert.Contains(t, errors.GetDetail(err), "Возникла ошибка РїСЂРё настройке роли 1(test)") + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &rolesMock.Roles{} + if tt.rolesCall != nil { + tt.rolesCall(r) + } + + s := NewSetup(&content.Content{Roles: r}, "sp", "env", nil) + s.AddRoles(tt.roles) + tt.wantErr(t, s.InstallRoles(context.Background())) + }) + } +} diff --git a/pkg/setup/setup.go b/pkg/setup/setup.go new file mode 100644 index 0000000000000000000000000000000000000000..6360eadee88a9cc2345e140f7acf5829e0fc9f26 --- /dev/null +++ b/pkg/setup/setup.go @@ -0,0 +1,188 @@ +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)) + + if logger == nil { + logger = zap.NewNop() + } + + 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..979e29053bdb5316495a409aedd079afe34cb711 --- /dev/null +++ b/pkg/setup/setup_test.go @@ -0,0 +1,1219 @@ +package setup + +import ( + "context" + "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/errors" + "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{} + itmMock.On("Find", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, 0, nil). + Once() + for _, act := range getActions() { + 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{} + itms := getActions() + itmMock.On("Find", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(itms, 0, nil). + Once() + itmMock.On("Update", mock.Anything, mock.Anything). + Return(nil). + Times(len(itms)) + itmMock.On("Publish", mock.Anything, mock.Anything). + Return(nil). + Times(len(itms)) + + 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{} + itmMock.On("Find", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, 0, nil). + Once() + for _, act := range getActions() { + 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 client") + + 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 collection") + + 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 collection") + + 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 collection") + + 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 collection") + + 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 collection") + + 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("Find", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, 0, nil). + 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 item") + + 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, mock.Anything).Run(func(args mock.Arguments) { + del := args[1].(*items.Item) + require.Equal(t, spaceID, del.SpaceID) + require.Equal(t, envID, del.EnvID) + require.Equal(t, extension.ActionsCollectionID, del.CollectionID) + require.Equal(t, act.ID, del.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 client") + + 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 collection") + + 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, mock.Anything).Run(func(args mock.Arguments) { + del := args[1].(*items.Item) + require.Equal(t, spaceID, del.SpaceID) + require.Equal(t, envID, del.EnvID) + require.Equal(t, extension.ActionsCollectionID, del.CollectionID) + require.Equal(t, act.ID, del.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 item") + + 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{} + itmMock.On( + "Find", + mock.Anything, + spaceID, + envID, + extension.ActionsCollectionID, + mock.MatchedBy(func(filter *items.Filter) bool { return data.Contains("act", filter.ID) }), + mock.MatchedBy(func(opt *items.FindOptions) bool { return opt.Regular && opt.Hidden && opt.Templates }), + ).Return(getActions(), 0, 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("Find", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, 0, nil). + 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) + //}) +}