Skip to content
Snippets Groups Projects
Commit 6ca8b34b authored by Pavel Antonov's avatar Pavel Antonov :asterisk:
Browse files

Merge branch 'feature/PRXS-1363-SetupExtsToSdk' into 'master'

Перенос Setup и Extentsion. Исправлена сортировка импортов по проекту

See merge request perxis/perxis-go!64
parents 9f5ad4e9 338f5f0a
No related branches found
No related tags found
No related merge requests found
Showing
with 1542 additions and 6 deletions
...@@ -4,6 +4,7 @@ go 1.18 ...@@ -4,6 +4,7 @@ go 1.18
require ( require (
github.com/antonmedv/expr v1.9.0 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/go-kit/kit v0.12.0
github.com/golang/protobuf v1.5.2 github.com/golang/protobuf v1.5.2
github.com/gosimple/slug v1.13.1 github.com/gosimple/slug v1.13.1
......
...@@ -5,8 +5,8 @@ ...@@ -5,8 +5,8 @@
package transportgrpc package transportgrpc
import ( import (
pb "git.perx.ru/perxis/perxis-go/proto/environments"
service "git.perx.ru/perxis/perxis-go/pkg/environments" service "git.perx.ru/perxis/perxis-go/pkg/environments"
pb "git.perx.ru/perxis/perxis-go/proto/environments"
"github.com/golang/protobuf/ptypes" "github.com/golang/protobuf/ptypes"
) )
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
package transport package transport
import ( import (
"context" "context"
"git.perx.ru/perxis/perxis-go/pkg/environments" "git.perx.ru/perxis/perxis-go/pkg/environments"
......
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,
// }
//}
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
}
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)
}
})
}
}
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
}
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
}
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
}
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
}
// 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
}
// 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
}
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
}
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
}
package service
/*
Пакет содержит инструментарий для построения расширений
Для реализации расширения сервисом нужно включение реализации расширения.
Для автоматизации регистрации расширений
реализуемых сервисом требуется включение регистратора.
```
type Service struct {
*service.Extension
*service.Registrar
}
```
*/
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
}
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()
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment