snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
This commit is contained in:
622
server/federation/service/access_control.gen.go
generated
Normal file
622
server/federation/service/access_control.gen.go
generated
Normal file
@@ -0,0 +1,622 @@
|
||||
package service
|
||||
|
||||
// This file is auto-generated.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
//
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/cortezaproject/corteza/server/federation/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/actionlog"
|
||||
internalAuth "github.com/cortezaproject/corteza/server/pkg/auth"
|
||||
"github.com/cortezaproject/corteza/server/pkg/rbac"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
systemTypes "github.com/cortezaproject/corteza/server/system/types"
|
||||
"github.com/spf13/cast"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type (
|
||||
rbacService interface {
|
||||
Can(rbac.Session, string, rbac.Resource) bool
|
||||
Trace(rbac.Session, string, rbac.Resource) *rbac.Trace
|
||||
Grant(context.Context, ...*rbac.Rule) error
|
||||
FindRulesByRoleID(roleID uint64) (rr rbac.RuleSet)
|
||||
}
|
||||
|
||||
accessControl struct {
|
||||
actionlog actionlog.Recorder
|
||||
|
||||
store store.Storer
|
||||
rbac rbacService
|
||||
}
|
||||
)
|
||||
|
||||
func AccessControl(s store.Storer) *accessControl {
|
||||
return &accessControl{
|
||||
store: s,
|
||||
rbac: rbac.Global(),
|
||||
actionlog: DefaultActionlog,
|
||||
}
|
||||
}
|
||||
|
||||
func (svc accessControl) can(ctx context.Context, op string, res rbac.Resource) bool {
|
||||
return svc.rbac.Can(rbac.ContextToSession(ctx), op, res)
|
||||
}
|
||||
|
||||
// Effective returns a list of effective permissions for all given resource
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) Effective(ctx context.Context, rr ...rbac.Resource) (ee rbac.EffectiveSet) {
|
||||
for _, res := range rr {
|
||||
r := res.RbacResource()
|
||||
for op := range rbacResourceOperations(r) {
|
||||
ee.Push(r, op, svc.can(ctx, op, res))
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Evaluate returns a list of permissions evaluated for the given user/roles combo
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) Trace(ctx context.Context, userID uint64, roles []uint64, rr ...string) (ee []*rbac.Trace, err error) {
|
||||
// Reusing the grant permission since this is who the feature is for
|
||||
if !svc.CanGrant(ctx) {
|
||||
// @todo should be altered to check grant permissions PER resource
|
||||
return nil, AccessControlErrNotAllowedToSetPermissions()
|
||||
}
|
||||
|
||||
var (
|
||||
resource rbac.Resource
|
||||
resources []rbac.Resource
|
||||
members systemTypes.RoleMemberSet
|
||||
)
|
||||
if len(rr) > 0 {
|
||||
resources = make([]rbac.Resource, 0, len(rr))
|
||||
for _, r := range rr {
|
||||
if err = rbacResourceValidator(r); err != nil {
|
||||
return nil, fmt.Errorf("can not use resource %q: %w", r, err)
|
||||
}
|
||||
|
||||
resource, err = svc.resourceLoader(ctx, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
resources = append(resources, resource)
|
||||
}
|
||||
} else {
|
||||
resources = svc.Resources()
|
||||
}
|
||||
|
||||
// User ID specified, load its roles
|
||||
if userID != 0 {
|
||||
if len(roles) > 0 {
|
||||
// should be prevented on the client
|
||||
return nil, fmt.Errorf("userID and roles are mutually exclusive")
|
||||
}
|
||||
|
||||
members, _, err = store.SearchRoleMembers(ctx, svc.store, systemTypes.RoleMemberFilter{Resource: fmt.Sprintf("corteza::system:user/%d", userID)})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, m := range members {
|
||||
roles = append(roles, m.RoleID)
|
||||
}
|
||||
|
||||
for _, r := range internalAuth.AuthenticatedRoles() {
|
||||
roles = append(roles, r.ID)
|
||||
}
|
||||
}
|
||||
|
||||
if len(roles) == 0 {
|
||||
// should be prevented on the client
|
||||
return nil, fmt.Errorf("no roles specified")
|
||||
}
|
||||
|
||||
session := rbac.ParamsToSession(ctx, userID, roles...)
|
||||
for _, res := range resources {
|
||||
r := res.RbacResource()
|
||||
for op := range rbacResourceOperations(r) {
|
||||
ee = append(ee, svc.rbac.Trace(session, op, res))
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Resources returns list of resources
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) Resources() []rbac.Resource {
|
||||
return []rbac.Resource{
|
||||
rbac.NewResource(types.NodeRbacResource(0)),
|
||||
rbac.NewResource(types.ExposedModuleRbacResource(0, 0)),
|
||||
rbac.NewResource(types.SharedModuleRbacResource(0, 0)),
|
||||
rbac.NewResource(types.ComponentRbacResource()),
|
||||
}
|
||||
}
|
||||
|
||||
// List returns list of operations on all resources
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) List() (out []map[string]string) {
|
||||
def := []map[string]string{
|
||||
{
|
||||
"type": types.NodeResourceType,
|
||||
"any": types.NodeRbacResource(0),
|
||||
"op": "manage",
|
||||
},
|
||||
{
|
||||
"type": types.NodeResourceType,
|
||||
"any": types.NodeRbacResource(0),
|
||||
"op": "module.create",
|
||||
},
|
||||
{
|
||||
"type": types.ExposedModuleResourceType,
|
||||
"any": types.ExposedModuleRbacResource(0, 0),
|
||||
"op": "manage",
|
||||
},
|
||||
{
|
||||
"type": types.SharedModuleResourceType,
|
||||
"any": types.SharedModuleRbacResource(0, 0),
|
||||
"op": "map",
|
||||
},
|
||||
{
|
||||
"type": types.ComponentResourceType,
|
||||
"any": types.ComponentRbacResource(),
|
||||
"op": "grant",
|
||||
},
|
||||
{
|
||||
"type": types.ComponentResourceType,
|
||||
"any": types.ComponentRbacResource(),
|
||||
"op": "pair",
|
||||
},
|
||||
{
|
||||
"type": types.ComponentResourceType,
|
||||
"any": types.ComponentRbacResource(),
|
||||
"op": "settings.read",
|
||||
},
|
||||
{
|
||||
"type": types.ComponentResourceType,
|
||||
"any": types.ComponentRbacResource(),
|
||||
"op": "settings.manage",
|
||||
},
|
||||
{
|
||||
"type": types.ComponentResourceType,
|
||||
"any": types.ComponentRbacResource(),
|
||||
"op": "node.create",
|
||||
},
|
||||
{
|
||||
"type": types.ComponentResourceType,
|
||||
"any": types.ComponentRbacResource(),
|
||||
"op": "nodes.search",
|
||||
},
|
||||
}
|
||||
|
||||
func(svc interface{}) {
|
||||
if svc, is := svc.(interface{}).(interface{ list() []map[string]string }); is {
|
||||
def = append(def, svc.list()...)
|
||||
}
|
||||
}(svc)
|
||||
|
||||
return def
|
||||
}
|
||||
|
||||
// Grant applies one or more RBAC rules
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) Grant(ctx context.Context, rr ...*rbac.Rule) error {
|
||||
if !svc.CanGrant(ctx) {
|
||||
// @todo should be altered to check grant permissions PER resource
|
||||
return AccessControlErrNotAllowedToSetPermissions()
|
||||
}
|
||||
|
||||
for _, r := range rr {
|
||||
err := rbacResourceValidator(r.Resource, r.Operation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := svc.rbac.Grant(ctx, rr...); err != nil {
|
||||
return AccessControlErrGeneric().Wrap(err)
|
||||
}
|
||||
|
||||
svc.logGrants(ctx, rr)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) logGrants(ctx context.Context, rr []*rbac.Rule) {
|
||||
if svc.actionlog == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, r := range rr {
|
||||
g := AccessControlActionGrant(&accessControlActionProps{r})
|
||||
g.log = r.String()
|
||||
g.resource = r.Resource
|
||||
|
||||
svc.actionlog.Record(ctx, g.ToAction())
|
||||
}
|
||||
}
|
||||
|
||||
// FindRules find all rules based on filters
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) FindRules(ctx context.Context, roleID uint64, rr ...string) (out rbac.RuleSet, err error) {
|
||||
if !svc.CanGrant(ctx) {
|
||||
return nil, AccessControlErrNotAllowedToSetPermissions()
|
||||
}
|
||||
|
||||
out, err = svc.FindRulesByRoleID(ctx, roleID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var resources []rbac.Resource
|
||||
if len(rr) > 0 {
|
||||
resources = make([]rbac.Resource, 0, len(rr))
|
||||
for _, r := range rr {
|
||||
if err = rbacResourceValidator(r); err != nil {
|
||||
return nil, fmt.Errorf("can not use resource %q: %w", r, err)
|
||||
}
|
||||
|
||||
resources = append(resources, rbac.NewResource(r))
|
||||
}
|
||||
} else {
|
||||
resources = svc.Resources()
|
||||
}
|
||||
|
||||
return out.FilterResource(resources...), nil
|
||||
}
|
||||
|
||||
// FindRulesByRoleID find all rules for a specific role
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) FindRulesByRoleID(ctx context.Context, roleID uint64) (rbac.RuleSet, error) {
|
||||
if !svc.CanGrant(ctx) {
|
||||
return nil, AccessControlErrNotAllowedToSetPermissions()
|
||||
}
|
||||
|
||||
return svc.rbac.FindRulesByRoleID(roleID), nil
|
||||
}
|
||||
|
||||
// CanManageNode checks if current user can manage federation node
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) CanManageNode(ctx context.Context, r *types.Node) bool {
|
||||
return svc.can(ctx, "manage", r)
|
||||
}
|
||||
|
||||
// CanCreateModuleOnNode checks if current user can create shared module
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) CanCreateModuleOnNode(ctx context.Context, r *types.Node) bool {
|
||||
return svc.can(ctx, "module.create", r)
|
||||
}
|
||||
|
||||
// CanManageExposedModule checks if current user can manage exposed module module
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) CanManageExposedModule(ctx context.Context, r *types.ExposedModule) bool {
|
||||
return svc.can(ctx, "manage", r)
|
||||
}
|
||||
|
||||
// CanMapSharedModule checks if current user can map shared module
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) CanMapSharedModule(ctx context.Context, r *types.SharedModule) bool {
|
||||
return svc.can(ctx, "map", r)
|
||||
}
|
||||
|
||||
// CanGrant checks if current user can manage federation permissions
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) CanGrant(ctx context.Context) bool {
|
||||
r := &types.Component{}
|
||||
return svc.can(ctx, "grant", r)
|
||||
}
|
||||
|
||||
// CanPair checks if current user can pair federation nodes
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) CanPair(ctx context.Context) bool {
|
||||
r := &types.Component{}
|
||||
return svc.can(ctx, "pair", r)
|
||||
}
|
||||
|
||||
// CanReadSettings checks if current user can read settings
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) CanReadSettings(ctx context.Context) bool {
|
||||
r := &types.Component{}
|
||||
return svc.can(ctx, "settings.read", r)
|
||||
}
|
||||
|
||||
// CanManageSettings checks if current user can manage settings
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) CanManageSettings(ctx context.Context) bool {
|
||||
r := &types.Component{}
|
||||
return svc.can(ctx, "settings.manage", r)
|
||||
}
|
||||
|
||||
// CanCreateNode checks if current user can create new federation node
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) CanCreateNode(ctx context.Context) bool {
|
||||
r := &types.Component{}
|
||||
return svc.can(ctx, "node.create", r)
|
||||
}
|
||||
|
||||
// CanSearchNodes checks if current user can list, search or filter federation nodes
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) CanSearchNodes(ctx context.Context) bool {
|
||||
r := &types.Component{}
|
||||
return svc.can(ctx, "nodes.search", r)
|
||||
}
|
||||
|
||||
// rbacResourceValidator validates known component's resource by routing it to the appropriate validator
|
||||
//
|
||||
// This function is auto-generated
|
||||
func rbacResourceValidator(r string, oo ...string) error {
|
||||
switch rbac.ResourceType(r) {
|
||||
case types.NodeResourceType:
|
||||
return rbacNodeResourceValidator(r, oo...)
|
||||
case types.ExposedModuleResourceType:
|
||||
return rbacExposedModuleResourceValidator(r, oo...)
|
||||
case types.SharedModuleResourceType:
|
||||
return rbacSharedModuleResourceValidator(r, oo...)
|
||||
case types.ComponentResourceType:
|
||||
return rbacComponentResourceValidator(r, oo...)
|
||||
}
|
||||
|
||||
return fmt.Errorf("unknown resource type %q", r)
|
||||
}
|
||||
|
||||
// resourceLoader loads resource from store
|
||||
//
|
||||
// # Notes
|
||||
// Function assumes existence of loader functions for all resource types
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) resourceLoader(ctx context.Context, resource string) (rbac.Resource, error) {
|
||||
var (
|
||||
hasWildcard = false
|
||||
resourceType, ids = rbac.ParseResourceID(resource)
|
||||
)
|
||||
|
||||
for _, id := range ids {
|
||||
if id == 0 {
|
||||
hasWildcard = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
switch rbac.ResourceType(resourceType) {
|
||||
case types.NodeResourceType:
|
||||
if hasWildcard {
|
||||
return rbac.NewResource(types.NodeRbacResource(ids[0])), nil
|
||||
}
|
||||
|
||||
return loadNode(ctx, svc.store, ids[0])
|
||||
case types.ExposedModuleResourceType:
|
||||
if hasWildcard {
|
||||
return rbac.NewResource(types.ExposedModuleRbacResource(ids[0], ids[1])), nil
|
||||
}
|
||||
|
||||
return loadExposedModule(ctx, svc.store, ids[0], ids[1])
|
||||
case types.SharedModuleResourceType:
|
||||
if hasWildcard {
|
||||
return rbac.NewResource(types.SharedModuleRbacResource(ids[0], ids[1])), nil
|
||||
}
|
||||
|
||||
return loadSharedModule(ctx, svc.store, ids[0], ids[1])
|
||||
case types.ComponentResourceType:
|
||||
return &types.Component{}, nil
|
||||
}
|
||||
|
||||
_ = ids
|
||||
return nil, fmt.Errorf("unknown resource type %q", resourceType)
|
||||
}
|
||||
|
||||
// rbacResourceOperations returns defined operations for a requested resource
|
||||
//
|
||||
// This function is auto-generated
|
||||
func rbacResourceOperations(r string) map[string]bool {
|
||||
switch rbac.ResourceType(r) {
|
||||
case types.NodeResourceType:
|
||||
return map[string]bool{
|
||||
"manage": true,
|
||||
"module.create": true,
|
||||
}
|
||||
case types.ExposedModuleResourceType:
|
||||
return map[string]bool{
|
||||
"manage": true,
|
||||
}
|
||||
case types.SharedModuleResourceType:
|
||||
return map[string]bool{
|
||||
"map": true,
|
||||
}
|
||||
case types.ComponentResourceType:
|
||||
return map[string]bool{
|
||||
"grant": true,
|
||||
"pair": true,
|
||||
"settings.read": true,
|
||||
"settings.manage": true,
|
||||
"node.create": true,
|
||||
"nodes.search": true,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// rbacNodeResourceValidator checks validity of RBAC resource and operations
|
||||
//
|
||||
// # Notes
|
||||
// Can be called without operations to check for validity of resource string only
|
||||
//
|
||||
// This function is auto-generated
|
||||
func rbacNodeResourceValidator(r string, oo ...string) error {
|
||||
if !strings.HasPrefix(r, types.NodeResourceType) {
|
||||
// expecting resource to always include path
|
||||
return fmt.Errorf("invalid resource type")
|
||||
}
|
||||
|
||||
defOps := rbacResourceOperations(r)
|
||||
for _, o := range oo {
|
||||
if !defOps[o] {
|
||||
return fmt.Errorf("invalid operation '%s' for node resource", o)
|
||||
}
|
||||
}
|
||||
|
||||
const sep = "/"
|
||||
var (
|
||||
pp = strings.Split(strings.Trim(r[len(types.NodeResourceType):], sep), sep)
|
||||
prc = []string{
|
||||
"ID",
|
||||
}
|
||||
)
|
||||
|
||||
if len(pp) != len(prc) {
|
||||
return fmt.Errorf("invalid resource path structure")
|
||||
}
|
||||
|
||||
for i := 0; i < len(pp); i++ {
|
||||
if pp[i] != "*" {
|
||||
if i > 0 && pp[i-1] == "*" {
|
||||
return fmt.Errorf("invalid path wildcard level (%d) for node resource", i)
|
||||
}
|
||||
|
||||
if _, err := cast.ToUint64E(pp[i]); err != nil {
|
||||
return fmt.Errorf("invalid reference for %s: '%s'", prc[i], pp[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// rbacExposedModuleResourceValidator checks validity of RBAC resource and operations
|
||||
//
|
||||
// # Notes
|
||||
// Can be called without operations to check for validity of resource string only
|
||||
//
|
||||
// This function is auto-generated
|
||||
func rbacExposedModuleResourceValidator(r string, oo ...string) error {
|
||||
if !strings.HasPrefix(r, types.ExposedModuleResourceType) {
|
||||
// expecting resource to always include path
|
||||
return fmt.Errorf("invalid resource type")
|
||||
}
|
||||
|
||||
defOps := rbacResourceOperations(r)
|
||||
for _, o := range oo {
|
||||
if !defOps[o] {
|
||||
return fmt.Errorf("invalid operation '%s' for exposedModule resource", o)
|
||||
}
|
||||
}
|
||||
|
||||
const sep = "/"
|
||||
var (
|
||||
pp = strings.Split(strings.Trim(r[len(types.ExposedModuleResourceType):], sep), sep)
|
||||
prc = []string{
|
||||
"NodeID",
|
||||
"ID",
|
||||
}
|
||||
)
|
||||
|
||||
if len(pp) != len(prc) {
|
||||
return fmt.Errorf("invalid resource path structure")
|
||||
}
|
||||
|
||||
for i := 0; i < len(pp); i++ {
|
||||
if pp[i] != "*" {
|
||||
if i > 0 && pp[i-1] == "*" {
|
||||
return fmt.Errorf("invalid path wildcard level (%d) for exposedModule resource", i)
|
||||
}
|
||||
|
||||
if _, err := cast.ToUint64E(pp[i]); err != nil {
|
||||
return fmt.Errorf("invalid reference for %s: '%s'", prc[i], pp[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// rbacSharedModuleResourceValidator checks validity of RBAC resource and operations
|
||||
//
|
||||
// # Notes
|
||||
// Can be called without operations to check for validity of resource string only
|
||||
//
|
||||
// This function is auto-generated
|
||||
func rbacSharedModuleResourceValidator(r string, oo ...string) error {
|
||||
if !strings.HasPrefix(r, types.SharedModuleResourceType) {
|
||||
// expecting resource to always include path
|
||||
return fmt.Errorf("invalid resource type")
|
||||
}
|
||||
|
||||
defOps := rbacResourceOperations(r)
|
||||
for _, o := range oo {
|
||||
if !defOps[o] {
|
||||
return fmt.Errorf("invalid operation '%s' for sharedModule resource", o)
|
||||
}
|
||||
}
|
||||
|
||||
const sep = "/"
|
||||
var (
|
||||
pp = strings.Split(strings.Trim(r[len(types.SharedModuleResourceType):], sep), sep)
|
||||
prc = []string{
|
||||
"NodeID",
|
||||
"ID",
|
||||
}
|
||||
)
|
||||
|
||||
if len(pp) != len(prc) {
|
||||
return fmt.Errorf("invalid resource path structure")
|
||||
}
|
||||
|
||||
for i := 0; i < len(pp); i++ {
|
||||
if pp[i] != "*" {
|
||||
if i > 0 && pp[i-1] == "*" {
|
||||
return fmt.Errorf("invalid path wildcard level (%d) for sharedModule resource", i)
|
||||
}
|
||||
|
||||
if _, err := cast.ToUint64E(pp[i]); err != nil {
|
||||
return fmt.Errorf("invalid reference for %s: '%s'", prc[i], pp[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// rbacComponentResourceValidator checks validity of RBAC resource and operations
|
||||
//
|
||||
// # Notes
|
||||
// Can be called without operations to check for validity of resource string only
|
||||
//
|
||||
// This function is auto-generated
|
||||
func rbacComponentResourceValidator(r string, oo ...string) error {
|
||||
if r != types.ComponentResourceType+"/" {
|
||||
// expecting resource to always include path
|
||||
return fmt.Errorf("invalid component resource, expecting " + types.ComponentResourceType + "/")
|
||||
}
|
||||
|
||||
defOps := rbacResourceOperations(r)
|
||||
for _, o := range oo {
|
||||
if !defOps[o] {
|
||||
return fmt.Errorf("invalid operation '%s' for federation component resource", o)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
284
server/federation/service/access_control_actions.gen.go
generated
Normal file
284
server/federation/service/access_control_actions.gen.go
generated
Normal file
@@ -0,0 +1,284 @@
|
||||
package service
|
||||
|
||||
// This file is auto-generated.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
//
|
||||
// Definitions file that controls how this file is generated:
|
||||
// federation/service/access_control_actions.yaml
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/cortezaproject/corteza/server/pkg/actionlog"
|
||||
"github.com/cortezaproject/corteza/server/pkg/errors"
|
||||
"github.com/cortezaproject/corteza/server/pkg/locale"
|
||||
"github.com/cortezaproject/corteza/server/pkg/rbac"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
accessControlActionProps struct {
|
||||
rule *rbac.Rule
|
||||
}
|
||||
|
||||
accessControlAction struct {
|
||||
timestamp time.Time
|
||||
resource string
|
||||
action string
|
||||
log string
|
||||
severity actionlog.Severity
|
||||
|
||||
// prefix for error when action fails
|
||||
errorMessage string
|
||||
|
||||
props *accessControlActionProps
|
||||
}
|
||||
|
||||
accessControlLogMetaKey struct{}
|
||||
accessControlPropsMetaKey struct{}
|
||||
)
|
||||
|
||||
var (
|
||||
// just a placeholder to cover template cases w/o fmt package use
|
||||
_ = fmt.Println
|
||||
)
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Props methods
|
||||
// setRule updates accessControlActionProps's rule
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p *accessControlActionProps) setRule(rule *rbac.Rule) *accessControlActionProps {
|
||||
p.rule = rule
|
||||
return p
|
||||
}
|
||||
|
||||
// Serialize converts accessControlActionProps to actionlog.Meta
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p accessControlActionProps) Serialize() actionlog.Meta {
|
||||
var (
|
||||
m = make(actionlog.Meta)
|
||||
)
|
||||
|
||||
if p.rule != nil {
|
||||
m.Set("rule.operation", p.rule.Operation, true)
|
||||
m.Set("rule.roleID", p.rule.RoleID, true)
|
||||
m.Set("rule.access", p.rule.Access, true)
|
||||
m.Set("rule.resource", p.rule.Resource, true)
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// tr translates string and replaces meta value placeholder with values
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p accessControlActionProps) Format(in string, err error) string {
|
||||
var (
|
||||
pairs = []string{"{{err}}"}
|
||||
// first non-empty string
|
||||
fns = func(ii ...interface{}) string {
|
||||
for _, i := range ii {
|
||||
if s := fmt.Sprintf("%v", i); len(s) > 0 {
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
pairs = append(pairs, err.Error())
|
||||
} else {
|
||||
pairs = append(pairs, "nil")
|
||||
}
|
||||
|
||||
if p.rule != nil {
|
||||
// replacement for "{{rule}}" (in order how fields are defined)
|
||||
pairs = append(
|
||||
pairs,
|
||||
"{{rule}}",
|
||||
fns(
|
||||
p.rule.Operation,
|
||||
p.rule.RoleID,
|
||||
p.rule.Access,
|
||||
p.rule.Resource,
|
||||
),
|
||||
)
|
||||
pairs = append(pairs, "{{rule.operation}}", fns(p.rule.Operation))
|
||||
pairs = append(pairs, "{{rule.roleID}}", fns(p.rule.RoleID))
|
||||
pairs = append(pairs, "{{rule.access}}", fns(p.rule.Access))
|
||||
pairs = append(pairs, "{{rule.resource}}", fns(p.rule.Resource))
|
||||
}
|
||||
return strings.NewReplacer(pairs...).Replace(in)
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Action methods
|
||||
|
||||
// String returns loggable description as string
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (a *accessControlAction) String() string {
|
||||
var props = &accessControlActionProps{}
|
||||
|
||||
if a.props != nil {
|
||||
props = a.props
|
||||
}
|
||||
|
||||
return props.Format(a.log, nil)
|
||||
}
|
||||
|
||||
func (e *accessControlAction) ToAction() *actionlog.Action {
|
||||
return &actionlog.Action{
|
||||
Resource: e.resource,
|
||||
Action: e.action,
|
||||
Severity: e.severity,
|
||||
Description: e.String(),
|
||||
Meta: e.props.Serialize(),
|
||||
}
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Action constructors
|
||||
|
||||
// AccessControlActionGrant returns "federation:access_control.grant" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func AccessControlActionGrant(props ...*accessControlActionProps) *accessControlAction {
|
||||
a := &accessControlAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "federation:access_control",
|
||||
action: "grant",
|
||||
log: "grant",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Error constructors
|
||||
|
||||
// AccessControlErrGeneric returns "federation:access_control.generic" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func AccessControlErrGeneric(mm ...*accessControlActionProps) *errors.Error {
|
||||
var p = &accessControlActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("failed to complete request due to internal error", nil),
|
||||
|
||||
errors.Meta("type", "generic"),
|
||||
errors.Meta("resource", "federation:access_control"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(accessControlLogMetaKey{}, "{err}"),
|
||||
errors.Meta(accessControlPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "access-control.errors.generic"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// AccessControlErrNotAllowedToSetPermissions returns "federation:access_control.notAllowedToSetPermissions" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func AccessControlErrNotAllowedToSetPermissions(mm ...*accessControlActionProps) *errors.Error {
|
||||
var p = &accessControlActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("not allowed to set permissions", nil),
|
||||
|
||||
errors.Meta("type", "notAllowedToSetPermissions"),
|
||||
errors.Meta("resource", "federation:access_control"),
|
||||
|
||||
errors.Meta(accessControlPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "access-control.errors.notAllowedToSetPermissions"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
|
||||
// recordAction is a service helper function wraps function that can return error
|
||||
//
|
||||
// It will wrap unrecognized/internal errors with generic errors.
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (svc accessControl) recordAction(ctx context.Context, props *accessControlActionProps, actionFn func(...*accessControlActionProps) *accessControlAction, err error) error {
|
||||
if svc.actionlog == nil || actionFn == nil {
|
||||
// action log disabled or no action fn passed, return error as-is
|
||||
return err
|
||||
} else if err == nil {
|
||||
// action completed w/o error, record it
|
||||
svc.actionlog.Record(ctx, actionFn(props).ToAction())
|
||||
return nil
|
||||
}
|
||||
|
||||
a := actionFn(props).ToAction()
|
||||
|
||||
// Extracting error information and recording it as action
|
||||
a.Error = err.Error()
|
||||
|
||||
switch c := err.(type) {
|
||||
case *errors.Error:
|
||||
m := c.Meta()
|
||||
|
||||
a.Error = err.Error()
|
||||
a.Severity = actionlog.Severity(m.AsInt("severity"))
|
||||
a.Description = props.Format(m.AsString(accessControlLogMetaKey{}), err)
|
||||
|
||||
if p, has := m[accessControlPropsMetaKey{}]; has {
|
||||
a.Meta = p.(*accessControlActionProps).Serialize()
|
||||
}
|
||||
|
||||
svc.actionlog.Record(ctx, a)
|
||||
default:
|
||||
svc.actionlog.Record(ctx, a)
|
||||
}
|
||||
|
||||
// Original error is passed on
|
||||
return err
|
||||
}
|
||||
25
server/federation/service/access_control_actions.yaml
Normal file
25
server/federation/service/access_control_actions.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
# List of security/audit events and errors that we need to log
|
||||
|
||||
resource: federation:access_control
|
||||
service: accessControl
|
||||
|
||||
# Default sensitivity for actions
|
||||
defaultActionSeverity: notice
|
||||
|
||||
# default severity for errors
|
||||
defaultErrorSeverity: alert
|
||||
|
||||
import:
|
||||
- github.com/cortezaproject/corteza/server/pkg/rbac
|
||||
|
||||
props:
|
||||
- name: rule
|
||||
type: "*rbac.Rule"
|
||||
fields: [ operation, roleID, access, resource ]
|
||||
|
||||
actions:
|
||||
- action: grant
|
||||
|
||||
errors:
|
||||
- error: notAllowedToSetPermissions
|
||||
message: "not allowed to set permissions"
|
||||
11
server/federation/service/decoder/compose_module.go
Normal file
11
server/federation/service/decoder/compose_module.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package decoder
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
)
|
||||
|
||||
type (
|
||||
ComposeModule struct {
|
||||
types.Module
|
||||
}
|
||||
)
|
||||
16
server/federation/service/decoder/compose_record.go
Normal file
16
server/federation/service/decoder/compose_record.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package decoder
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
)
|
||||
|
||||
type (
|
||||
ComposeRecord struct {
|
||||
types.Record
|
||||
}
|
||||
ComposeRecordSet []*ComposeRecord
|
||||
|
||||
ComposeRecordFilter struct {
|
||||
types.RecordFilter
|
||||
}
|
||||
)
|
||||
105
server/federation/service/decoder/decoder.go
Normal file
105
server/federation/service/decoder/decoder.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package decoder
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
ftypes "github.com/cortezaproject/corteza/server/federation/types"
|
||||
)
|
||||
|
||||
type (
|
||||
ExposedRecord struct {
|
||||
ID uint64 `json:"recordID,string"`
|
||||
Values types.RecordValueSet `json:"values"`
|
||||
|
||||
CreatedAt time.Time `json:"createdAt,omitempty"`
|
||||
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
|
||||
DeletedAt *time.Time `json:"deletedAt,omitempty"`
|
||||
}
|
||||
|
||||
// Bits for decoding structure sync
|
||||
ModuleDocument struct {
|
||||
Response ComposeModule
|
||||
}
|
||||
|
||||
ExposedModuleDocument struct {
|
||||
Response struct {
|
||||
Set ftypes.ExposedModuleSet
|
||||
}
|
||||
}
|
||||
|
||||
ExposedRecordDocument struct {
|
||||
Response struct {
|
||||
Set []*ExposedRecord
|
||||
}
|
||||
}
|
||||
|
||||
ExposedModule struct {
|
||||
ftypes.ExposedModule
|
||||
}
|
||||
|
||||
// Bits for decoding data sync
|
||||
ComposeRecordResponse struct {
|
||||
Filter ComposeRecordFilter `json:",omitempty"`
|
||||
Set ComposeRecordSet `json:",omitempty"`
|
||||
}
|
||||
ComposeRecordDocument struct {
|
||||
Response ComposeRecordResponse
|
||||
}
|
||||
)
|
||||
|
||||
// DecodeModuleSync decodes the response from the structure sync request.
|
||||
//
|
||||
// It returns a set of modules.
|
||||
func DecodeModuleSync(in []byte) (types.ModuleSet, error) {
|
||||
out := &ModuleDocument{}
|
||||
|
||||
err := json.Unmarshal(in, out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return types.ModuleSet{&out.Response.Module}, nil
|
||||
}
|
||||
|
||||
func DecodeFederationModuleSync(in []byte) (ftypes.ExposedModuleSet, error) {
|
||||
out := &ExposedModuleDocument{}
|
||||
|
||||
err := json.Unmarshal(in, out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return out.Response.Set, nil
|
||||
}
|
||||
|
||||
func DecodeFederationRecordSync(in []byte) ([]*ExposedRecord, error) {
|
||||
out := &ExposedRecordDocument{}
|
||||
|
||||
err := json.Unmarshal(in, out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return out.Response.Set, nil
|
||||
}
|
||||
|
||||
// DecodeRecordSync decodes the response from the data sync request.
|
||||
//
|
||||
// It returns a set of records along with the used filter.
|
||||
func DecodeRecordSync(in []byte) (types.RecordSet, *types.RecordFilter, error) {
|
||||
out := &ComposeRecordDocument{}
|
||||
|
||||
err := json.Unmarshal(in, out)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
ss := make(types.RecordSet, 0, len(out.Response.Set))
|
||||
for _, r := range out.Response.Set {
|
||||
ss = append(ss, &r.Record)
|
||||
}
|
||||
|
||||
return ss, &out.Response.Filter.RecordFilter, nil
|
||||
}
|
||||
341
server/federation/service/exposed_module.go
Normal file
341
server/federation/service/exposed_module.go
Normal file
@@ -0,0 +1,341 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/errors"
|
||||
|
||||
cs "github.com/cortezaproject/corteza/server/compose/service"
|
||||
ct "github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/federation/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/actionlog"
|
||||
"github.com/cortezaproject/corteza/server/pkg/auth"
|
||||
"github.com/cortezaproject/corteza/server/pkg/rbac"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
ss "github.com/cortezaproject/corteza/server/system/service"
|
||||
st "github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
exposedModule struct {
|
||||
node node
|
||||
ac exposedModuleAccessController
|
||||
module cs.ModuleService
|
||||
namespace cs.NamespaceService
|
||||
role ss.RoleService
|
||||
store store.Storer
|
||||
actionlog actionlog.Recorder
|
||||
}
|
||||
|
||||
exposedModuleAccessController interface {
|
||||
CanCreateModuleOnNode(ctx context.Context, r *types.Node) bool
|
||||
CanManageExposedModule(ctx context.Context, r *types.ExposedModule) bool
|
||||
}
|
||||
|
||||
ExposedModuleService interface {
|
||||
Create(ctx context.Context, new *types.ExposedModule) (*types.ExposedModule, error)
|
||||
Update(ctx context.Context, updated *types.ExposedModule) (*types.ExposedModule, error)
|
||||
Find(ctx context.Context, filter types.ExposedModuleFilter) (types.ExposedModuleSet, types.ExposedModuleFilter, error)
|
||||
FindByID(ctx context.Context, nodeID uint64, moduleID uint64) (*types.ExposedModule, error)
|
||||
DeleteByID(ctx context.Context, nodeID, moduleID uint64) (*types.ExposedModule, error)
|
||||
}
|
||||
|
||||
moduleUpdateHandler func(ctx context.Context, ns *types.Node, c *types.ExposedModule) (bool, bool, error)
|
||||
)
|
||||
|
||||
func ExposedModule() *exposedModule {
|
||||
return &exposedModule{
|
||||
ac: DefaultAccessControl,
|
||||
node: *DefaultNode,
|
||||
module: cs.DefaultModule,
|
||||
role: ss.DefaultRole,
|
||||
namespace: cs.DefaultNamespace,
|
||||
store: DefaultStore,
|
||||
actionlog: DefaultActionlog,
|
||||
}
|
||||
}
|
||||
|
||||
// FindByAny tries to find module in a particular namespace by id, handle or name
|
||||
func (svc exposedModule) FindByAny(ctx context.Context, nodeID uint64, identifier interface{}) (m *types.ExposedModule, err error) {
|
||||
if ID, ok := identifier.(uint64); ok {
|
||||
m, err = svc.FindByID(ctx, nodeID, ID)
|
||||
} else if strIdentifier, ok := identifier.(string); ok {
|
||||
if ID, _ := strconv.ParseUint(strIdentifier, 10, 64); ID > 0 {
|
||||
m, err = svc.FindByID(ctx, nodeID, ID)
|
||||
}
|
||||
} else {
|
||||
// force invalid ID error
|
||||
// we do that to wrap error with lookup action context
|
||||
_, err = svc.FindByID(ctx, nodeID, 0)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (svc exposedModule) FindByID(ctx context.Context, nodeID uint64, moduleID uint64) (module *types.ExposedModule, err error) {
|
||||
err = func() error {
|
||||
if module, err = loadExposedModule(ctx, svc.store, nodeID, moduleID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !svc.ac.CanManageExposedModule(ctx, module) {
|
||||
return ExposedModuleErrNotAllowedToManage()
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
return module, err
|
||||
}
|
||||
|
||||
func (svc exposedModule) Update(ctx context.Context, updated *types.ExposedModule) (*types.ExposedModule, error) {
|
||||
var (
|
||||
aProps = &exposedModuleActionProps{update: updated}
|
||||
)
|
||||
|
||||
err := store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
|
||||
var (
|
||||
m *ct.Module
|
||||
node *types.Node
|
||||
old *types.ExposedModule
|
||||
)
|
||||
|
||||
if node, err = svc.node.FindByID(ctx, updated.NodeID); err != nil {
|
||||
return ExposedModuleErrNodeNotFound()
|
||||
}
|
||||
|
||||
if !svc.ac.CanManageExposedModule(ctx, updated) {
|
||||
return ExposedModuleErrNotAllowedToManage()
|
||||
}
|
||||
|
||||
if _, err := svc.namespace.FindByID(ctx, updated.ComposeNamespaceID); err != nil {
|
||||
return ExposedModuleErrComposeNamespaceNotFound()
|
||||
}
|
||||
|
||||
if m, err = svc.module.FindByID(ctx, updated.ComposeNamespaceID, updated.ComposeModuleID); err != nil {
|
||||
return ExposedModuleErrComposeModuleNotFound()
|
||||
}
|
||||
|
||||
if old, err = svc.FindByID(ctx, updated.NodeID, updated.ID); err != nil {
|
||||
return ExposedModuleErrNotFound()
|
||||
}
|
||||
|
||||
updated.UpdatedAt = now()
|
||||
updated.CreatedAt = old.CreatedAt
|
||||
updated.UpdatedBy = auth.GetIdentityFromContext(ctx).Identity()
|
||||
|
||||
// set labels
|
||||
AddFederationLabel(m, "federation", node.BaseURL)
|
||||
|
||||
if _, err := svc.module.Update(ctx, m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aProps.setModule(updated)
|
||||
|
||||
if err = store.UpdateFederationExposedModule(ctx, s, updated); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return updated, svc.recordAction(ctx, aProps, ExposedModuleActionUpdate, err)
|
||||
}
|
||||
|
||||
func (svc exposedModule) updater(ctx context.Context, nodeID, moduleID uint64, action func(...*exposedModuleActionProps) *exposedModuleAction, fn moduleUpdateHandler) (*types.ExposedModule, error) {
|
||||
var (
|
||||
moduleChanged, fieldsChanged bool
|
||||
|
||||
n *types.Node
|
||||
m *types.ExposedModule
|
||||
aProps = &exposedModuleActionProps{module: &types.ExposedModule{ID: moduleID, NodeID: nodeID}}
|
||||
err error
|
||||
)
|
||||
|
||||
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
|
||||
if m, err = loadExposedModule(ctx, svc.store, nodeID, moduleID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO - handle node id also
|
||||
if moduleChanged, fieldsChanged, err = fn(ctx, n, m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = moduleChanged
|
||||
_ = fieldsChanged
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return m, svc.recordAction(ctx, aProps, action, err)
|
||||
}
|
||||
|
||||
func (svc exposedModule) DeleteByID(ctx context.Context, nodeID, moduleID uint64) (m *types.ExposedModule, err error) {
|
||||
var (
|
||||
aProps = &exposedModuleActionProps{}
|
||||
)
|
||||
|
||||
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
|
||||
var (
|
||||
m *types.ExposedModule
|
||||
)
|
||||
|
||||
if _, err = svc.node.FindByID(ctx, nodeID); err != nil {
|
||||
return ExposedModuleErrNodeNotFound()
|
||||
}
|
||||
|
||||
if m, err = svc.FindByID(ctx, nodeID, moduleID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !svc.ac.CanManageExposedModule(ctx, m) {
|
||||
return ExposedModuleErrNotAllowedToManage()
|
||||
}
|
||||
|
||||
m.DeletedAt = now()
|
||||
m.DeletedBy = auth.GetIdentityFromContext(ctx).Identity()
|
||||
|
||||
if err = store.UpdateFederationExposedModule(ctx, s, m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aProps.delete = m
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return m, svc.recordAction(ctx, aProps, ExposedModuleActionDelete, err)
|
||||
}
|
||||
|
||||
func (svc exposedModule) Find(ctx context.Context, filter types.ExposedModuleFilter) (set types.ExposedModuleSet, f types.ExposedModuleFilter, err error) {
|
||||
filter.Check = func(res *types.ExposedModule) (bool, error) {
|
||||
if !svc.ac.CanManageExposedModule(ctx, res) {
|
||||
return false, ExposedModuleErrNotAllowedToManage()
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
err = func() error {
|
||||
if set, f, err = store.SearchFederationExposedModules(ctx, svc.store, filter); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
return set, f, err
|
||||
}
|
||||
|
||||
func (svc exposedModule) Create(ctx context.Context, new *types.ExposedModule) (*types.ExposedModule, error) {
|
||||
var (
|
||||
aProps = &exposedModuleActionProps{create: new}
|
||||
)
|
||||
|
||||
err := store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
|
||||
var (
|
||||
m *ct.Module
|
||||
node *types.Node
|
||||
fedRole *st.Role
|
||||
)
|
||||
|
||||
if node, err = svc.node.FindByID(ctx, new.NodeID); err != nil {
|
||||
return ExposedModuleErrNodeNotFound()
|
||||
}
|
||||
|
||||
if !svc.ac.CanCreateModuleOnNode(ctx, node) {
|
||||
return ExposedModuleErrNotAllowedToCreate()
|
||||
}
|
||||
|
||||
if _, err := svc.namespace.FindByID(ctx, new.ComposeNamespaceID); err != nil {
|
||||
return ExposedModuleErrComposeNamespaceNotFound()
|
||||
}
|
||||
|
||||
if m, err = svc.module.FindByID(ctx, new.ComposeNamespaceID, new.ComposeModuleID); err != nil {
|
||||
return ExposedModuleErrComposeModuleNotFound()
|
||||
}
|
||||
|
||||
if err = svc.uniqueCheck(ctx, new); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctxIdentity := auth.GetIdentityFromContext(ctx)
|
||||
|
||||
new.ID = nextID()
|
||||
new.CreatedAt = *now()
|
||||
new.CreatedBy = ctxIdentity.Identity()
|
||||
|
||||
// find the federation role
|
||||
if fedRole, err = svc.role.FindByHandle(ctx, "federation"); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if fedRole != nil {
|
||||
// get first ID from role and add it as allow rule
|
||||
err = cs.DefaultAccessControl.Grant(ctx, rbac.AllowRule(fedRole.ID, ct.RecordRbacResource(m.NamespaceID, m.ID, 0), "read"))
|
||||
}
|
||||
|
||||
// set labels
|
||||
AddFederationLabel(m, "federation", node.BaseURL)
|
||||
|
||||
if _, err := svc.module.Update(ctx, m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aProps.setModule(new)
|
||||
|
||||
if err = store.CreateFederationExposedModule(ctx, s, new); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return new, svc.recordAction(ctx, aProps, ExposedModuleActionCreate, err)
|
||||
}
|
||||
|
||||
func (svc exposedModule) uniqueCheck(ctx context.Context, m *types.ExposedModule) (err error) {
|
||||
f := types.ExposedModuleFilter{
|
||||
NodeID: m.NodeID,
|
||||
ComposeModuleID: m.ComposeModuleID,
|
||||
ComposeNamespaceID: m.ComposeNamespaceID,
|
||||
}
|
||||
|
||||
set, _, err := store.SearchFederationExposedModules(ctx, svc.store, f)
|
||||
|
||||
if len(set) > 0 && err == nil {
|
||||
return ExposedModuleErrNotUnique()
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadExposedModule(ctx context.Context, s store.FederationExposedModules, nodeID, ID uint64) (res *types.ExposedModule, err error) {
|
||||
if ID == 0 || nodeID == 0 {
|
||||
return nil, SharedModuleErrInvalidID()
|
||||
}
|
||||
|
||||
if res, err = store.LookupFederationExposedModuleByID(ctx, s, ID); errors.IsNotFound(err) {
|
||||
err = SharedModuleErrNotFound()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if nodeID != res.NodeID {
|
||||
// Make sure chart belongs to the right namespace
|
||||
return nil, SharedModuleErrNotFound()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
829
server/federation/service/exposed_module_actions.gen.go
generated
Normal file
829
server/federation/service/exposed_module_actions.gen.go
generated
Normal file
@@ -0,0 +1,829 @@
|
||||
package service
|
||||
|
||||
// This file is auto-generated.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
//
|
||||
// Definitions file that controls how this file is generated:
|
||||
// federation/service/exposed_module_actions.yaml
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/cortezaproject/corteza/server/federation/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/actionlog"
|
||||
"github.com/cortezaproject/corteza/server/pkg/errors"
|
||||
"github.com/cortezaproject/corteza/server/pkg/locale"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
exposedModuleActionProps struct {
|
||||
module *types.ExposedModule
|
||||
update *types.ExposedModule
|
||||
create *types.ExposedModule
|
||||
delete *types.ExposedModule
|
||||
filter *types.ExposedModuleFilter
|
||||
node *types.Node
|
||||
}
|
||||
|
||||
exposedModuleAction struct {
|
||||
timestamp time.Time
|
||||
resource string
|
||||
action string
|
||||
log string
|
||||
severity actionlog.Severity
|
||||
|
||||
// prefix for error when action fails
|
||||
errorMessage string
|
||||
|
||||
props *exposedModuleActionProps
|
||||
}
|
||||
|
||||
exposedModuleLogMetaKey struct{}
|
||||
exposedModulePropsMetaKey struct{}
|
||||
)
|
||||
|
||||
var (
|
||||
// just a placeholder to cover template cases w/o fmt package use
|
||||
_ = fmt.Println
|
||||
)
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Props methods
|
||||
// setModule updates exposedModuleActionProps's module
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p *exposedModuleActionProps) setModule(module *types.ExposedModule) *exposedModuleActionProps {
|
||||
p.module = module
|
||||
return p
|
||||
}
|
||||
|
||||
// setUpdate updates exposedModuleActionProps's update
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p *exposedModuleActionProps) setUpdate(update *types.ExposedModule) *exposedModuleActionProps {
|
||||
p.update = update
|
||||
return p
|
||||
}
|
||||
|
||||
// setCreate updates exposedModuleActionProps's create
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p *exposedModuleActionProps) setCreate(create *types.ExposedModule) *exposedModuleActionProps {
|
||||
p.create = create
|
||||
return p
|
||||
}
|
||||
|
||||
// setDelete updates exposedModuleActionProps's delete
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p *exposedModuleActionProps) setDelete(delete *types.ExposedModule) *exposedModuleActionProps {
|
||||
p.delete = delete
|
||||
return p
|
||||
}
|
||||
|
||||
// setFilter updates exposedModuleActionProps's filter
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p *exposedModuleActionProps) setFilter(filter *types.ExposedModuleFilter) *exposedModuleActionProps {
|
||||
p.filter = filter
|
||||
return p
|
||||
}
|
||||
|
||||
// setNode updates exposedModuleActionProps's node
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p *exposedModuleActionProps) setNode(node *types.Node) *exposedModuleActionProps {
|
||||
p.node = node
|
||||
return p
|
||||
}
|
||||
|
||||
// Serialize converts exposedModuleActionProps to actionlog.Meta
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p exposedModuleActionProps) Serialize() actionlog.Meta {
|
||||
var (
|
||||
m = make(actionlog.Meta)
|
||||
)
|
||||
|
||||
if p.module != nil {
|
||||
m.Set("module.ID", p.module.ID, true)
|
||||
m.Set("module.ComposeNamespaceID", p.module.ComposeNamespaceID, true)
|
||||
m.Set("module.ComposeModuleID", p.module.ComposeModuleID, true)
|
||||
m.Set("module.NodeID", p.module.NodeID, true)
|
||||
}
|
||||
if p.update != nil {
|
||||
m.Set("update.ID", p.update.ID, true)
|
||||
m.Set("update.ComposeNamespaceID", p.update.ComposeNamespaceID, true)
|
||||
m.Set("update.ComposeModuleID", p.update.ComposeModuleID, true)
|
||||
m.Set("update.NodeID", p.update.NodeID, true)
|
||||
}
|
||||
if p.create != nil {
|
||||
m.Set("create.ID", p.create.ID, true)
|
||||
m.Set("create.ComposeNamespaceID", p.create.ComposeNamespaceID, true)
|
||||
m.Set("create.ComposeModuleID", p.create.ComposeModuleID, true)
|
||||
m.Set("create.NodeID", p.create.NodeID, true)
|
||||
}
|
||||
if p.delete != nil {
|
||||
m.Set("delete.ID", p.delete.ID, true)
|
||||
m.Set("delete.ComposeNamespaceID", p.delete.ComposeNamespaceID, true)
|
||||
m.Set("delete.ComposeModuleID", p.delete.ComposeModuleID, true)
|
||||
m.Set("delete.NodeID", p.delete.NodeID, true)
|
||||
}
|
||||
if p.filter != nil {
|
||||
m.Set("filter.query", p.filter.Query, true)
|
||||
m.Set("filter.sort", p.filter.Sort, true)
|
||||
m.Set("filter.limit", p.filter.Limit, true)
|
||||
}
|
||||
if p.node != nil {
|
||||
m.Set("node.ID", p.node.ID, true)
|
||||
m.Set("node.Name", p.node.Name, true)
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// tr translates string and replaces meta value placeholder with values
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p exposedModuleActionProps) Format(in string, err error) string {
|
||||
var (
|
||||
pairs = []string{"{{err}}"}
|
||||
// first non-empty string
|
||||
fns = func(ii ...interface{}) string {
|
||||
for _, i := range ii {
|
||||
if s := fmt.Sprintf("%v", i); len(s) > 0 {
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
pairs = append(pairs, err.Error())
|
||||
} else {
|
||||
pairs = append(pairs, "nil")
|
||||
}
|
||||
|
||||
if p.module != nil {
|
||||
// replacement for "{{module}}" (in order how fields are defined)
|
||||
pairs = append(
|
||||
pairs,
|
||||
"{{module}}",
|
||||
fns(
|
||||
p.module.ID,
|
||||
p.module.ComposeNamespaceID,
|
||||
p.module.ComposeModuleID,
|
||||
p.module.NodeID,
|
||||
),
|
||||
)
|
||||
pairs = append(pairs, "{{module.ID}}", fns(p.module.ID))
|
||||
pairs = append(pairs, "{{module.ComposeNamespaceID}}", fns(p.module.ComposeNamespaceID))
|
||||
pairs = append(pairs, "{{module.ComposeModuleID}}", fns(p.module.ComposeModuleID))
|
||||
pairs = append(pairs, "{{module.NodeID}}", fns(p.module.NodeID))
|
||||
}
|
||||
|
||||
if p.update != nil {
|
||||
// replacement for "{{update}}" (in order how fields are defined)
|
||||
pairs = append(
|
||||
pairs,
|
||||
"{{update}}",
|
||||
fns(
|
||||
p.update.ID,
|
||||
p.update.ComposeNamespaceID,
|
||||
p.update.ComposeModuleID,
|
||||
p.update.NodeID,
|
||||
),
|
||||
)
|
||||
pairs = append(pairs, "{{update.ID}}", fns(p.update.ID))
|
||||
pairs = append(pairs, "{{update.ComposeNamespaceID}}", fns(p.update.ComposeNamespaceID))
|
||||
pairs = append(pairs, "{{update.ComposeModuleID}}", fns(p.update.ComposeModuleID))
|
||||
pairs = append(pairs, "{{update.NodeID}}", fns(p.update.NodeID))
|
||||
}
|
||||
|
||||
if p.create != nil {
|
||||
// replacement for "{{create}}" (in order how fields are defined)
|
||||
pairs = append(
|
||||
pairs,
|
||||
"{{create}}",
|
||||
fns(
|
||||
p.create.ID,
|
||||
p.create.ComposeNamespaceID,
|
||||
p.create.ComposeModuleID,
|
||||
p.create.NodeID,
|
||||
),
|
||||
)
|
||||
pairs = append(pairs, "{{create.ID}}", fns(p.create.ID))
|
||||
pairs = append(pairs, "{{create.ComposeNamespaceID}}", fns(p.create.ComposeNamespaceID))
|
||||
pairs = append(pairs, "{{create.ComposeModuleID}}", fns(p.create.ComposeModuleID))
|
||||
pairs = append(pairs, "{{create.NodeID}}", fns(p.create.NodeID))
|
||||
}
|
||||
|
||||
if p.delete != nil {
|
||||
// replacement for "{{delete}}" (in order how fields are defined)
|
||||
pairs = append(
|
||||
pairs,
|
||||
"{{delete}}",
|
||||
fns(
|
||||
p.delete.ID,
|
||||
p.delete.ComposeNamespaceID,
|
||||
p.delete.ComposeModuleID,
|
||||
p.delete.NodeID,
|
||||
),
|
||||
)
|
||||
pairs = append(pairs, "{{delete.ID}}", fns(p.delete.ID))
|
||||
pairs = append(pairs, "{{delete.ComposeNamespaceID}}", fns(p.delete.ComposeNamespaceID))
|
||||
pairs = append(pairs, "{{delete.ComposeModuleID}}", fns(p.delete.ComposeModuleID))
|
||||
pairs = append(pairs, "{{delete.NodeID}}", fns(p.delete.NodeID))
|
||||
}
|
||||
|
||||
if p.filter != nil {
|
||||
// replacement for "{{filter}}" (in order how fields are defined)
|
||||
pairs = append(
|
||||
pairs,
|
||||
"{{filter}}",
|
||||
fns(
|
||||
p.filter.Query,
|
||||
p.filter.Sort,
|
||||
p.filter.Limit,
|
||||
),
|
||||
)
|
||||
pairs = append(pairs, "{{filter.query}}", fns(p.filter.Query))
|
||||
pairs = append(pairs, "{{filter.sort}}", fns(p.filter.Sort))
|
||||
pairs = append(pairs, "{{filter.limit}}", fns(p.filter.Limit))
|
||||
}
|
||||
|
||||
if p.node != nil {
|
||||
// replacement for "{{node}}" (in order how fields are defined)
|
||||
pairs = append(
|
||||
pairs,
|
||||
"{{node}}",
|
||||
fns(
|
||||
p.node.ID,
|
||||
p.node.Name,
|
||||
),
|
||||
)
|
||||
pairs = append(pairs, "{{node.ID}}", fns(p.node.ID))
|
||||
pairs = append(pairs, "{{node.Name}}", fns(p.node.Name))
|
||||
}
|
||||
return strings.NewReplacer(pairs...).Replace(in)
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Action methods
|
||||
|
||||
// String returns loggable description as string
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (a *exposedModuleAction) String() string {
|
||||
var props = &exposedModuleActionProps{}
|
||||
|
||||
if a.props != nil {
|
||||
props = a.props
|
||||
}
|
||||
|
||||
return props.Format(a.log, nil)
|
||||
}
|
||||
|
||||
func (e *exposedModuleAction) ToAction() *actionlog.Action {
|
||||
return &actionlog.Action{
|
||||
Resource: e.resource,
|
||||
Action: e.action,
|
||||
Severity: e.severity,
|
||||
Description: e.String(),
|
||||
Meta: e.props.Serialize(),
|
||||
}
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Action constructors
|
||||
|
||||
// ExposedModuleActionSearch returns "federation:exposed_module.search" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func ExposedModuleActionSearch(props ...*exposedModuleActionProps) *exposedModuleAction {
|
||||
a := &exposedModuleAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "federation:exposed_module",
|
||||
action: "search",
|
||||
log: "searched for modules",
|
||||
severity: actionlog.Info,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// ExposedModuleActionLookup returns "federation:exposed_module.lookup" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func ExposedModuleActionLookup(props ...*exposedModuleActionProps) *exposedModuleAction {
|
||||
a := &exposedModuleAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "federation:exposed_module",
|
||||
action: "lookup",
|
||||
log: "looked-up for a {{module}}",
|
||||
severity: actionlog.Info,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// ExposedModuleActionCreate returns "federation:exposed_module.create" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func ExposedModuleActionCreate(props ...*exposedModuleActionProps) *exposedModuleAction {
|
||||
a := &exposedModuleAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "federation:exposed_module",
|
||||
action: "create",
|
||||
log: "created {{module}}",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// ExposedModuleActionUpdate returns "federation:exposed_module.update" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func ExposedModuleActionUpdate(props ...*exposedModuleActionProps) *exposedModuleAction {
|
||||
a := &exposedModuleAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "federation:exposed_module",
|
||||
action: "update",
|
||||
log: "updated {{module}}",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// ExposedModuleActionDelete returns "federation:exposed_module.delete" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func ExposedModuleActionDelete(props ...*exposedModuleActionProps) *exposedModuleAction {
|
||||
a := &exposedModuleAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "federation:exposed_module",
|
||||
action: "delete",
|
||||
log: "deleted {{module}}",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// ExposedModuleActionUndelete returns "federation:exposed_module.undelete" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func ExposedModuleActionUndelete(props ...*exposedModuleActionProps) *exposedModuleAction {
|
||||
a := &exposedModuleAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "federation:exposed_module",
|
||||
action: "undelete",
|
||||
log: "undeleted {{module}}",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Error constructors
|
||||
|
||||
// ExposedModuleErrGeneric returns "federation:exposed_module.generic" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func ExposedModuleErrGeneric(mm ...*exposedModuleActionProps) *errors.Error {
|
||||
var p = &exposedModuleActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("failed to complete request due to internal error", nil),
|
||||
|
||||
errors.Meta("type", "generic"),
|
||||
errors.Meta("resource", "federation:exposed_module"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(exposedModuleLogMetaKey{}, "{err}"),
|
||||
errors.Meta(exposedModulePropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "exposed-module.errors.generic"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// ExposedModuleErrNotFound returns "federation:exposed_module.notFound" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func ExposedModuleErrNotFound(mm ...*exposedModuleActionProps) *errors.Error {
|
||||
var p = &exposedModuleActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("module does not exist", nil),
|
||||
|
||||
errors.Meta("type", "notFound"),
|
||||
errors.Meta("resource", "federation:exposed_module"),
|
||||
|
||||
errors.Meta(exposedModulePropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "exposed-module.errors.notFound"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// ExposedModuleErrInvalidID returns "federation:exposed_module.invalidID" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func ExposedModuleErrInvalidID(mm ...*exposedModuleActionProps) *errors.Error {
|
||||
var p = &exposedModuleActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("invalid ID", nil),
|
||||
|
||||
errors.Meta("type", "invalidID"),
|
||||
errors.Meta("resource", "federation:exposed_module"),
|
||||
|
||||
errors.Meta(exposedModulePropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "exposed-module.errors.invalidID"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// ExposedModuleErrStaleData returns "federation:exposed_module.staleData" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func ExposedModuleErrStaleData(mm ...*exposedModuleActionProps) *errors.Error {
|
||||
var p = &exposedModuleActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("module was modified by someone else or by a workflow after you've opened it. Please refresh to see the latest updated version", nil),
|
||||
|
||||
errors.Meta("type", "staleData"),
|
||||
errors.Meta("resource", "federation:exposed_module"),
|
||||
|
||||
errors.Meta(exposedModulePropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "exposed-module.errors.staleData"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// ExposedModuleErrNotUnique returns "federation:exposed_module.notUnique" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func ExposedModuleErrNotUnique(mm ...*exposedModuleActionProps) *errors.Error {
|
||||
var p = &exposedModuleActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("node not unique", nil),
|
||||
|
||||
errors.Meta("type", "notUnique"),
|
||||
errors.Meta("resource", "federation:exposed_module"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(exposedModuleLogMetaKey{}, "used duplicate node TODO - {{module.NodeID}} for this compose module TODO - module.rel_compose_module"),
|
||||
errors.Meta(exposedModulePropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "exposed-module.errors.notUnique"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// ExposedModuleErrNodeNotFound returns "federation:exposed_module.nodeNotFound" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func ExposedModuleErrNodeNotFound(mm ...*exposedModuleActionProps) *errors.Error {
|
||||
var p = &exposedModuleActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("node does not exist", nil),
|
||||
|
||||
errors.Meta("type", "nodeNotFound"),
|
||||
errors.Meta("resource", "federation:exposed_module"),
|
||||
|
||||
errors.Meta(exposedModulePropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "exposed-module.errors.nodeNotFound"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// ExposedModuleErrComposeModuleNotFound returns "federation:exposed_module.composeModuleNotFound" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func ExposedModuleErrComposeModuleNotFound(mm ...*exposedModuleActionProps) *errors.Error {
|
||||
var p = &exposedModuleActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("compose module not found", nil),
|
||||
|
||||
errors.Meta("type", "composeModuleNotFound"),
|
||||
errors.Meta("resource", "federation:exposed_module"),
|
||||
|
||||
errors.Meta(exposedModulePropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "exposed-module.errors.composeModuleNotFound"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// ExposedModuleErrComposeNamespaceNotFound returns "federation:exposed_module.composeNamespaceNotFound" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func ExposedModuleErrComposeNamespaceNotFound(mm ...*exposedModuleActionProps) *errors.Error {
|
||||
var p = &exposedModuleActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("compose namespace not found", nil),
|
||||
|
||||
errors.Meta("type", "composeNamespaceNotFound"),
|
||||
errors.Meta("resource", "federation:exposed_module"),
|
||||
|
||||
errors.Meta(exposedModulePropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "exposed-module.errors.composeNamespaceNotFound"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// ExposedModuleErrRequestParametersInvalid returns "federation:exposed_module.requestParametersInvalid" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func ExposedModuleErrRequestParametersInvalid(mm ...*exposedModuleActionProps) *errors.Error {
|
||||
var p = &exposedModuleActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("request parameters invalid", nil),
|
||||
|
||||
errors.Meta("type", "requestParametersInvalid"),
|
||||
errors.Meta("resource", "federation:exposed_module"),
|
||||
|
||||
errors.Meta(exposedModulePropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "exposed-module.errors.requestParametersInvalid"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// ExposedModuleErrNotAllowedToCreate returns "federation:exposed_module.notAllowedToCreate" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func ExposedModuleErrNotAllowedToCreate(mm ...*exposedModuleActionProps) *errors.Error {
|
||||
var p = &exposedModuleActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("not allowed to create modules", nil),
|
||||
|
||||
errors.Meta("type", "notAllowedToCreate"),
|
||||
errors.Meta("resource", "federation:exposed_module"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(exposedModuleLogMetaKey{}, "could not create modules; insufficient permissions"),
|
||||
errors.Meta(exposedModulePropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "exposed-module.errors.notAllowedToCreate"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// ExposedModuleErrNotAllowedToManage returns "federation:exposed_module.notAllowedToManage" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func ExposedModuleErrNotAllowedToManage(mm ...*exposedModuleActionProps) *errors.Error {
|
||||
var p = &exposedModuleActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("not allowed to manage this module", nil),
|
||||
|
||||
errors.Meta("type", "notAllowedToManage"),
|
||||
errors.Meta("resource", "federation:exposed_module"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(exposedModuleLogMetaKey{}, "could not manage {{module}}; insufficient permissions"),
|
||||
errors.Meta(exposedModulePropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "exposed-module.errors.notAllowedToManage"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
|
||||
// recordAction is a service helper function wraps function that can return error
|
||||
//
|
||||
// It will wrap unrecognized/internal errors with generic errors.
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (svc exposedModule) recordAction(ctx context.Context, props *exposedModuleActionProps, actionFn func(...*exposedModuleActionProps) *exposedModuleAction, err error) error {
|
||||
if svc.actionlog == nil || actionFn == nil {
|
||||
// action log disabled or no action fn passed, return error as-is
|
||||
return err
|
||||
} else if err == nil {
|
||||
// action completed w/o error, record it
|
||||
svc.actionlog.Record(ctx, actionFn(props).ToAction())
|
||||
return nil
|
||||
}
|
||||
|
||||
a := actionFn(props).ToAction()
|
||||
|
||||
// Extracting error information and recording it as action
|
||||
a.Error = err.Error()
|
||||
|
||||
switch c := err.(type) {
|
||||
case *errors.Error:
|
||||
m := c.Meta()
|
||||
|
||||
a.Error = err.Error()
|
||||
a.Severity = actionlog.Severity(m.AsInt("severity"))
|
||||
a.Description = props.Format(m.AsString(exposedModuleLogMetaKey{}), err)
|
||||
|
||||
if p, has := m[exposedModulePropsMetaKey{}]; has {
|
||||
a.Meta = p.(*exposedModuleActionProps).Serialize()
|
||||
}
|
||||
|
||||
svc.actionlog.Record(ctx, a)
|
||||
default:
|
||||
svc.actionlog.Record(ctx, a)
|
||||
}
|
||||
|
||||
// Original error is passed on
|
||||
return err
|
||||
}
|
||||
97
server/federation/service/exposed_module_actions.yaml
Normal file
97
server/federation/service/exposed_module_actions.yaml
Normal file
@@ -0,0 +1,97 @@
|
||||
# List of loggable service actions
|
||||
|
||||
resource: federation:exposed_module
|
||||
service: exposedModule
|
||||
|
||||
# Default sensitivity for actions
|
||||
defaultActionSeverity: notice
|
||||
|
||||
# default severity for errors
|
||||
defaultErrorSeverity: error
|
||||
|
||||
import:
|
||||
- github.com/cortezaproject/corteza/server/federation/types
|
||||
|
||||
props:
|
||||
- name: module
|
||||
type: "*types.ExposedModule"
|
||||
fields: [ ID, ComposeNamespaceID, ComposeModuleID, NodeID ]
|
||||
- name: update
|
||||
type: "*types.ExposedModule"
|
||||
fields: [ ID, ComposeNamespaceID, ComposeModuleID, NodeID ]
|
||||
- name: create
|
||||
type: "*types.ExposedModule"
|
||||
fields: [ ID, ComposeNamespaceID, ComposeModuleID, NodeID ]
|
||||
- name: delete
|
||||
type: "*types.ExposedModule"
|
||||
fields: [ ID, ComposeNamespaceID, ComposeModuleID, NodeID ]
|
||||
- name: filter
|
||||
type: "*types.ExposedModuleFilter"
|
||||
fields: [ query, sort, limit ]
|
||||
- name: node
|
||||
type: "*types.Node"
|
||||
fields: [ ID, Name ]
|
||||
|
||||
actions:
|
||||
- action: search
|
||||
log: "searched for modules"
|
||||
severity: info
|
||||
|
||||
- action: lookup
|
||||
log: "looked-up for a {{module}}"
|
||||
severity: info
|
||||
|
||||
- action: create
|
||||
log: "created {{module}}"
|
||||
|
||||
- action: update
|
||||
log: "updated {{module}}"
|
||||
|
||||
- action: delete
|
||||
log: "deleted {{module}}"
|
||||
|
||||
- action: undelete
|
||||
log: "undeleted {{module}}"
|
||||
|
||||
errors:
|
||||
- error: notFound
|
||||
message: "module does not exist"
|
||||
severity: warning
|
||||
|
||||
- error: invalidID
|
||||
message: "invalid ID"
|
||||
severity: warning
|
||||
|
||||
- error: staleData
|
||||
message: "module was modified by someone else or by a workflow after you've opened it. Please refresh to see the latest updated version"
|
||||
severity: warning
|
||||
|
||||
- error: notUnique
|
||||
message: "node not unique"
|
||||
log: "used duplicate node TODO - {{module.NodeID}} for this compose module TODO - module.rel_compose_module"
|
||||
severity: warning
|
||||
|
||||
- error: nodeNotFound
|
||||
message: "node does not exist"
|
||||
severity: warning
|
||||
|
||||
- error: composeModuleNotFound
|
||||
message: "compose module not found"
|
||||
severity: "warning"
|
||||
|
||||
- error: composeNamespaceNotFound
|
||||
message: "compose namespace not found"
|
||||
severity: "warning"
|
||||
|
||||
- error: requestParametersInvalid
|
||||
message: "request parameters invalid"
|
||||
severity: "warning"
|
||||
|
||||
- error: notAllowedToCreate
|
||||
message: "not allowed to create modules"
|
||||
log: "could not create modules; insufficient permissions"
|
||||
|
||||
- error: notAllowedToManage
|
||||
message: "not allowed to manage this module"
|
||||
log: "could not manage {{module}}; insufficient permissions"
|
||||
|
||||
54
server/federation/service/mapper.go
Normal file
54
server/federation/service/mapper.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
ct "github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/federation/types"
|
||||
)
|
||||
|
||||
type (
|
||||
Mapper struct{}
|
||||
)
|
||||
|
||||
// Merge copies the values from originating structure
|
||||
// do the destination
|
||||
//
|
||||
// mostly, there will be less mapped fields on the destination
|
||||
// side, so start looping from here
|
||||
func (m *Mapper) Merge(in *ct.RecordValueSet, out *ct.RecordValueSet, mappings *types.ModuleFieldMappingSet) {
|
||||
var match *types.ModuleFieldMapping
|
||||
|
||||
for _, destVal := range *out {
|
||||
// preset the value, since we're working with
|
||||
// a pointer to an existing structure, could be some leftovers
|
||||
destVal.Value = ""
|
||||
|
||||
// find origin field
|
||||
if match, _ = mappings.FindByName(destVal.Name, types.ModuleFieldMappingSetFindTypeDestination); match == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// use the found origin, so the mapping is correct
|
||||
for _, origVal := range *in {
|
||||
if origVal.Name == match.Origin.Name {
|
||||
destVal.Value = origVal.Value
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Prepare creates a set of Records to be used later
|
||||
// when the fields will be mapped via Merge()
|
||||
func (m *Mapper) Prepare(mappings types.ModuleFieldMappingSet) (out ct.RecordValueSet) {
|
||||
for _, mm := range mappings {
|
||||
rv := &ct.RecordValue{
|
||||
Name: mm.Destination.Name,
|
||||
Value: "",
|
||||
}
|
||||
out = append(out, rv)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
97
server/federation/service/mapper_test.go
Normal file
97
server/federation/service/mapper_test.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
ct "github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/federation/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMapper_merge(t *testing.T) {
|
||||
var (
|
||||
tcc = []struct {
|
||||
name string
|
||||
m string
|
||||
in ct.RecordValueSet
|
||||
out ct.RecordValueSet
|
||||
expect interface{}
|
||||
}{
|
||||
{
|
||||
"merge_missing_destination_field",
|
||||
`[{"origin":{"kind":"String","name":"Description","label":"Description","isMulti":false},"destination":{"kind":"String","name":"Name","label":"Description","isMulti":false}},{"origin":{"kind":"Url","name":"Facebook","label":"Facebook","isMulti":false},"destination":{"kind":"Url","name":"Fb","label":"Facebook","isMulti":false}}]`,
|
||||
ct.RecordValueSet{&ct.RecordValue{Name: "Facebook", Value: "https://fb.com/user_1"}, &ct.RecordValue{Name: "Phone", Value: "000 111 222"}, &ct.RecordValue{Name: "Twitter", Value: "https://twitter.com/@russian_bot_0"}},
|
||||
ct.RecordValueSet{&ct.RecordValue{Name: "Name", Value: ""}, &ct.RecordValue{Name: "Fb", Value: ""}},
|
||||
ct.RecordValueSet{&ct.RecordValue{Name: "Name", Value: ""}, &ct.RecordValue{Name: "Fb", Value: "https://fb.com/user_1"}},
|
||||
},
|
||||
{
|
||||
"merge_empty_origin",
|
||||
`[{"origin":{"kind":"String","name":"Description","label":"Description","isMulti":false},"destination":{"kind":"String","name":"Name","label":"Description","isMulti":false}},{"origin":{"kind":"Url","name":"Facebook","label":"Facebook","isMulti":false},"destination":{"kind":"Url","name":"Fb","label":"Facebook","isMulti":false}}]`,
|
||||
ct.RecordValueSet{},
|
||||
ct.RecordValueSet{&ct.RecordValue{Name: "Name", Value: ""}, &ct.RecordValue{Name: "Fb", Value: ""}},
|
||||
ct.RecordValueSet{&ct.RecordValue{Name: "Name", Value: ""}, &ct.RecordValue{Name: "Fb", Value: ""}},
|
||||
},
|
||||
{
|
||||
"merge_empty_destination",
|
||||
`[{"origin":{"kind":"String","name":"Description","label":"Description","isMulti":false},"destination":{"kind":"String","name":"Name","label":"Description","isMulti":false}},{"origin":{"kind":"Url","name":"Facebook","label":"Facebook","isMulti":false},"destination":{"kind":"Url","name":"Fb","label":"Facebook","isMulti":false}}]`,
|
||||
ct.RecordValueSet{&ct.RecordValue{Name: "Facebook", Value: "https://fb.com/user_1"}, &ct.RecordValue{Name: "Phone", Value: "000 111 222"}, &ct.RecordValue{Name: "Twitter", Value: "https://twitter.com/@russian_bot_0"}},
|
||||
ct.RecordValueSet{},
|
||||
ct.RecordValueSet{},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
for _, tc := range tcc {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var (
|
||||
req = require.New(t)
|
||||
mapper = &Mapper{}
|
||||
)
|
||||
|
||||
// dont catch any helper errors
|
||||
mm := &types.ModuleFieldMappingSet{}
|
||||
json.Unmarshal([]byte(tc.m), mm)
|
||||
|
||||
mapper.Merge(&tc.in, &tc.out, mm)
|
||||
req.Equal(tc.out, tc.expect)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapper_prepare(t *testing.T) {
|
||||
var (
|
||||
tcc = []struct {
|
||||
name string
|
||||
m string
|
||||
expect ct.RecordValueSet
|
||||
}{
|
||||
{
|
||||
"prepare values",
|
||||
`[
|
||||
{"origin":{"kind":"String","name":"AccountName","label":"Account Name","isMulti":false},"destination":{"kind":"String","name":"AccountName","label":"Account Name","isMulti":false}},
|
||||
{"origin":{"kind":"String","name":"Phone","label":"Phone","isMulti":false},"destination":{"kind":"String","name":"Phone","label":"Phone","isMulti":false}},
|
||||
{"origin":{"kind":"Url","name":"Twitter","label":"Twitter","isMulti":false},"destination":{"kind":"Url","name":"Twitter","label":"Twitter","isMulti":false}}
|
||||
]`,
|
||||
ct.RecordValueSet{&ct.RecordValue{Name: "AccountName", Value: ""}, &ct.RecordValue{Name: "Phone", Value: ""}, &ct.RecordValue{Name: "Twitter", Value: ""}},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
for _, tc := range tcc {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var (
|
||||
req = require.New(t)
|
||||
|
||||
mapper = &Mapper{}
|
||||
)
|
||||
|
||||
// dont catch any helper errors
|
||||
mm := &types.ModuleFieldMappingSet{}
|
||||
json.Unmarshal([]byte(tc.m), mm)
|
||||
|
||||
out := mapper.Prepare(*mm)
|
||||
req.Equal(tc.expect, out)
|
||||
})
|
||||
}
|
||||
}
|
||||
219
server/federation/service/module_mapping.go
Normal file
219
server/federation/service/module_mapping.go
Normal file
@@ -0,0 +1,219 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
cs "github.com/cortezaproject/corteza/server/compose/service"
|
||||
ct "github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/federation/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/actionlog"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
)
|
||||
|
||||
type (
|
||||
moduleMapping struct {
|
||||
store store.Storer
|
||||
node node
|
||||
ac moduleMappingAccessController
|
||||
module cs.ModuleService
|
||||
smodule SharedModuleService
|
||||
namespace cs.NamespaceService
|
||||
actionlog actionlog.Recorder
|
||||
}
|
||||
|
||||
moduleMappingAccessController interface {
|
||||
CanMapSharedModule(ctx context.Context, r *types.SharedModule) bool
|
||||
}
|
||||
|
||||
ModuleMappingService interface {
|
||||
Find(ctx context.Context, filter types.ModuleMappingFilter) (types.ModuleMappingSet, types.ModuleMappingFilter, error)
|
||||
FindByID(ctx context.Context, federationModuleID uint64) (*types.ModuleMapping, error)
|
||||
Create(ctx context.Context, new *types.ModuleMapping) (*types.ModuleMapping, error)
|
||||
Update(ctx context.Context, updated *types.ModuleMapping) (*types.ModuleMapping, error)
|
||||
}
|
||||
|
||||
moduleMappingUpdateHandler func(ctx context.Context, c *types.ModuleMapping) (bool, bool, error)
|
||||
)
|
||||
|
||||
func ModuleMapping() *moduleMapping {
|
||||
return &moduleMapping{
|
||||
ac: DefaultAccessControl,
|
||||
node: *DefaultNode,
|
||||
store: DefaultStore,
|
||||
actionlog: DefaultActionlog,
|
||||
smodule: DefaultSharedModule,
|
||||
module: cs.DefaultModule,
|
||||
namespace: cs.DefaultNamespace,
|
||||
}
|
||||
}
|
||||
|
||||
func (svc moduleMapping) FindByID(ctx context.Context, federationModuleID uint64) (mm *types.ModuleMapping, err error) {
|
||||
err = func() error {
|
||||
var (
|
||||
sm *types.SharedModule
|
||||
)
|
||||
|
||||
if mm, err = store.LookupFederationModuleMappingByFederationModuleID(ctx, svc.store, federationModuleID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// fetch shared module for access check
|
||||
if sm, err = svc.smodule.FindByID(ctx, mm.NodeID, federationModuleID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !svc.ac.CanMapSharedModule(ctx, sm) {
|
||||
return ModuleMappingErrNotAllowedToMap()
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (svc moduleMapping) Find(ctx context.Context, filter types.ModuleMappingFilter) (set types.ModuleMappingSet, f types.ModuleMappingFilter, err error) {
|
||||
// @todo - optimise this access check
|
||||
filter.Check = func(res *types.ModuleMapping) (bool, error) {
|
||||
// fetch shared module for this
|
||||
sm, err := svc.smodule.FindByID(ctx, res.NodeID, res.FederationModuleID)
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if !svc.ac.CanMapSharedModule(ctx, sm) {
|
||||
return false, ModuleMappingErrNotAllowedToMap()
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
err = func() error {
|
||||
if set, f, err = store.SearchFederationModuleMappings(ctx, svc.store, filter); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
set.Walk(func(mm *types.ModuleMapping) error {
|
||||
mm.NodeID = f.NodeID
|
||||
return nil
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (svc moduleMapping) Create(ctx context.Context, new *types.ModuleMapping) (*types.ModuleMapping, error) {
|
||||
var (
|
||||
aProps = &moduleMappingActionProps{created: new}
|
||||
)
|
||||
|
||||
err := store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
|
||||
var (
|
||||
m *ct.Module
|
||||
sm *types.SharedModule
|
||||
)
|
||||
|
||||
if _, err := svc.namespace.FindByID(ctx, new.ComposeNamespaceID); err != nil {
|
||||
return ModuleMappingErrComposeNamespaceNotFound()
|
||||
}
|
||||
|
||||
if _, err = svc.node.FindByID(ctx, new.NodeID); err != nil {
|
||||
return ModuleMappingErrNodeNotFound()
|
||||
}
|
||||
|
||||
// Check for federation module - compose.Module combo
|
||||
if err = svc.uniqueCheck(ctx, new); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if m, err = svc.module.FindByID(ctx, new.ComposeNamespaceID, new.ComposeModuleID); err != nil {
|
||||
return ModuleMappingErrComposeModuleNotFound()
|
||||
}
|
||||
|
||||
if sm, err = svc.smodule.FindByID(ctx, new.NodeID, new.FederationModuleID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !svc.ac.CanMapSharedModule(ctx, sm) {
|
||||
return ModuleMappingErrNotAllowedToMap()
|
||||
}
|
||||
|
||||
if err = store.CreateFederationModuleMapping(ctx, s, new); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// set labels
|
||||
AddFederationLabel(m, "federation", "")
|
||||
|
||||
if _, err := svc.module.Update(ctx, m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return new, svc.recordAction(ctx, aProps, ModuleMappingActionCreate, err)
|
||||
}
|
||||
|
||||
func (svc moduleMapping) Update(ctx context.Context, updated *types.ModuleMapping) (*types.ModuleMapping, error) {
|
||||
var (
|
||||
aProps = &moduleMappingActionProps{changed: updated}
|
||||
)
|
||||
|
||||
err := store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
|
||||
var (
|
||||
m *ct.Module
|
||||
sm *types.SharedModule
|
||||
)
|
||||
|
||||
if _, err := svc.namespace.FindByID(ctx, updated.ComposeNamespaceID); err != nil {
|
||||
return ModuleMappingErrComposeNamespaceNotFound()
|
||||
}
|
||||
|
||||
if m, err = svc.module.FindByID(ctx, updated.ComposeNamespaceID, updated.ComposeModuleID); err != nil {
|
||||
return ModuleMappingErrComposeModuleNotFound()
|
||||
}
|
||||
|
||||
if sm, err = svc.smodule.FindByID(ctx, updated.NodeID, updated.FederationModuleID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !svc.ac.CanMapSharedModule(ctx, sm) {
|
||||
return ModuleMappingErrNotAllowedToMap()
|
||||
}
|
||||
|
||||
if err = store.UpdateFederationModuleMapping(ctx, s, updated); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// set labels
|
||||
AddFederationLabel(m, "federation", "")
|
||||
|
||||
if _, err := svc.module.Update(ctx, m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return updated, svc.recordAction(ctx, aProps, ModuleMappingActionUpdate, err)
|
||||
}
|
||||
|
||||
func (svc moduleMapping) uniqueCheck(ctx context.Context, m *types.ModuleMapping) (err error) {
|
||||
f := types.ModuleMappingFilter{
|
||||
FederationModuleID: m.FederationModuleID,
|
||||
ComposeModuleID: m.ComposeModuleID,
|
||||
ComposeNamespaceID: m.ComposeNamespaceID,
|
||||
}
|
||||
|
||||
if set, _, err := store.SearchFederationModuleMappings(ctx, svc.store, f); len(set) > 0 && err == nil {
|
||||
return ModuleMappingErrModuleMappingExists()
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
632
server/federation/service/module_mapping_actions.gen.go
generated
Normal file
632
server/federation/service/module_mapping_actions.gen.go
generated
Normal file
@@ -0,0 +1,632 @@
|
||||
package service
|
||||
|
||||
// This file is auto-generated.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
//
|
||||
// Definitions file that controls how this file is generated:
|
||||
// federation/service/module_mapping_actions.yaml
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/cortezaproject/corteza/server/federation/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/actionlog"
|
||||
"github.com/cortezaproject/corteza/server/pkg/errors"
|
||||
"github.com/cortezaproject/corteza/server/pkg/locale"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
moduleMappingActionProps struct {
|
||||
created *types.ModuleMapping
|
||||
mapping *types.ModuleMapping
|
||||
changed *types.ModuleMapping
|
||||
filter *types.ModuleMappingFilter
|
||||
}
|
||||
|
||||
moduleMappingAction struct {
|
||||
timestamp time.Time
|
||||
resource string
|
||||
action string
|
||||
log string
|
||||
severity actionlog.Severity
|
||||
|
||||
// prefix for error when action fails
|
||||
errorMessage string
|
||||
|
||||
props *moduleMappingActionProps
|
||||
}
|
||||
|
||||
moduleMappingLogMetaKey struct{}
|
||||
moduleMappingPropsMetaKey struct{}
|
||||
)
|
||||
|
||||
var (
|
||||
// just a placeholder to cover template cases w/o fmt package use
|
||||
_ = fmt.Println
|
||||
)
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Props methods
|
||||
// setCreated updates moduleMappingActionProps's created
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p *moduleMappingActionProps) setCreated(created *types.ModuleMapping) *moduleMappingActionProps {
|
||||
p.created = created
|
||||
return p
|
||||
}
|
||||
|
||||
// setMapping updates moduleMappingActionProps's mapping
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p *moduleMappingActionProps) setMapping(mapping *types.ModuleMapping) *moduleMappingActionProps {
|
||||
p.mapping = mapping
|
||||
return p
|
||||
}
|
||||
|
||||
// setChanged updates moduleMappingActionProps's changed
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p *moduleMappingActionProps) setChanged(changed *types.ModuleMapping) *moduleMappingActionProps {
|
||||
p.changed = changed
|
||||
return p
|
||||
}
|
||||
|
||||
// setFilter updates moduleMappingActionProps's filter
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p *moduleMappingActionProps) setFilter(filter *types.ModuleMappingFilter) *moduleMappingActionProps {
|
||||
p.filter = filter
|
||||
return p
|
||||
}
|
||||
|
||||
// Serialize converts moduleMappingActionProps to actionlog.Meta
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p moduleMappingActionProps) Serialize() actionlog.Meta {
|
||||
var (
|
||||
m = make(actionlog.Meta)
|
||||
)
|
||||
|
||||
if p.created != nil {
|
||||
m.Set("created.FederationModuleID", p.created.FederationModuleID, true)
|
||||
m.Set("created.ComposeModuleID", p.created.ComposeModuleID, true)
|
||||
}
|
||||
if p.mapping != nil {
|
||||
m.Set("mapping.FederationModuleID", p.mapping.FederationModuleID, true)
|
||||
m.Set("mapping.ComposeModuleID", p.mapping.ComposeModuleID, true)
|
||||
}
|
||||
if p.changed != nil {
|
||||
m.Set("changed.FederationModuleID", p.changed.FederationModuleID, true)
|
||||
m.Set("changed.ComposeModuleID", p.changed.ComposeModuleID, true)
|
||||
}
|
||||
if p.filter != nil {
|
||||
m.Set("filter.query", p.filter.Query, true)
|
||||
m.Set("filter.sort", p.filter.Sort, true)
|
||||
m.Set("filter.limit", p.filter.Limit, true)
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// tr translates string and replaces meta value placeholder with values
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p moduleMappingActionProps) Format(in string, err error) string {
|
||||
var (
|
||||
pairs = []string{"{{err}}"}
|
||||
// first non-empty string
|
||||
fns = func(ii ...interface{}) string {
|
||||
for _, i := range ii {
|
||||
if s := fmt.Sprintf("%v", i); len(s) > 0 {
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
pairs = append(pairs, err.Error())
|
||||
} else {
|
||||
pairs = append(pairs, "nil")
|
||||
}
|
||||
|
||||
if p.created != nil {
|
||||
// replacement for "{{created}}" (in order how fields are defined)
|
||||
pairs = append(
|
||||
pairs,
|
||||
"{{created}}",
|
||||
fns(
|
||||
p.created.FederationModuleID,
|
||||
p.created.ComposeModuleID,
|
||||
),
|
||||
)
|
||||
pairs = append(pairs, "{{created.FederationModuleID}}", fns(p.created.FederationModuleID))
|
||||
pairs = append(pairs, "{{created.ComposeModuleID}}", fns(p.created.ComposeModuleID))
|
||||
}
|
||||
|
||||
if p.mapping != nil {
|
||||
// replacement for "{{mapping}}" (in order how fields are defined)
|
||||
pairs = append(
|
||||
pairs,
|
||||
"{{mapping}}",
|
||||
fns(
|
||||
p.mapping.FederationModuleID,
|
||||
p.mapping.ComposeModuleID,
|
||||
),
|
||||
)
|
||||
pairs = append(pairs, "{{mapping.FederationModuleID}}", fns(p.mapping.FederationModuleID))
|
||||
pairs = append(pairs, "{{mapping.ComposeModuleID}}", fns(p.mapping.ComposeModuleID))
|
||||
}
|
||||
|
||||
if p.changed != nil {
|
||||
// replacement for "{{changed}}" (in order how fields are defined)
|
||||
pairs = append(
|
||||
pairs,
|
||||
"{{changed}}",
|
||||
fns(
|
||||
p.changed.FederationModuleID,
|
||||
p.changed.ComposeModuleID,
|
||||
),
|
||||
)
|
||||
pairs = append(pairs, "{{changed.FederationModuleID}}", fns(p.changed.FederationModuleID))
|
||||
pairs = append(pairs, "{{changed.ComposeModuleID}}", fns(p.changed.ComposeModuleID))
|
||||
}
|
||||
|
||||
if p.filter != nil {
|
||||
// replacement for "{{filter}}" (in order how fields are defined)
|
||||
pairs = append(
|
||||
pairs,
|
||||
"{{filter}}",
|
||||
fns(
|
||||
p.filter.Query,
|
||||
p.filter.Sort,
|
||||
p.filter.Limit,
|
||||
),
|
||||
)
|
||||
pairs = append(pairs, "{{filter.query}}", fns(p.filter.Query))
|
||||
pairs = append(pairs, "{{filter.sort}}", fns(p.filter.Sort))
|
||||
pairs = append(pairs, "{{filter.limit}}", fns(p.filter.Limit))
|
||||
}
|
||||
return strings.NewReplacer(pairs...).Replace(in)
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Action methods
|
||||
|
||||
// String returns loggable description as string
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (a *moduleMappingAction) String() string {
|
||||
var props = &moduleMappingActionProps{}
|
||||
|
||||
if a.props != nil {
|
||||
props = a.props
|
||||
}
|
||||
|
||||
return props.Format(a.log, nil)
|
||||
}
|
||||
|
||||
func (e *moduleMappingAction) ToAction() *actionlog.Action {
|
||||
return &actionlog.Action{
|
||||
Resource: e.resource,
|
||||
Action: e.action,
|
||||
Severity: e.severity,
|
||||
Description: e.String(),
|
||||
Meta: e.props.Serialize(),
|
||||
}
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Action constructors
|
||||
|
||||
// ModuleMappingActionSearch returns "federation:module_mapping.search" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func ModuleMappingActionSearch(props ...*moduleMappingActionProps) *moduleMappingAction {
|
||||
a := &moduleMappingAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "federation:module_mapping",
|
||||
action: "search",
|
||||
log: "searched for modules",
|
||||
severity: actionlog.Info,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// ModuleMappingActionLookup returns "federation:module_mapping.lookup" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func ModuleMappingActionLookup(props ...*moduleMappingActionProps) *moduleMappingAction {
|
||||
a := &moduleMappingAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "federation:module_mapping",
|
||||
action: "lookup",
|
||||
log: "looked-up for a module",
|
||||
severity: actionlog.Info,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// ModuleMappingActionCreate returns "federation:module_mapping.create" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func ModuleMappingActionCreate(props ...*moduleMappingActionProps) *moduleMappingAction {
|
||||
a := &moduleMappingAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "federation:module_mapping",
|
||||
action: "create",
|
||||
log: "created module",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// ModuleMappingActionUpdate returns "federation:module_mapping.update" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func ModuleMappingActionUpdate(props ...*moduleMappingActionProps) *moduleMappingAction {
|
||||
a := &moduleMappingAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "federation:module_mapping",
|
||||
action: "update",
|
||||
log: "updated module",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// ModuleMappingActionDelete returns "federation:module_mapping.delete" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func ModuleMappingActionDelete(props ...*moduleMappingActionProps) *moduleMappingAction {
|
||||
a := &moduleMappingAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "federation:module_mapping",
|
||||
action: "delete",
|
||||
log: "deleted module",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Error constructors
|
||||
|
||||
// ModuleMappingErrGeneric returns "federation:module_mapping.generic" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func ModuleMappingErrGeneric(mm ...*moduleMappingActionProps) *errors.Error {
|
||||
var p = &moduleMappingActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("failed to complete request due to internal error", nil),
|
||||
|
||||
errors.Meta("type", "generic"),
|
||||
errors.Meta("resource", "federation:module_mapping"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(moduleMappingLogMetaKey{}, "{err}"),
|
||||
errors.Meta(moduleMappingPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "module-mapping.errors.generic"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// ModuleMappingErrNotFound returns "federation:module_mapping.notFound" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func ModuleMappingErrNotFound(mm ...*moduleMappingActionProps) *errors.Error {
|
||||
var p = &moduleMappingActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("module mapping does not exist", nil),
|
||||
|
||||
errors.Meta("type", "notFound"),
|
||||
errors.Meta("resource", "federation:module_mapping"),
|
||||
|
||||
errors.Meta(moduleMappingPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "module-mapping.errors.notFound"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// ModuleMappingErrComposeModuleNotFound returns "federation:module_mapping.composeModuleNotFound" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func ModuleMappingErrComposeModuleNotFound(mm ...*moduleMappingActionProps) *errors.Error {
|
||||
var p = &moduleMappingActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("compose module not found", nil),
|
||||
|
||||
errors.Meta("type", "composeModuleNotFound"),
|
||||
errors.Meta("resource", "federation:module_mapping"),
|
||||
|
||||
errors.Meta(moduleMappingPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "module-mapping.errors.composeModuleNotFound"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// ModuleMappingErrComposeNamespaceNotFound returns "federation:module_mapping.composeNamespaceNotFound" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func ModuleMappingErrComposeNamespaceNotFound(mm ...*moduleMappingActionProps) *errors.Error {
|
||||
var p = &moduleMappingActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("compose namespace not found", nil),
|
||||
|
||||
errors.Meta("type", "composeNamespaceNotFound"),
|
||||
errors.Meta("resource", "federation:module_mapping"),
|
||||
|
||||
errors.Meta(moduleMappingPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "module-mapping.errors.composeNamespaceNotFound"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// ModuleMappingErrFederationModuleNotFound returns "federation:module_mapping.federationModuleNotFound" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func ModuleMappingErrFederationModuleNotFound(mm ...*moduleMappingActionProps) *errors.Error {
|
||||
var p = &moduleMappingActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("federation module not found", nil),
|
||||
|
||||
errors.Meta("type", "federationModuleNotFound"),
|
||||
errors.Meta("resource", "federation:module_mapping"),
|
||||
|
||||
errors.Meta(moduleMappingPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "module-mapping.errors.federationModuleNotFound"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// ModuleMappingErrNodeNotFound returns "federation:module_mapping.nodeNotFound" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func ModuleMappingErrNodeNotFound(mm ...*moduleMappingActionProps) *errors.Error {
|
||||
var p = &moduleMappingActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("node does not exist", nil),
|
||||
|
||||
errors.Meta("type", "nodeNotFound"),
|
||||
errors.Meta("resource", "federation:module_mapping"),
|
||||
|
||||
errors.Meta(moduleMappingPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "module-mapping.errors.nodeNotFound"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// ModuleMappingErrModuleMappingExists returns "federation:module_mapping.moduleMappingExists" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func ModuleMappingErrModuleMappingExists(mm ...*moduleMappingActionProps) *errors.Error {
|
||||
var p = &moduleMappingActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("module mapping already exists", nil),
|
||||
|
||||
errors.Meta("type", "moduleMappingExists"),
|
||||
errors.Meta("resource", "federation:module_mapping"),
|
||||
|
||||
errors.Meta(moduleMappingPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "module-mapping.errors.moduleMappingExists"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// ModuleMappingErrNotAllowedToMap returns "federation:module_mapping.notAllowedToMap" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func ModuleMappingErrNotAllowedToMap(mm ...*moduleMappingActionProps) *errors.Error {
|
||||
var p = &moduleMappingActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("not allowed to map this module", nil),
|
||||
|
||||
errors.Meta("type", "notAllowedToMap"),
|
||||
errors.Meta("resource", "federation:module_mapping"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(moduleMappingLogMetaKey{}, "could not manage mapping; insufficient permissions"),
|
||||
errors.Meta(moduleMappingPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "module-mapping.errors.notAllowedToMap"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
|
||||
// recordAction is a service helper function wraps function that can return error
|
||||
//
|
||||
// It will wrap unrecognized/internal errors with generic errors.
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (svc moduleMapping) recordAction(ctx context.Context, props *moduleMappingActionProps, actionFn func(...*moduleMappingActionProps) *moduleMappingAction, err error) error {
|
||||
if svc.actionlog == nil || actionFn == nil {
|
||||
// action log disabled or no action fn passed, return error as-is
|
||||
return err
|
||||
} else if err == nil {
|
||||
// action completed w/o error, record it
|
||||
svc.actionlog.Record(ctx, actionFn(props).ToAction())
|
||||
return nil
|
||||
}
|
||||
|
||||
a := actionFn(props).ToAction()
|
||||
|
||||
// Extracting error information and recording it as action
|
||||
a.Error = err.Error()
|
||||
|
||||
switch c := err.(type) {
|
||||
case *errors.Error:
|
||||
m := c.Meta()
|
||||
|
||||
a.Error = err.Error()
|
||||
a.Severity = actionlog.Severity(m.AsInt("severity"))
|
||||
a.Description = props.Format(m.AsString(moduleMappingLogMetaKey{}), err)
|
||||
|
||||
if p, has := m[moduleMappingPropsMetaKey{}]; has {
|
||||
a.Meta = p.(*moduleMappingActionProps).Serialize()
|
||||
}
|
||||
|
||||
svc.actionlog.Record(ctx, a)
|
||||
default:
|
||||
svc.actionlog.Record(ctx, a)
|
||||
}
|
||||
|
||||
// Original error is passed on
|
||||
return err
|
||||
}
|
||||
74
server/federation/service/module_mapping_actions.yaml
Normal file
74
server/federation/service/module_mapping_actions.yaml
Normal file
@@ -0,0 +1,74 @@
|
||||
# List of loggable service actions
|
||||
|
||||
resource: federation:module_mapping
|
||||
service: moduleMapping
|
||||
|
||||
# Default sensitivity for actions
|
||||
defaultActionSeverity: notice
|
||||
|
||||
# default severity for errors
|
||||
defaultErrorSeverity: error
|
||||
|
||||
import:
|
||||
- github.com/cortezaproject/corteza/server/federation/types
|
||||
|
||||
props:
|
||||
- name: created
|
||||
type: "*types.ModuleMapping"
|
||||
fields: [ FederationModuleID, ComposeModuleID ]
|
||||
- name: mapping
|
||||
type: "*types.ModuleMapping"
|
||||
fields: [ FederationModuleID, ComposeModuleID ]
|
||||
- name: changed
|
||||
type: "*types.ModuleMapping"
|
||||
fields: [ FederationModuleID, ComposeModuleID ]
|
||||
- name: filter
|
||||
type: "*types.ModuleMappingFilter"
|
||||
fields: [ query, sort, limit ]
|
||||
|
||||
actions:
|
||||
- action: search
|
||||
log: "searched for modules"
|
||||
severity: info
|
||||
|
||||
- action: lookup
|
||||
log: "looked-up for a module"
|
||||
severity: info
|
||||
|
||||
- action: create
|
||||
log: "created module"
|
||||
|
||||
- action: update
|
||||
log: "updated module"
|
||||
|
||||
- action: delete
|
||||
log: "deleted module"
|
||||
|
||||
errors:
|
||||
- error: notFound
|
||||
message: "module mapping does not exist"
|
||||
severity: warning
|
||||
|
||||
- error: composeModuleNotFound
|
||||
message: "compose module not found"
|
||||
severity: "warning"
|
||||
|
||||
- error: composeNamespaceNotFound
|
||||
message: "compose namespace not found"
|
||||
severity: "warning"
|
||||
|
||||
- error: federationModuleNotFound
|
||||
message: "federation module not found"
|
||||
severity: "warning"
|
||||
|
||||
- error: nodeNotFound
|
||||
message: "node does not exist"
|
||||
severity: warning
|
||||
|
||||
- error: moduleMappingExists
|
||||
message: "module mapping already exists"
|
||||
# severity: "warning"
|
||||
|
||||
- error: notAllowedToMap
|
||||
message: "not allowed to map this module"
|
||||
log: "could not manage mapping; insufficient permissions"
|
||||
602
server/federation/service/node.go
Normal file
602
server/federation/service/node.go
Normal file
@@ -0,0 +1,602 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/errors"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/federation/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/actionlog"
|
||||
"github.com/cortezaproject/corteza/server/pkg/auth"
|
||||
"github.com/cortezaproject/corteza/server/pkg/options"
|
||||
"github.com/cortezaproject/corteza/server/pkg/rand"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
"github.com/cortezaproject/corteza/server/system/service"
|
||||
sysTypes "github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
const (
|
||||
TokenLength = 32
|
||||
)
|
||||
|
||||
type (
|
||||
tokenIssuer func(context.Context, auth.Identifiable) (token []byte, err error)
|
||||
|
||||
node struct {
|
||||
store store.Storer
|
||||
sysUser service.UserService
|
||||
|
||||
actionlog actionlog.Recorder
|
||||
|
||||
tokenIssuer tokenIssuer
|
||||
|
||||
name string
|
||||
host string
|
||||
baseURL string
|
||||
|
||||
handshaker nodeHandshaker
|
||||
|
||||
ac nodeAccessController
|
||||
}
|
||||
|
||||
nodeAccessController interface {
|
||||
CanPair(ctx context.Context) bool
|
||||
CanSearchNodes(ctx context.Context) bool
|
||||
CanCreateNode(ctx context.Context) bool
|
||||
CanManageNode(ctx context.Context, r *types.Node) bool
|
||||
}
|
||||
|
||||
nodeUpdateHandler func(ctx context.Context, n *types.Node) error
|
||||
|
||||
nodeHandshaker interface {
|
||||
Init(ctx context.Context, n *types.Node, authToken string) error
|
||||
Complete(ctx context.Context, n *types.Node, authToken string) error
|
||||
}
|
||||
)
|
||||
|
||||
func Node(s store.Storer, u service.UserService, al actionlog.Recorder, th tokenIssuer, options options.FederationOpt, sopt options.HttpServerOpt, ac nodeAccessController) *node {
|
||||
return &node{
|
||||
store: s,
|
||||
sysUser: u,
|
||||
actionlog: al,
|
||||
tokenIssuer: th,
|
||||
ac: ac,
|
||||
name: options.Label,
|
||||
host: options.Host,
|
||||
|
||||
baseURL: fmt.Sprintf("%s/federation", strings.TrimRight(sopt.ApiBaseUrl, "/")),
|
||||
|
||||
handshaker: HttpHandshake(http.DefaultClient),
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *node) SetHandshaker(h nodeHandshaker) {
|
||||
svc.handshaker = h
|
||||
}
|
||||
|
||||
func (svc node) Search(ctx context.Context, filter types.NodeFilter) (set types.NodeSet, f types.NodeFilter, err error) {
|
||||
var (
|
||||
aProps = &nodeActionProps{filter: &filter}
|
||||
)
|
||||
|
||||
filter.Check = func(res *types.Node) (bool, error) {
|
||||
if !svc.ac.CanManageNode(ctx, res) {
|
||||
return false, NodeErrNotAllowedToManage()
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
err = func() error {
|
||||
if !svc.ac.CanSearchNodes(ctx) {
|
||||
return NodeErrNotAllowedToSearch()
|
||||
}
|
||||
|
||||
if set, f, err = store.SearchFederationNodes(ctx, svc.store, filter); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
return set, f, svc.recordAction(ctx, aProps, NodeActionSearch, err)
|
||||
}
|
||||
|
||||
// Create is used on server A to create federation with server B
|
||||
func (svc node) Create(ctx context.Context, new *types.Node) (*types.Node, error) {
|
||||
var (
|
||||
aProps = &nodeActionProps{}
|
||||
n *types.Node
|
||||
err error
|
||||
)
|
||||
|
||||
// @todo verify new.baseURL; if it's set it must be valid URL that returns 200
|
||||
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
|
||||
n = &types.Node{
|
||||
ID: nextID(),
|
||||
Name: new.Name,
|
||||
BaseURL: new.BaseURL,
|
||||
Contact: new.Contact,
|
||||
Status: types.NodeStatusPending,
|
||||
CreatedAt: *now(),
|
||||
}
|
||||
|
||||
if !svc.ac.CanCreateNode(ctx) {
|
||||
return NodeErrNotAllowedToCreate()
|
||||
}
|
||||
|
||||
return store.CreateFederationNode(ctx, s, n)
|
||||
})
|
||||
|
||||
return n, svc.recordAction(ctx, aProps, NodeActionCreate, err)
|
||||
}
|
||||
|
||||
// Read is used mainly in UI, when retrieving details about the node
|
||||
func (svc node) Read(ctx context.Context, ID uint64) (*types.Node, error) {
|
||||
var (
|
||||
n, err = loadNode(ctx, svc.store, ID)
|
||||
aProps = &nodeActionProps{node: n}
|
||||
)
|
||||
|
||||
// permission takes precedence over any db error
|
||||
if n != nil && !svc.ac.CanManageNode(ctx, n) {
|
||||
err = NodeErrNotAllowedToManage()
|
||||
}
|
||||
|
||||
return n, svc.recordAction(ctx, aProps, NodeActionCreate, err)
|
||||
}
|
||||
|
||||
// CreateFromURI is used on server B to create federation with server A
|
||||
func (svc node) CreateFromPairingURI(ctx context.Context, uri string) (n *types.Node, err error) {
|
||||
var (
|
||||
existing *types.Node
|
||||
aProps = &nodeActionProps{pairingURI: uri}
|
||||
)
|
||||
|
||||
err = func(ctx context.Context) error {
|
||||
if !svc.ac.CanPair(ctx) {
|
||||
return NodeErrNotAllowedToPair()
|
||||
}
|
||||
|
||||
n, err = svc.decodePairingURI(uri)
|
||||
|
||||
return err
|
||||
}(ctx)
|
||||
|
||||
if err != nil {
|
||||
return nil, svc.recordAction(ctx, aProps, NodeActionCreateFromPairingURI, err)
|
||||
}
|
||||
|
||||
existing, err = store.LookupFederationNodeByBaseURLSharedNodeID(ctx, svc.store, n.BaseURL, n.SharedNodeID)
|
||||
aProps.setNode(existing)
|
||||
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
if !svc.ac.CanCreateNode(ctx) {
|
||||
return n, svc.recordAction(ctx, aProps, NodeActionCreateFromPairingURI, NodeErrNotAllowedToCreate())
|
||||
}
|
||||
|
||||
n.ID = nextID()
|
||||
n.CreatedAt = *now()
|
||||
n.UpdatedBy = auth.GetIdentityFromContext(ctx).Identity()
|
||||
n.Status = types.NodeStatusPending
|
||||
|
||||
err = store.CreateFederationNode(ctx, svc.store, n)
|
||||
return n, svc.recordAction(ctx, aProps, NodeActionCreateFromPairingURI, err)
|
||||
|
||||
} else if err != nil {
|
||||
return nil, svc.recordAction(ctx, aProps, NodeActionCreateFromPairingURI, err)
|
||||
} else {
|
||||
if !svc.ac.CanManageNode(ctx, n) {
|
||||
return n, svc.recordAction(ctx, aProps, NodeActionCreateFromPairingURI, NodeErrNotAllowedToManage())
|
||||
}
|
||||
|
||||
// Node with the same sharing ID and domain already exists
|
||||
// so we'll reset status and tokens
|
||||
|
||||
existing.Status = types.NodeStatusPending
|
||||
|
||||
// Remove existing auth token
|
||||
existing.AuthToken = ""
|
||||
|
||||
// Reset pairing token
|
||||
existing.PairToken = n.PairToken
|
||||
existing.UpdatedBy = auth.GetIdentityFromContext(ctx).Identity()
|
||||
existing.UpdatedAt = now()
|
||||
err = store.UpdateFederationNode(ctx, svc.store, n)
|
||||
return n, svc.recordAction(ctx, aProps, NodeActionRecreateFromPairingURI, err)
|
||||
}
|
||||
}
|
||||
|
||||
// RegenerateNodeURI loads and updates node with the new OTT and returns sharable link
|
||||
func (svc node) RegenerateNodeURI(ctx context.Context, nodeID uint64) (string, error) {
|
||||
var (
|
||||
uri string
|
||||
)
|
||||
|
||||
_, err := svc.updater(
|
||||
ctx,
|
||||
nodeID,
|
||||
NodeActionOttRegenerated,
|
||||
func(ctx context.Context, n *types.Node) error {
|
||||
n.PairToken = string(rand.Bytes(TokenLength))
|
||||
return nil
|
||||
},
|
||||
func(ctx context.Context, n *types.Node) error {
|
||||
uri = svc.makePairingURI(n)
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return uri, nil
|
||||
}
|
||||
|
||||
func (svc node) Update(ctx context.Context, upd *types.Node) (*types.Node, error) {
|
||||
return svc.updater(ctx, upd.ID, NodeActionUpdate, func(ctx context.Context, n *types.Node) error {
|
||||
n.Name = upd.Name
|
||||
n.BaseURL = upd.BaseURL
|
||||
n.Contact = upd.Contact
|
||||
|
||||
n.UpdatedBy = auth.GetIdentityFromContext(ctx).Identity()
|
||||
n.UpdatedAt = now()
|
||||
|
||||
return nil
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (svc node) DeleteByID(ctx context.Context, ID uint64) error {
|
||||
_, err := svc.updater(ctx, ID, NodeActionDelete, func(ctx context.Context, n *types.Node) error {
|
||||
n.DeletedAt = now()
|
||||
n.DeletedBy = auth.GetIdentityFromContext(ctx).Identity()
|
||||
|
||||
return nil
|
||||
}, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (svc node) UndeleteByID(ctx context.Context, ID uint64) error {
|
||||
_, err := svc.updater(ctx, ID, NodeActionUndelete, func(ctx context.Context, n *types.Node) error {
|
||||
n.DeletedAt = nil
|
||||
n.DeletedBy = 0
|
||||
|
||||
return nil
|
||||
}, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// Pair is used on server B to send request to server A
|
||||
func (svc node) Pair(ctx context.Context, nodeID uint64) error {
|
||||
if !svc.ac.CanPair(ctx) {
|
||||
return NodeErrNotAllowedToPair()
|
||||
}
|
||||
|
||||
// elevate permissions for user lookup & creation!
|
||||
ctx = auth.SetIdentityToContext(ctx, auth.FederationUser())
|
||||
|
||||
_, err := svc.updater(
|
||||
ctx,
|
||||
nodeID,
|
||||
NodeActionPair,
|
||||
func(ctx context.Context, n *types.Node) error {
|
||||
// Handle federated user
|
||||
u, err := svc.fetchFederatedUser(ctx, n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var accessToken []byte
|
||||
// Generate JWT token for the federated user
|
||||
accessToken, err = svc.tokenIssuer(ctx, u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.UpdatedBy = auth.GetIdentityFromContext(ctx).Identity()
|
||||
n.UpdatedAt = now()
|
||||
|
||||
// Start handshake initialization remote node
|
||||
if err = svc.handshaker.Init(ctx, n, string(accessToken)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.Status = types.NodeStatusPairRequested
|
||||
return nil
|
||||
},
|
||||
nil,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// HandshakeInit is used on server A to handle pairing request (see Pair fn above) from server B
|
||||
func (svc node) HandshakeInit(ctx context.Context, nodeID uint64, pairToken string, sharedNodeID uint64, authToken string) error {
|
||||
// if !svc.ac.CanPair(ctx) {
|
||||
// return NodeErrNotAllowedToPair()
|
||||
// }
|
||||
|
||||
_, err := svc.updater(
|
||||
ctx,
|
||||
sharedNodeID,
|
||||
NodeActionHandshakeInit,
|
||||
func(ctx context.Context, n *types.Node) error {
|
||||
// @todo need to check node status before we can proceed with initialization
|
||||
if n.PairToken != pairToken {
|
||||
return NodeErrPairingTokenInvalid()
|
||||
}
|
||||
|
||||
n.SharedNodeID = sharedNodeID
|
||||
n.AuthToken = authToken
|
||||
|
||||
n.UpdatedBy = auth.GetIdentityFromContext(ctx).Identity()
|
||||
n.UpdatedAt = now()
|
||||
|
||||
n.Status = types.NodeStatusPairRequested
|
||||
return nil
|
||||
},
|
||||
func(ctx context.Context, n *types.Node) error {
|
||||
// @todo notify the node administrator about the request
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// HandshakeConfirm is used by server A to manually confirm the handshake
|
||||
func (svc node) HandshakeConfirm(ctx context.Context, nodeID uint64) error {
|
||||
if !svc.ac.CanPair(ctx) {
|
||||
return NodeErrNotAllowedToPair()
|
||||
}
|
||||
|
||||
_, err := svc.updater(ctx, nodeID, NodeActionHandshakeConfirm, func(ctx context.Context, n *types.Node) error {
|
||||
// Handle federated user
|
||||
u, err := svc.fetchFederatedUser(ctx, n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Generate JWT token for the federated user
|
||||
var accessToken []byte
|
||||
if accessToken, err = svc.tokenIssuer(ctx, u); err != nil {
|
||||
return fmt.Errorf("could not confirm handshake: %w", err)
|
||||
}
|
||||
|
||||
n.UpdatedBy = auth.GetIdentityFromContext(ctx).Identity()
|
||||
n.UpdatedAt = now()
|
||||
|
||||
// Complete handshake on remote node
|
||||
if err = svc.handshaker.Complete(ctx, n, string(accessToken)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.Status = types.NodeStatusPaired
|
||||
return nil
|
||||
}, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// HandshakeComplete is used by server B to handle handshake confirmation
|
||||
func (svc node) HandshakeComplete(ctx context.Context, sharedNodeID uint64, token string) error {
|
||||
var (
|
||||
n *types.Node
|
||||
err error
|
||||
)
|
||||
|
||||
if !svc.ac.CanPair(ctx) {
|
||||
return NodeErrNotAllowedToPair()
|
||||
}
|
||||
|
||||
if n, err = store.LookupFederationNodeBySharedNodeID(ctx, svc.store, sharedNodeID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = svc.updater(ctx, n.ID, NodeActionHandshakeComplete, func(ctx context.Context, n *types.Node) error {
|
||||
n.AuthToken = token
|
||||
n.Status = types.NodeStatusPaired
|
||||
n.UpdatedBy = auth.GetIdentityFromContext(ctx).Identity()
|
||||
n.UpdatedAt = now()
|
||||
|
||||
return nil
|
||||
}, nil)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (svc node) updater(ctx context.Context, nodeID uint64, action func(...*nodeActionProps) *nodeAction, fn, afterFn nodeUpdateHandler) (*types.Node, error) {
|
||||
var (
|
||||
err error
|
||||
n *types.Node
|
||||
aProps = &nodeActionProps{node: &types.Node{ID: nodeID}}
|
||||
)
|
||||
|
||||
err = func() error {
|
||||
n, err = loadNode(ctx, svc.store, nodeID)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aProps.setNode(n)
|
||||
|
||||
if err = fn(ctx, n); err != nil {
|
||||
n.Status = types.NodeStatusFailed
|
||||
|
||||
// persist the node status change
|
||||
if updateErr := store.UpdateFederationNode(ctx, svc.store, n); updateErr != nil {
|
||||
return updateErr
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// any other type of access check
|
||||
// should be done in fn() before
|
||||
// if !svc.ac.CanManageNode(ctx, n) {
|
||||
// return NodeErrNotAllowedToManage()
|
||||
// }
|
||||
|
||||
if err = store.UpdateFederationNode(ctx, svc.store, n); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if afterFn != nil {
|
||||
if err = afterFn(ctx, n); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
return n, svc.recordAction(ctx, aProps, action, err)
|
||||
}
|
||||
|
||||
func (svc node) FindBySharedNodeID(ctx context.Context, sharedNodeID uint64) (*types.Node, error) {
|
||||
n, err := store.LookupFederationNodeBySharedNodeID(ctx, svc.store, sharedNodeID)
|
||||
|
||||
if n != nil && !svc.ac.CanManageNode(ctx, n) {
|
||||
return nil, NodeErrNotAllowedToManage()
|
||||
}
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (svc node) FindByID(ctx context.Context, nodeID uint64) (n *types.Node, err error) {
|
||||
if n, err = loadNode(ctx, svc.store, nodeID); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !svc.ac.CanManageNode(ctx, n) {
|
||||
return nil, NodeErrNotAllowedToManage()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Looks for existing user or crates a new one
|
||||
func (svc node) fetchFederatedUser(ctx context.Context, n *types.Node) (*sysTypes.User, error) {
|
||||
// Generate handle for user that se this node
|
||||
uHandle := fmt.Sprintf("federation_%d", n.ID)
|
||||
|
||||
u, err := svc.sysUser.FindByHandle(ctx, uHandle)
|
||||
if err == nil {
|
||||
// Reuse existing user
|
||||
return u, nil
|
||||
}
|
||||
|
||||
if service.UserErrNotFound().Is(err) {
|
||||
user := &sysTypes.User{
|
||||
Email: strconv.FormatUint(n.ID, 10) + "@federation.corteza",
|
||||
Handle: uHandle,
|
||||
}
|
||||
|
||||
AddFederationLabel(user, "federation", n.BaseURL)
|
||||
|
||||
// Create a user to service this node
|
||||
r, err := service.DefaultRole.FindByHandle(ctx, "federation")
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// context with fed. user credentials
|
||||
ctxfed := auth.SetIdentityToContext(ctx, auth.FederationUser())
|
||||
|
||||
u, err = svc.sysUser.Create(ctxfed, user)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = service.DefaultRole.MemberAdd(ctxfed, r.ID, u.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u.SetRoles(append(u.Roles(), r.ID)...)
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// decodePairingURI decodes URI (string) to federation node
|
||||
//
|
||||
// Four parts are collected from the given URI:
|
||||
// 1. node host from URI's host
|
||||
// 2. shared node ID from URI's username
|
||||
// 3. shared token from URI's password
|
||||
// 4. name of the node from query string param "name" (optional)
|
||||
func (node) decodePairingURI(uri string) (*types.Node, error) {
|
||||
var (
|
||||
n = &types.Node{}
|
||||
)
|
||||
|
||||
return n, func() error {
|
||||
parsedURI, err := url.Parse(uri)
|
||||
if err != nil {
|
||||
return NodeErrPairingURIInvalid().Wrap(err)
|
||||
}
|
||||
|
||||
n.PairToken, _ = parsedURI.User.Password()
|
||||
if len(n.PairToken) != TokenLength {
|
||||
return NodeErrPairingURITokenInvalid()
|
||||
}
|
||||
|
||||
n.SharedNodeID, err = strconv.ParseUint(parsedURI.User.Username(), 10, 64)
|
||||
if err != nil || n.SharedNodeID == 0 {
|
||||
return NodeErrPairingURISourceIDInvalid().Wrap(err)
|
||||
}
|
||||
|
||||
n.Name = parsedURI.Query().Get("name")
|
||||
n.BaseURL = fmt.Sprintf("%s://%s/%s", parsedURI.Scheme, parsedURI.Host, strings.Trim(parsedURI.Path, "/"))
|
||||
return nil
|
||||
}()
|
||||
}
|
||||
|
||||
// makePairingURI encodes details about this deployment and pairing token into sharable URI
|
||||
//
|
||||
// This URI contains info about this server (name, host, fed. api base url) and node created here
|
||||
// that is used to identify remote federation server ID with pairing token
|
||||
//
|
||||
// URI structure:
|
||||
// corteza+federation://<node ID>:<pairing token>@<this-host-where-the-api-is></path-to-federation-api>?qs-meta-data
|
||||
func (svc node) makePairingURI(n *types.Node) string {
|
||||
uri := url.URL{
|
||||
Scheme: "https",
|
||||
User: url.UserPassword(strconv.FormatUint(n.ID, 10), n.PairToken),
|
||||
Host: svc.host,
|
||||
Path: svc.baseURL,
|
||||
}
|
||||
|
||||
qs := url.Values{}
|
||||
if len(svc.name) > 0 {
|
||||
qs.Add("name", svc.name)
|
||||
}
|
||||
|
||||
if len(qs) > 0 {
|
||||
uri.RawQuery = qs.Encode()
|
||||
}
|
||||
|
||||
return uri.String()
|
||||
}
|
||||
|
||||
func loadNode(ctx context.Context, s store.FederationNodes, ID uint64) (res *types.Node, err error) {
|
||||
if ID == 0 {
|
||||
return nil, NodeErrInvalidID()
|
||||
}
|
||||
|
||||
if res, err = store.LookupFederationNodeByID(ctx, s, ID); errors.IsNotFound(err) {
|
||||
err = NodeErrNotFound()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
846
server/federation/service/node_actions.gen.go
generated
Normal file
846
server/federation/service/node_actions.gen.go
generated
Normal file
@@ -0,0 +1,846 @@
|
||||
package service
|
||||
|
||||
// This file is auto-generated.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
//
|
||||
// Definitions file that controls how this file is generated:
|
||||
// federation/service/node_actions.yaml
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/cortezaproject/corteza/server/federation/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/actionlog"
|
||||
"github.com/cortezaproject/corteza/server/pkg/errors"
|
||||
"github.com/cortezaproject/corteza/server/pkg/locale"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
nodeActionProps struct {
|
||||
node *types.Node
|
||||
pairingURI string
|
||||
filter *types.NodeFilter
|
||||
}
|
||||
|
||||
nodeAction struct {
|
||||
timestamp time.Time
|
||||
resource string
|
||||
action string
|
||||
log string
|
||||
severity actionlog.Severity
|
||||
|
||||
// prefix for error when action fails
|
||||
errorMessage string
|
||||
|
||||
props *nodeActionProps
|
||||
}
|
||||
|
||||
nodeLogMetaKey struct{}
|
||||
nodePropsMetaKey struct{}
|
||||
)
|
||||
|
||||
var (
|
||||
// just a placeholder to cover template cases w/o fmt package use
|
||||
_ = fmt.Println
|
||||
)
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Props methods
|
||||
// setNode updates nodeActionProps's node
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p *nodeActionProps) setNode(node *types.Node) *nodeActionProps {
|
||||
p.node = node
|
||||
return p
|
||||
}
|
||||
|
||||
// setPairingURI updates nodeActionProps's pairingURI
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p *nodeActionProps) setPairingURI(pairingURI string) *nodeActionProps {
|
||||
p.pairingURI = pairingURI
|
||||
return p
|
||||
}
|
||||
|
||||
// setFilter updates nodeActionProps's filter
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p *nodeActionProps) setFilter(filter *types.NodeFilter) *nodeActionProps {
|
||||
p.filter = filter
|
||||
return p
|
||||
}
|
||||
|
||||
// Serialize converts nodeActionProps to actionlog.Meta
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p nodeActionProps) Serialize() actionlog.Meta {
|
||||
var (
|
||||
m = make(actionlog.Meta)
|
||||
)
|
||||
|
||||
if p.node != nil {
|
||||
m.Set("node.Name", p.node.Name, true)
|
||||
m.Set("node.BaseURL", p.node.BaseURL, true)
|
||||
m.Set("node.ID", p.node.ID, true)
|
||||
m.Set("node.Status", p.node.Status, true)
|
||||
}
|
||||
m.Set("pairingURI", p.pairingURI, true)
|
||||
if p.filter != nil {
|
||||
m.Set("filter.query", p.filter.Query, true)
|
||||
m.Set("filter.status", p.filter.Status, true)
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// tr translates string and replaces meta value placeholder with values
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p nodeActionProps) Format(in string, err error) string {
|
||||
var (
|
||||
pairs = []string{"{{err}}"}
|
||||
// first non-empty string
|
||||
fns = func(ii ...interface{}) string {
|
||||
for _, i := range ii {
|
||||
if s := fmt.Sprintf("%v", i); len(s) > 0 {
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
pairs = append(pairs, err.Error())
|
||||
} else {
|
||||
pairs = append(pairs, "nil")
|
||||
}
|
||||
|
||||
if p.node != nil {
|
||||
// replacement for "{{node}}" (in order how fields are defined)
|
||||
pairs = append(
|
||||
pairs,
|
||||
"{{node}}",
|
||||
fns(
|
||||
p.node.Name,
|
||||
p.node.BaseURL,
|
||||
p.node.ID,
|
||||
p.node.Status,
|
||||
),
|
||||
)
|
||||
pairs = append(pairs, "{{node.Name}}", fns(p.node.Name))
|
||||
pairs = append(pairs, "{{node.BaseURL}}", fns(p.node.BaseURL))
|
||||
pairs = append(pairs, "{{node.ID}}", fns(p.node.ID))
|
||||
pairs = append(pairs, "{{node.Status}}", fns(p.node.Status))
|
||||
}
|
||||
pairs = append(pairs, "{{pairingURI}}", fns(p.pairingURI))
|
||||
|
||||
if p.filter != nil {
|
||||
// replacement for "{{filter}}" (in order how fields are defined)
|
||||
pairs = append(
|
||||
pairs,
|
||||
"{{filter}}",
|
||||
fns(
|
||||
p.filter.Query,
|
||||
p.filter.Status,
|
||||
),
|
||||
)
|
||||
pairs = append(pairs, "{{filter.query}}", fns(p.filter.Query))
|
||||
pairs = append(pairs, "{{filter.status}}", fns(p.filter.Status))
|
||||
}
|
||||
return strings.NewReplacer(pairs...).Replace(in)
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Action methods
|
||||
|
||||
// String returns loggable description as string
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (a *nodeAction) String() string {
|
||||
var props = &nodeActionProps{}
|
||||
|
||||
if a.props != nil {
|
||||
props = a.props
|
||||
}
|
||||
|
||||
return props.Format(a.log, nil)
|
||||
}
|
||||
|
||||
func (e *nodeAction) ToAction() *actionlog.Action {
|
||||
return &actionlog.Action{
|
||||
Resource: e.resource,
|
||||
Action: e.action,
|
||||
Severity: e.severity,
|
||||
Description: e.String(),
|
||||
Meta: e.props.Serialize(),
|
||||
}
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Action constructors
|
||||
|
||||
// NodeActionSearch returns "federation:node.search" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func NodeActionSearch(props ...*nodeActionProps) *nodeAction {
|
||||
a := &nodeAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "federation:node",
|
||||
action: "search",
|
||||
log: "searched for nodes",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// NodeActionLookup returns "federation:node.lookup" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func NodeActionLookup(props ...*nodeActionProps) *nodeAction {
|
||||
a := &nodeAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "federation:node",
|
||||
action: "lookup",
|
||||
log: "looked-up for a {{node}}",
|
||||
severity: actionlog.Info,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// NodeActionCreate returns "federation:node.create" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func NodeActionCreate(props ...*nodeActionProps) *nodeAction {
|
||||
a := &nodeAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "federation:node",
|
||||
action: "create",
|
||||
log: "created {{node}}",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// NodeActionCreateFromPairingURI returns "federation:node.createFromPairingURI" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func NodeActionCreateFromPairingURI(props ...*nodeActionProps) *nodeAction {
|
||||
a := &nodeAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "federation:node",
|
||||
action: "createFromPairingURI",
|
||||
log: "created {{node}} from pairing URI",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// NodeActionRecreateFromPairingURI returns "federation:node.recreateFromPairingURI" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func NodeActionRecreateFromPairingURI(props ...*nodeActionProps) *nodeAction {
|
||||
a := &nodeAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "federation:node",
|
||||
action: "recreateFromPairingURI",
|
||||
log: "recreate {{node}} from pairing URI",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// NodeActionUpdate returns "federation:node.update" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func NodeActionUpdate(props ...*nodeActionProps) *nodeAction {
|
||||
a := &nodeAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "federation:node",
|
||||
action: "update",
|
||||
log: "updated {{node}}",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// NodeActionDelete returns "federation:node.delete" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func NodeActionDelete(props ...*nodeActionProps) *nodeAction {
|
||||
a := &nodeAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "federation:node",
|
||||
action: "delete",
|
||||
log: "deleted {{node}}",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// NodeActionUndelete returns "federation:node.undelete" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func NodeActionUndelete(props ...*nodeActionProps) *nodeAction {
|
||||
a := &nodeAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "federation:node",
|
||||
action: "undelete",
|
||||
log: "undeleted {{node}}",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// NodeActionOttRegenerated returns "federation:node.ottRegenerated" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func NodeActionOttRegenerated(props ...*nodeActionProps) *nodeAction {
|
||||
a := &nodeAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "federation:node",
|
||||
action: "ottRegenerated",
|
||||
log: "regenerated one-time-token for {{node}}",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// NodeActionPair returns "federation:node.pair" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func NodeActionPair(props ...*nodeActionProps) *nodeAction {
|
||||
a := &nodeAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "federation:node",
|
||||
action: "pair",
|
||||
log: "{{node}} pairing started",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// NodeActionHandshakeInit returns "federation:node.handshakeInit" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func NodeActionHandshakeInit(props ...*nodeActionProps) *nodeAction {
|
||||
a := &nodeAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "federation:node",
|
||||
action: "handshakeInit",
|
||||
log: "{{node}} handshake initialized",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// NodeActionHandshakeConfirm returns "federation:node.handshakeConfirm" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func NodeActionHandshakeConfirm(props ...*nodeActionProps) *nodeAction {
|
||||
a := &nodeAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "federation:node",
|
||||
action: "handshakeConfirm",
|
||||
log: "{{node}} handshake confirmed",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// NodeActionHandshakeComplete returns "federation:node.handshakeComplete" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func NodeActionHandshakeComplete(props ...*nodeActionProps) *nodeAction {
|
||||
a := &nodeAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "federation:node",
|
||||
action: "handshakeComplete",
|
||||
log: "{{node}} handshake completed",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Error constructors
|
||||
|
||||
// NodeErrGeneric returns "federation:node.generic" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func NodeErrGeneric(mm ...*nodeActionProps) *errors.Error {
|
||||
var p = &nodeActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("failed to complete request due to internal error", nil),
|
||||
|
||||
errors.Meta("type", "generic"),
|
||||
errors.Meta("resource", "federation:node"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(nodeLogMetaKey{}, "{err}"),
|
||||
errors.Meta(nodePropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "node.errors.generic"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// NodeErrNotFound returns "federation:node.notFound" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func NodeErrNotFound(mm ...*nodeActionProps) *errors.Error {
|
||||
var p = &nodeActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("node does not exist", nil),
|
||||
|
||||
errors.Meta("type", "notFound"),
|
||||
errors.Meta("resource", "federation:node"),
|
||||
|
||||
errors.Meta(nodePropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "node.errors.notFound"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// NodeErrInvalidID returns "federation:node.invalidID" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func NodeErrInvalidID(mm ...*nodeActionProps) *errors.Error {
|
||||
var p = &nodeActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("invalid ID", nil),
|
||||
|
||||
errors.Meta("type", "invalidID"),
|
||||
errors.Meta("resource", "federation:node"),
|
||||
|
||||
errors.Meta(nodePropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "node.errors.invalidID"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// NodeErrPairingURIInvalid returns "federation:node.pairingURIInvalid" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func NodeErrPairingURIInvalid(mm ...*nodeActionProps) *errors.Error {
|
||||
var p = &nodeActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("pairing URI invalid: {{err}}", nil),
|
||||
|
||||
errors.Meta("type", "pairingURIInvalid"),
|
||||
errors.Meta("resource", "federation:node"),
|
||||
|
||||
errors.Meta(nodePropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "node.errors.pairingURIInvalid"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// NodeErrPairingURITokenInvalid returns "federation:node.pairingURITokenInvalid" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func NodeErrPairingURITokenInvalid(mm ...*nodeActionProps) *errors.Error {
|
||||
var p = &nodeActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("pairing URI with invalid pairing token", nil),
|
||||
|
||||
errors.Meta("type", "pairingURITokenInvalid"),
|
||||
errors.Meta("resource", "federation:node"),
|
||||
|
||||
errors.Meta(nodePropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "node.errors.pairingURITokenInvalid"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// NodeErrPairingURISourceIDInvalid returns "federation:node.pairingURISourceIDInvalid" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func NodeErrPairingURISourceIDInvalid(mm ...*nodeActionProps) *errors.Error {
|
||||
var p = &nodeActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("pairing URI without source node ID", nil),
|
||||
|
||||
errors.Meta("type", "pairingURISourceIDInvalid"),
|
||||
errors.Meta("resource", "federation:node"),
|
||||
|
||||
errors.Meta(nodePropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "node.errors.pairingURISourceIDInvalid"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// NodeErrPairingTokenInvalid returns "federation:node.pairingTokenInvalid" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func NodeErrPairingTokenInvalid(mm ...*nodeActionProps) *errors.Error {
|
||||
var p = &nodeActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("pairing token invalid", nil),
|
||||
|
||||
errors.Meta("type", "pairingTokenInvalid"),
|
||||
errors.Meta("resource", "federation:node"),
|
||||
|
||||
errors.Meta(nodePropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "node.errors.pairingTokenInvalid"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// NodeErrNotAllowedToCreate returns "federation:node.notAllowedToCreate" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func NodeErrNotAllowedToCreate(mm ...*nodeActionProps) *errors.Error {
|
||||
var p = &nodeActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("not allowed to create nodes", nil),
|
||||
|
||||
errors.Meta("type", "notAllowedToCreate"),
|
||||
errors.Meta("resource", "federation:node"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(nodeLogMetaKey{}, "could not create nodes; insufficient permissions"),
|
||||
errors.Meta(nodePropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "node.errors.notAllowedToCreate"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// NodeErrNotAllowedToSearch returns "federation:node.notAllowedToSearch" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func NodeErrNotAllowedToSearch(mm ...*nodeActionProps) *errors.Error {
|
||||
var p = &nodeActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("not allowed to search or list nodes", nil),
|
||||
|
||||
errors.Meta("type", "notAllowedToSearch"),
|
||||
errors.Meta("resource", "federation:node"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(nodeLogMetaKey{}, "could not search or list nodes; insufficient permissions"),
|
||||
errors.Meta(nodePropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "node.errors.notAllowedToSearch"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// NodeErrNotAllowedToManage returns "federation:node.notAllowedToManage" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func NodeErrNotAllowedToManage(mm ...*nodeActionProps) *errors.Error {
|
||||
var p = &nodeActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("not allowed to manage this node", nil),
|
||||
|
||||
errors.Meta("type", "notAllowedToManage"),
|
||||
errors.Meta("resource", "federation:node"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(nodeLogMetaKey{}, "could not manage {{node}}; insufficient permissions"),
|
||||
errors.Meta(nodePropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "node.errors.notAllowedToManage"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// NodeErrNotAllowedToPair returns "federation:node.notAllowedToPair" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func NodeErrNotAllowedToPair(mm ...*nodeActionProps) *errors.Error {
|
||||
var p = &nodeActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("not allowed to pair this node", nil),
|
||||
|
||||
errors.Meta("type", "notAllowedToPair"),
|
||||
errors.Meta("resource", "federation:node"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(nodeLogMetaKey{}, "could not pair {{node}}; insufficient permissions"),
|
||||
errors.Meta(nodePropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "node.errors.notAllowedToPair"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
|
||||
// recordAction is a service helper function wraps function that can return error
|
||||
//
|
||||
// It will wrap unrecognized/internal errors with generic errors.
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (svc node) recordAction(ctx context.Context, props *nodeActionProps, actionFn func(...*nodeActionProps) *nodeAction, err error) error {
|
||||
if svc.actionlog == nil || actionFn == nil {
|
||||
// action log disabled or no action fn passed, return error as-is
|
||||
return err
|
||||
} else if err == nil {
|
||||
// action completed w/o error, record it
|
||||
svc.actionlog.Record(ctx, actionFn(props).ToAction())
|
||||
return nil
|
||||
}
|
||||
|
||||
a := actionFn(props).ToAction()
|
||||
|
||||
// Extracting error information and recording it as action
|
||||
a.Error = err.Error()
|
||||
|
||||
switch c := err.(type) {
|
||||
case *errors.Error:
|
||||
m := c.Meta()
|
||||
|
||||
a.Error = err.Error()
|
||||
a.Severity = actionlog.Severity(m.AsInt("severity"))
|
||||
a.Description = props.Format(m.AsString(nodeLogMetaKey{}), err)
|
||||
|
||||
if p, has := m[nodePropsMetaKey{}]; has {
|
||||
a.Meta = p.(*nodeActionProps).Serialize()
|
||||
}
|
||||
|
||||
svc.actionlog.Record(ctx, a)
|
||||
default:
|
||||
svc.actionlog.Record(ctx, a)
|
||||
}
|
||||
|
||||
// Original error is passed on
|
||||
return err
|
||||
}
|
||||
100
server/federation/service/node_actions.yaml
Normal file
100
server/federation/service/node_actions.yaml
Normal file
@@ -0,0 +1,100 @@
|
||||
# List of loggable service actions
|
||||
|
||||
resource: federation:node
|
||||
service: node
|
||||
|
||||
# Default sensitivity for actions
|
||||
defaultActionSeverity: notice
|
||||
|
||||
# default severity for errors
|
||||
defaultErrorSeverity: error
|
||||
|
||||
import:
|
||||
- github.com/cortezaproject/corteza/server/federation/types
|
||||
|
||||
props:
|
||||
- name: node
|
||||
type: "*types.Node"
|
||||
fields: [ Name, BaseURL, ID, Status ]
|
||||
- name: pairingURI
|
||||
- name: filter
|
||||
type: "*types.NodeFilter"
|
||||
fields: [ query, status ]
|
||||
|
||||
actions:
|
||||
- action: search
|
||||
log: "searched for nodes"
|
||||
|
||||
- action: lookup
|
||||
log: "looked-up for a {{node}}"
|
||||
severity: info
|
||||
|
||||
- action: create
|
||||
log: "created {{node}}"
|
||||
|
||||
- action: createFromPairingURI
|
||||
log: "created {{node}} from pairing URI"
|
||||
|
||||
- action: recreateFromPairingURI
|
||||
log: "recreate {{node}} from pairing URI"
|
||||
|
||||
- action: update
|
||||
log: "updated {{node}}"
|
||||
|
||||
- action: delete
|
||||
log: "deleted {{node}}"
|
||||
|
||||
- action: undelete
|
||||
log: "undeleted {{node}}"
|
||||
|
||||
- action: ottRegenerated
|
||||
log: "regenerated one-time-token for {{node}}"
|
||||
|
||||
- action: pair
|
||||
log: "{{node}} pairing started"
|
||||
|
||||
- action: handshakeInit
|
||||
log: "{{node}} handshake initialized"
|
||||
|
||||
- action: handshakeConfirm
|
||||
log: "{{node}} handshake confirmed"
|
||||
|
||||
- action: handshakeComplete
|
||||
log: "{{node}} handshake completed"
|
||||
|
||||
errors:
|
||||
- error: notFound
|
||||
message: "node does not exist"
|
||||
severity: warning
|
||||
|
||||
- error: invalidID
|
||||
message: "invalid ID"
|
||||
severity: warning
|
||||
|
||||
- error: pairingURIInvalid
|
||||
message: "pairing URI invalid: {{err}}"
|
||||
#
|
||||
- error: pairingURITokenInvalid
|
||||
message: "pairing URI with invalid pairing token"
|
||||
|
||||
- error: pairingURISourceIDInvalid
|
||||
message: "pairing URI without source node ID"
|
||||
|
||||
- error: pairingTokenInvalid
|
||||
message: "pairing token invalid"
|
||||
|
||||
- error: notAllowedToCreate
|
||||
message: "not allowed to create nodes"
|
||||
log: "could not create nodes; insufficient permissions"
|
||||
|
||||
- error: notAllowedToSearch
|
||||
message: "not allowed to search or list nodes"
|
||||
log: "could not search or list nodes; insufficient permissions"
|
||||
|
||||
- error: notAllowedToManage
|
||||
message: "not allowed to manage this node"
|
||||
log: "could not manage {{node}}; insufficient permissions"
|
||||
|
||||
- error: notAllowedToPair
|
||||
message: "not allowed to pair this node"
|
||||
log: "could not pair {{node}}; insufficient permissions"
|
||||
97
server/federation/service/node_handshaker.go
Normal file
97
server/federation/service/node_handshaker.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/federation/types"
|
||||
)
|
||||
|
||||
type (
|
||||
// HttpHandshake performs HTTP handshake with remote node
|
||||
httpNodeHandshake struct {
|
||||
httpClient httpNodeHandshaker
|
||||
}
|
||||
|
||||
httpNodeHandshaker interface {
|
||||
Do(*http.Request) (*http.Response, error)
|
||||
}
|
||||
)
|
||||
|
||||
func HttpHandshake(client httpNodeHandshaker) *httpNodeHandshake {
|
||||
return &httpNodeHandshake{httpClient: client}
|
||||
}
|
||||
|
||||
// Init calls handshake init on a remote node via HTTP
|
||||
//
|
||||
// Called from node.Pair() and sends POST request to remote Corteza
|
||||
// There, it is handled with node.HandshakeInit
|
||||
//
|
||||
// Fn is not part of the node struct to allow injection
|
||||
func (h httpNodeHandshake) Init(ctx context.Context, n *types.Node, authToken string) error {
|
||||
var (
|
||||
endpoint = fmt.Sprintf("%s/nodes/%d/handshake", n.BaseURL, n.SharedNodeID)
|
||||
payload = map[string]string{
|
||||
// Share pairing token, so we can authenticate ourselves
|
||||
"pairToken": n.PairToken,
|
||||
|
||||
// Share auth token for the federation service user for this node
|
||||
"authToken": authToken,
|
||||
"sharedNodeID": strconv.FormatUint(n.SharedNodeID, 10),
|
||||
}
|
||||
)
|
||||
|
||||
return h.send(ctx, endpoint, payload)
|
||||
}
|
||||
|
||||
// Confirm calls handshake init on a remote node via HTTP
|
||||
//
|
||||
// Called from node.Confirm and sends POST request to remote Corteza
|
||||
// There, it's handled with node.HandshakeComplete
|
||||
func (h httpNodeHandshake) Complete(ctx context.Context, n *types.Node, authToken string) error {
|
||||
var (
|
||||
endpoint = fmt.Sprintf("%s/nodes/%d/handshake-complete", n.BaseURL, n.SharedNodeID)
|
||||
payload = map[string]string{
|
||||
// Share auth token for the federation service user for this node
|
||||
"authToken": authToken,
|
||||
}
|
||||
)
|
||||
|
||||
// use n.AuthToken in context
|
||||
ctx = context.WithValue(ctx, "authToken", n.AuthToken)
|
||||
|
||||
return h.send(ctx, endpoint, payload)
|
||||
}
|
||||
|
||||
func (h httpNodeHandshake) send(ctx context.Context, endpoint string, payload map[string]string) error {
|
||||
body := bytes.NewBuffer(nil)
|
||||
if err := json.NewEncoder(body).Encode(payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, endpoint, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
if authToken := ctx.Value("authToken"); authToken != nil {
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", authToken))
|
||||
}
|
||||
|
||||
rsp, err := h.httpClient.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rsp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("pairing failed, remote host retuned non-200 response")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
71
server/federation/service/node_sync.go
Normal file
71
server/federation/service/node_sync.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/federation/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/actionlog"
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
)
|
||||
|
||||
type (
|
||||
nodeSync struct {
|
||||
store store.Storer
|
||||
actionlog actionlog.Recorder
|
||||
}
|
||||
|
||||
NodeSyncService interface {
|
||||
Create(ctx context.Context, new *types.NodeSync) (*types.NodeSync, error)
|
||||
Search(ctx context.Context, f types.NodeSyncFilter) (types.NodeSyncSet, types.NodeSyncFilter, error)
|
||||
LookupLastSuccessfulSync(ctx context.Context, nodeID uint64, syncType string) (*types.NodeSync, error)
|
||||
}
|
||||
)
|
||||
|
||||
func NodeSync() NodeSyncService {
|
||||
return &nodeSync{
|
||||
store: DefaultStore,
|
||||
actionlog: DefaultActionlog,
|
||||
}
|
||||
}
|
||||
|
||||
func (svc nodeSync) Create(ctx context.Context, new *types.NodeSync) (*types.NodeSync, error) {
|
||||
var (
|
||||
aProps = &nodeSyncActionProps{nodeSync: new}
|
||||
)
|
||||
|
||||
err := store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
|
||||
if _, err := DefaultNode.FindByID(ctx, new.NodeID); err != nil {
|
||||
return NodeSyncErrNodeNotFound()
|
||||
}
|
||||
|
||||
return store.CreateFederationNodeSync(ctx, s, new)
|
||||
})
|
||||
|
||||
return new, svc.recordAction(ctx, aProps, NodeSyncActionCreate, err)
|
||||
}
|
||||
|
||||
func (svc nodeSync) Search(ctx context.Context, f types.NodeSyncFilter) (types.NodeSyncSet, types.NodeSyncFilter, error) {
|
||||
return store.SearchFederationNodeSyncs(ctx, svc.store, f)
|
||||
}
|
||||
|
||||
func (svc nodeSync) LookupLastSuccessfulSync(ctx context.Context, nodeID uint64, syncType string) (ns *types.NodeSync, err error) {
|
||||
// todo - filter by sync-type does not work
|
||||
s, _, err := store.SearchFederationNodeSyncs(ctx, svc.store, types.NodeSyncFilter{
|
||||
NodeID: nodeID,
|
||||
SyncType: syncType,
|
||||
SyncStatus: types.NodeSyncStatusSuccess,
|
||||
Sorting: filter.Sorting{
|
||||
Sort: filter.SortExprSet{
|
||||
&filter.SortExpr{Column: "time_of_action", Descending: true},
|
||||
},
|
||||
},
|
||||
Paging: filter.Paging{Limit: 1},
|
||||
})
|
||||
|
||||
if err != nil || len(s) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s[0], nil
|
||||
}
|
||||
359
server/federation/service/node_sync_actions.gen.go
generated
Normal file
359
server/federation/service/node_sync_actions.gen.go
generated
Normal file
@@ -0,0 +1,359 @@
|
||||
package service
|
||||
|
||||
// This file is auto-generated.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
//
|
||||
// Definitions file that controls how this file is generated:
|
||||
// federation/service/node_sync_actions.yaml
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/cortezaproject/corteza/server/federation/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/actionlog"
|
||||
"github.com/cortezaproject/corteza/server/pkg/errors"
|
||||
"github.com/cortezaproject/corteza/server/pkg/locale"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
nodeSyncActionProps struct {
|
||||
nodeSync *types.NodeSync
|
||||
nodeSyncFilter *types.NodeSyncFilter
|
||||
}
|
||||
|
||||
nodeSyncAction struct {
|
||||
timestamp time.Time
|
||||
resource string
|
||||
action string
|
||||
log string
|
||||
severity actionlog.Severity
|
||||
|
||||
// prefix for error when action fails
|
||||
errorMessage string
|
||||
|
||||
props *nodeSyncActionProps
|
||||
}
|
||||
|
||||
nodeSyncLogMetaKey struct{}
|
||||
nodeSyncPropsMetaKey struct{}
|
||||
)
|
||||
|
||||
var (
|
||||
// just a placeholder to cover template cases w/o fmt package use
|
||||
_ = fmt.Println
|
||||
)
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Props methods
|
||||
// setNodeSync updates nodeSyncActionProps's nodeSync
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p *nodeSyncActionProps) setNodeSync(nodeSync *types.NodeSync) *nodeSyncActionProps {
|
||||
p.nodeSync = nodeSync
|
||||
return p
|
||||
}
|
||||
|
||||
// setNodeSyncFilter updates nodeSyncActionProps's nodeSyncFilter
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p *nodeSyncActionProps) setNodeSyncFilter(nodeSyncFilter *types.NodeSyncFilter) *nodeSyncActionProps {
|
||||
p.nodeSyncFilter = nodeSyncFilter
|
||||
return p
|
||||
}
|
||||
|
||||
// Serialize converts nodeSyncActionProps to actionlog.Meta
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p nodeSyncActionProps) Serialize() actionlog.Meta {
|
||||
var (
|
||||
m = make(actionlog.Meta)
|
||||
)
|
||||
|
||||
if p.nodeSync != nil {
|
||||
m.Set("nodeSync.NodeID", p.nodeSync.NodeID, true)
|
||||
m.Set("nodeSync.SyncStatus", p.nodeSync.SyncStatus, true)
|
||||
m.Set("nodeSync.SyncType", p.nodeSync.SyncType, true)
|
||||
m.Set("nodeSync.TimeOfAction", p.nodeSync.TimeOfAction, true)
|
||||
}
|
||||
if p.nodeSyncFilter != nil {
|
||||
m.Set("nodeSyncFilter.Query", p.nodeSyncFilter.Query, true)
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// tr translates string and replaces meta value placeholder with values
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p nodeSyncActionProps) Format(in string, err error) string {
|
||||
var (
|
||||
pairs = []string{"{{err}}"}
|
||||
// first non-empty string
|
||||
fns = func(ii ...interface{}) string {
|
||||
for _, i := range ii {
|
||||
if s := fmt.Sprintf("%v", i); len(s) > 0 {
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
pairs = append(pairs, err.Error())
|
||||
} else {
|
||||
pairs = append(pairs, "nil")
|
||||
}
|
||||
|
||||
if p.nodeSync != nil {
|
||||
// replacement for "{{nodeSync}}" (in order how fields are defined)
|
||||
pairs = append(
|
||||
pairs,
|
||||
"{{nodeSync}}",
|
||||
fns(
|
||||
p.nodeSync.NodeID,
|
||||
p.nodeSync.SyncStatus,
|
||||
p.nodeSync.SyncType,
|
||||
p.nodeSync.TimeOfAction,
|
||||
),
|
||||
)
|
||||
pairs = append(pairs, "{{nodeSync.NodeID}}", fns(p.nodeSync.NodeID))
|
||||
pairs = append(pairs, "{{nodeSync.SyncStatus}}", fns(p.nodeSync.SyncStatus))
|
||||
pairs = append(pairs, "{{nodeSync.SyncType}}", fns(p.nodeSync.SyncType))
|
||||
pairs = append(pairs, "{{nodeSync.TimeOfAction}}", fns(p.nodeSync.TimeOfAction))
|
||||
}
|
||||
|
||||
if p.nodeSyncFilter != nil {
|
||||
// replacement for "{{nodeSyncFilter}}" (in order how fields are defined)
|
||||
pairs = append(
|
||||
pairs,
|
||||
"{{nodeSyncFilter}}",
|
||||
fns(
|
||||
p.nodeSyncFilter.Query,
|
||||
),
|
||||
)
|
||||
pairs = append(pairs, "{{nodeSyncFilter.Query}}", fns(p.nodeSyncFilter.Query))
|
||||
}
|
||||
return strings.NewReplacer(pairs...).Replace(in)
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Action methods
|
||||
|
||||
// String returns loggable description as string
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (a *nodeSyncAction) String() string {
|
||||
var props = &nodeSyncActionProps{}
|
||||
|
||||
if a.props != nil {
|
||||
props = a.props
|
||||
}
|
||||
|
||||
return props.Format(a.log, nil)
|
||||
}
|
||||
|
||||
func (e *nodeSyncAction) ToAction() *actionlog.Action {
|
||||
return &actionlog.Action{
|
||||
Resource: e.resource,
|
||||
Action: e.action,
|
||||
Severity: e.severity,
|
||||
Description: e.String(),
|
||||
Meta: e.props.Serialize(),
|
||||
}
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Action constructors
|
||||
|
||||
// NodeSyncActionLookup returns "federation:node_sync.lookup" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func NodeSyncActionLookup(props ...*nodeSyncActionProps) *nodeSyncAction {
|
||||
a := &nodeSyncAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "federation:node_sync",
|
||||
action: "lookup",
|
||||
log: "looked-up for the last successful sync",
|
||||
severity: actionlog.Info,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// NodeSyncActionCreate returns "federation:node_sync.create" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func NodeSyncActionCreate(props ...*nodeSyncActionProps) *nodeSyncAction {
|
||||
a := &nodeSyncAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "federation:node_sync",
|
||||
action: "create",
|
||||
log: "created node_sync",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Error constructors
|
||||
|
||||
// NodeSyncErrGeneric returns "federation:node_sync.generic" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func NodeSyncErrGeneric(mm ...*nodeSyncActionProps) *errors.Error {
|
||||
var p = &nodeSyncActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("failed to complete request due to internal error", nil),
|
||||
|
||||
errors.Meta("type", "generic"),
|
||||
errors.Meta("resource", "federation:node_sync"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(nodeSyncLogMetaKey{}, "{err}"),
|
||||
errors.Meta(nodeSyncPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "node-sync.errors.generic"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// NodeSyncErrNotFound returns "federation:node_sync.notFound" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func NodeSyncErrNotFound(mm ...*nodeSyncActionProps) *errors.Error {
|
||||
var p = &nodeSyncActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("node_sync does not exist", nil),
|
||||
|
||||
errors.Meta("type", "notFound"),
|
||||
errors.Meta("resource", "federation:node_sync"),
|
||||
|
||||
errors.Meta(nodeSyncPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "node-sync.errors.notFound"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// NodeSyncErrNodeNotFound returns "federation:node_sync.nodeNotFound" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func NodeSyncErrNodeNotFound(mm ...*nodeSyncActionProps) *errors.Error {
|
||||
var p = &nodeSyncActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("node does not exist", nil),
|
||||
|
||||
errors.Meta("type", "nodeNotFound"),
|
||||
errors.Meta("resource", "federation:node_sync"),
|
||||
|
||||
errors.Meta(nodeSyncPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "node-sync.errors.nodeNotFound"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
|
||||
// recordAction is a service helper function wraps function that can return error
|
||||
//
|
||||
// It will wrap unrecognized/internal errors with generic errors.
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (svc nodeSync) recordAction(ctx context.Context, props *nodeSyncActionProps, actionFn func(...*nodeSyncActionProps) *nodeSyncAction, err error) error {
|
||||
if svc.actionlog == nil || actionFn == nil {
|
||||
// action log disabled or no action fn passed, return error as-is
|
||||
return err
|
||||
} else if err == nil {
|
||||
// action completed w/o error, record it
|
||||
svc.actionlog.Record(ctx, actionFn(props).ToAction())
|
||||
return nil
|
||||
}
|
||||
|
||||
a := actionFn(props).ToAction()
|
||||
|
||||
// Extracting error information and recording it as action
|
||||
a.Error = err.Error()
|
||||
|
||||
switch c := err.(type) {
|
||||
case *errors.Error:
|
||||
m := c.Meta()
|
||||
|
||||
a.Error = err.Error()
|
||||
a.Severity = actionlog.Severity(m.AsInt("severity"))
|
||||
a.Description = props.Format(m.AsString(nodeSyncLogMetaKey{}), err)
|
||||
|
||||
if p, has := m[nodeSyncPropsMetaKey{}]; has {
|
||||
a.Meta = p.(*nodeSyncActionProps).Serialize()
|
||||
}
|
||||
|
||||
svc.actionlog.Record(ctx, a)
|
||||
default:
|
||||
svc.actionlog.Record(ctx, a)
|
||||
}
|
||||
|
||||
// Original error is passed on
|
||||
return err
|
||||
}
|
||||
38
server/federation/service/node_sync_actions.yaml
Normal file
38
server/federation/service/node_sync_actions.yaml
Normal file
@@ -0,0 +1,38 @@
|
||||
# List of loggable service actions
|
||||
|
||||
resource: federation:node_sync
|
||||
service: nodeSync
|
||||
|
||||
# Default sensitivity for actions
|
||||
defaultActionSeverity: notice
|
||||
|
||||
# default severity for errors
|
||||
defaultErrorSeverity: error
|
||||
|
||||
import:
|
||||
- github.com/cortezaproject/corteza/server/federation/types
|
||||
|
||||
props:
|
||||
- name: nodeSync
|
||||
type: "*types.NodeSync"
|
||||
fields: [ NodeID, SyncStatus, SyncType, TimeOfAction ]
|
||||
- name: nodeSyncFilter
|
||||
type: "*types.NodeSyncFilter"
|
||||
fields: [ Query ]
|
||||
|
||||
actions:
|
||||
- action: lookup
|
||||
log: "looked-up for the last successful sync"
|
||||
severity: info
|
||||
|
||||
- action: create
|
||||
log: "created node_sync"
|
||||
|
||||
errors:
|
||||
- error: notFound
|
||||
message: "node_sync does not exist"
|
||||
severity: warning
|
||||
|
||||
- error: nodeNotFound
|
||||
message: "node does not exist"
|
||||
severity: warning
|
||||
77
server/federation/service/node_test.go
Normal file
77
server/federation/service/node_test.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/cortezaproject/corteza/server/federation/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNode_decodePairingURI(t *testing.T) {
|
||||
var (
|
||||
tcc = []struct {
|
||||
name string
|
||||
uri string
|
||||
node *types.Node
|
||||
err error
|
||||
}{
|
||||
{
|
||||
"happy path",
|
||||
"https://42:secret-secret-secret-secret-1234@example.tld/federation?name=Noddy",
|
||||
&types.Node{SharedNodeID: 42, Name: "Noddy", BaseURL: "https://example.tld/federation", PairToken: "secret-secret-secret-secret-1234"},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"no name",
|
||||
"https://42:secret-secret-secret-secret-1234@example.tld",
|
||||
&types.Node{SharedNodeID: 42, BaseURL: "https://example.tld/", PairToken: "secret-secret-secret-secret-1234"},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"no token",
|
||||
"https://42@example.tld",
|
||||
nil,
|
||||
NodeErrPairingURITokenInvalid(),
|
||||
},
|
||||
{
|
||||
"no node id",
|
||||
"https://:secret-secret-secret-secret-1234@example.tld",
|
||||
nil,
|
||||
NodeErrPairingURISourceIDInvalid(),
|
||||
},
|
||||
{
|
||||
"invalid URL",
|
||||
"https://this is not a valid url",
|
||||
nil,
|
||||
NodeErrPairingURIInvalid().Wrap(fmt.Errorf(`invalid character " " in host name`)),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
for _, tc := range tcc {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var (
|
||||
req = require.New(t)
|
||||
out, err = (&node{}).decodePairingURI(tc.uri)
|
||||
)
|
||||
|
||||
if tc.err != nil {
|
||||
req.Equal(tc.err.Error(), err.Error())
|
||||
} else {
|
||||
req.NoError(err)
|
||||
req.Equal(tc.node, out)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNode_makePairingURI(t *testing.T) {
|
||||
var (
|
||||
svc = &node{host: "example.tld", baseURL: "federation", name: "Noddy"}
|
||||
req = require.New(t)
|
||||
n = &types.Node{ID: 42, PairToken: "secret"}
|
||||
uri = svc.makePairingURI(n)
|
||||
)
|
||||
|
||||
req.Equal("https://42:secret@example.tld/federation?name=Noddy", uri)
|
||||
}
|
||||
11
server/federation/service/processer.go
Normal file
11
server/federation/service/processer.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package service
|
||||
|
||||
import "context"
|
||||
|
||||
type (
|
||||
Processer interface {
|
||||
Process(ctx context.Context, payload []byte) (ProcesserResponse, error)
|
||||
}
|
||||
|
||||
ProcesserResponse interface{}
|
||||
)
|
||||
134
server/federation/service/processer_data.go
Normal file
134
server/federation/service/processer_data.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
ct "github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/federation/service/decoder"
|
||||
"github.com/cortezaproject/corteza/server/federation/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/auth"
|
||||
st "github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
dataProcesser struct {
|
||||
ID uint64
|
||||
ComposeModuleID uint64
|
||||
ComposeNamespaceID uint64
|
||||
NodeBaseURL string
|
||||
ModuleMappings *types.ModuleFieldMappingSet
|
||||
ModuleMappingValues *ct.RecordValueSet
|
||||
SyncService *Sync
|
||||
Node *types.Node
|
||||
User *st.User
|
||||
}
|
||||
|
||||
dataProcesserResponse struct {
|
||||
Processed int
|
||||
}
|
||||
)
|
||||
|
||||
// Process gets the payload from syncer and
|
||||
// uses the decode package to decode the whole set, depending on
|
||||
// the filtering that was used (limit)
|
||||
func (dp *dataProcesser) Process(ctx context.Context, payload []byte) (ProcesserResponse, error) {
|
||||
processed := 0
|
||||
o, err := decoder.DecodeFederationRecordSync([]byte(payload))
|
||||
|
||||
if err != nil {
|
||||
return dataProcesserResponse{
|
||||
Processed: processed,
|
||||
}, err
|
||||
}
|
||||
|
||||
if len(o) == 0 {
|
||||
return dataProcesserResponse{
|
||||
Processed: processed,
|
||||
}, nil
|
||||
}
|
||||
|
||||
ctx = auth.SetIdentityToContext(ctx, auth.FederationUser())
|
||||
|
||||
for _, er := range o {
|
||||
var (
|
||||
rec *ct.Record
|
||||
err error
|
||||
)
|
||||
|
||||
dp.SyncService.mapper.Merge(&er.Values, dp.ModuleMappingValues, dp.ModuleMappings)
|
||||
|
||||
if er.DeletedAt != nil {
|
||||
// find the record
|
||||
if rec, err = dp.findRecordByFederationID(ctx, er.ID, dp.ComposeModuleID, dp.ComposeNamespaceID); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle edge cases where the data doesn't exist anymore
|
||||
if rec != nil {
|
||||
dp.SyncService.DeleteRecord(ctx, rec)
|
||||
}
|
||||
processed++
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if er.UpdatedAt != nil {
|
||||
if rec, err = dp.findRecordByFederationID(ctx, er.ID, dp.ComposeModuleID, dp.ComposeNamespaceID); err != nil {
|
||||
// could not find existing record
|
||||
continue
|
||||
}
|
||||
|
||||
if rec != nil {
|
||||
rec.Values = *dp.ModuleMappingValues
|
||||
}
|
||||
}
|
||||
|
||||
// if the record was updated on origin, but we somehow do not have it
|
||||
// create it anyway
|
||||
if rec == nil {
|
||||
rec = &ct.Record{
|
||||
ModuleID: dp.ComposeModuleID,
|
||||
NamespaceID: dp.ComposeNamespaceID,
|
||||
Values: *dp.ModuleMappingValues,
|
||||
Meta: map[string]any{
|
||||
"federation": dp.NodeBaseURL,
|
||||
"federation_extrecord": fmt.Sprintf("%d", er.ID),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if rec.ID != 0 {
|
||||
_, err = dp.SyncService.UpdateRecord(ctx, rec)
|
||||
} else {
|
||||
_, err = dp.SyncService.CreateRecord(ctx, rec)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
processed++
|
||||
}
|
||||
|
||||
return dataProcesserResponse{
|
||||
Processed: processed,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// findRecordByFederationID finds any already existing records via
|
||||
// federation label
|
||||
func (dp *dataProcesser) findRecordByFederationID(ctx context.Context, recordID, moduleID, namespaceID uint64) (r *ct.Record, err error) {
|
||||
filter := ct.RecordFilter{
|
||||
NamespaceID: namespaceID,
|
||||
ModuleID: moduleID,
|
||||
Meta: map[string]any{"federation_extrecord": fmt.Sprintf("%d", recordID)}}
|
||||
|
||||
if s, err := dp.SyncService.FindRecords(ctx, filter); err == nil {
|
||||
if len(s) == 1 {
|
||||
r = s[0]
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
230
server/federation/service/processer_data_test.go
Normal file
230
server/federation/service/processer_data_test.go
Normal file
@@ -0,0 +1,230 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
cs "github.com/cortezaproject/corteza/server/compose/service"
|
||||
ct "github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/federation/types"
|
||||
ss "github.com/cortezaproject/corteza/server/system/service"
|
||||
st "github.com/cortezaproject/corteza/server/system/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type (
|
||||
testSyncService struct {
|
||||
Sync
|
||||
}
|
||||
|
||||
testSharedModuleService struct {
|
||||
SharedModuleService
|
||||
}
|
||||
testRecordServicePersistSuccess struct {
|
||||
cs.RecordService
|
||||
}
|
||||
testRecordServiceUpdateSuccess struct {
|
||||
cs.RecordService
|
||||
}
|
||||
testRecordServiceDeleteSuccess struct {
|
||||
cs.RecordService
|
||||
}
|
||||
testRecordServicePersistError struct {
|
||||
cs.RecordService
|
||||
}
|
||||
testUserService struct {
|
||||
ss.UserService
|
||||
}
|
||||
testRoleService struct {
|
||||
ss.RoleService
|
||||
}
|
||||
)
|
||||
|
||||
func TestProcesserData_persist(t *testing.T) {
|
||||
|
||||
var (
|
||||
tcc = []struct {
|
||||
name string
|
||||
payload string
|
||||
mappings string
|
||||
persisted int
|
||||
err string
|
||||
values *ct.RecordValueSet
|
||||
s *Sync
|
||||
}{
|
||||
{
|
||||
"successful persist on valid mapping",
|
||||
`{"response": {"set": [{"recordID":"1","values":[{"name":"Facebook","value":"foobar"}],"createdAt":"2020-12-05T10:10:10Z"}]}}`,
|
||||
`[{"origin":{"kind":"String","name":"Description","label":"Description","isMulti":false},"destination":{"kind":"String","name":"Name","label":"Description","isMulti":false}},{"origin":{"kind":"Url","name":"Facebook","label":"Facebook","isMulti":false},"destination":{"kind":"Url","name":"Fb","label":"Facebook","isMulti":false}}]`,
|
||||
1,
|
||||
"",
|
||||
&ct.RecordValueSet{&ct.RecordValue{Name: "Fb", Value: ""}},
|
||||
NewSync(
|
||||
&Syncer{},
|
||||
&Mapper{},
|
||||
&testSharedModuleService{},
|
||||
&testRecordServicePersistSuccess{},
|
||||
&testUserService{},
|
||||
&testRoleService{}),
|
||||
},
|
||||
{
|
||||
"successful update on valid mapping and existing federated record",
|
||||
`{"response": {"set": [{"recordID":"1","values":[{"name":"Facebook","value":"foobar"}],"createdAt":"2020-12-05T10:10:10Z", "updatedAt":"2020-12-06T10:10:10Z"}]}}`,
|
||||
`[{"origin":{"kind":"String","name":"Description","label":"Description","isMulti":false},"destination":{"kind":"String","name":"Name","label":"Description","isMulti":false}},{"origin":{"kind":"Url","name":"Facebook","label":"Facebook","isMulti":false},"destination":{"kind":"Url","name":"Fb","label":"Facebook","isMulti":false}}]`,
|
||||
1,
|
||||
"",
|
||||
&ct.RecordValueSet{&ct.RecordValue{Name: "Fb", Value: ""}},
|
||||
NewSync(
|
||||
&Syncer{},
|
||||
&Mapper{},
|
||||
&testSharedModuleService{},
|
||||
&testRecordServiceUpdateSuccess{},
|
||||
&testUserService{},
|
||||
&testRoleService{}),
|
||||
},
|
||||
{
|
||||
"successful delete on valid mapping and existing federated record",
|
||||
`{"response": {"set": [{"recordID":"1","values":[{"name":"Facebook","value":"foobar"}],"createdAt":"2020-12-05T10:10:10Z", "deletedAt":"2020-12-07T10:10:10Z"}]}}`,
|
||||
`[{"origin":{"kind":"String","name":"Description","label":"Description","isMulti":false},"destination":{"kind":"String","name":"Name","label":"Description","isMulti":false}},{"origin":{"kind":"Url","name":"Facebook","label":"Facebook","isMulti":false},"destination":{"kind":"Url","name":"Fb","label":"Facebook","isMulti":false}}]`,
|
||||
1,
|
||||
"",
|
||||
&ct.RecordValueSet{&ct.RecordValue{Name: "Fb", Value: ""}},
|
||||
NewSync(
|
||||
&Syncer{},
|
||||
&Mapper{},
|
||||
&testSharedModuleService{},
|
||||
&testRecordServiceDeleteSuccess{},
|
||||
&testUserService{},
|
||||
&testRoleService{}),
|
||||
},
|
||||
{
|
||||
"persist error on valid mapping",
|
||||
`{"response": {"set": [{"recordID":"1","values":[{"name":"Facebook","value":"foobar"}]}]}}`,
|
||||
`[{"origin":{"kind":"String","name":"Description","label":"Description","isMulti":false},"destination":{"kind":"String","name":"Name","label":"Description","isMulti":false}},{"origin":{"kind":"Url","name":"Facebook","label":"Facebook","isMulti":false},"destination":{"kind":"Url","name":"Fb","label":"Facebook","isMulti":false}}]`,
|
||||
0,
|
||||
"",
|
||||
&ct.RecordValueSet{&ct.RecordValue{Name: "Fb", Value: ""}},
|
||||
NewSync(
|
||||
&Syncer{},
|
||||
&Mapper{},
|
||||
&testSharedModuleService{},
|
||||
&testRecordServicePersistError{},
|
||||
&testUserService{},
|
||||
&testRoleService{}),
|
||||
},
|
||||
{
|
||||
"persist one record on mixed mappings",
|
||||
`{"response": {"set": [{"recordID":"1","values":[{"name":"THIS_FIELD_NAME_IS_NOT_ON_ORIGIN","value":"this value will not be set"}]}, {"recordID":"1","values":[{"name":"Facebook","value":"this value WILL be set"}]}]}}`,
|
||||
`[{"origin":{"kind":"String","name":"Description","label":"Description","isMulti":false},"destination":{"kind":"String","name":"Name","label":"Description","isMulti":false}},{"origin":{"kind":"Url","name":"Facebook","label":"Facebook","isMulti":false},"destination":{"kind":"Url","name":"Fb","label":"Facebook","isMulti":false}}]`,
|
||||
2,
|
||||
"",
|
||||
&ct.RecordValueSet{&ct.RecordValue{Name: "Fb", Value: ""}},
|
||||
NewSync(
|
||||
&Syncer{},
|
||||
&Mapper{},
|
||||
&testSharedModuleService{},
|
||||
&testRecordServicePersistSuccess{},
|
||||
&testUserService{},
|
||||
&testRoleService{}),
|
||||
},
|
||||
{
|
||||
"no records from payload",
|
||||
`{"response": {"set": []}}`,
|
||||
`[{"origin":{"kind":"String","name":"Description","label":"Description","isMulti":false},"destination":{"kind":"String","name":"Name","label":"Description","isMulti":false}},{"origin":{"kind":"Url","name":"Facebook","label":"Facebook","isMulti":false},"destination":{"kind":"Url","name":"Fb","label":"Facebook","isMulti":false}}]`,
|
||||
0,
|
||||
"",
|
||||
&ct.RecordValueSet{&ct.RecordValue{Name: "Fb", Value: ""}},
|
||||
NewSync(
|
||||
&Syncer{},
|
||||
&Mapper{},
|
||||
&testSharedModuleService{},
|
||||
&testRecordServicePersistSuccess{},
|
||||
&testUserService{},
|
||||
&testRoleService{}),
|
||||
},
|
||||
{
|
||||
"validation error, no persist",
|
||||
`{"respon`,
|
||||
`[{"origin":{"kind":"String","name":"Description","label":"Description","isMulti":false},"destination":{"kind":"String","name":"Name","label":"Description","isMulti":false}},{"origin":{"kind":"Url","name":"Facebook","label":"Facebook","isMulti":false},"destination":{"kind":"Url","name":"Fb","label":"Facebook","isMulti":false}}]`,
|
||||
0,
|
||||
"unexpected end of JSON input",
|
||||
&ct.RecordValueSet{&ct.RecordValue{Name: "Fb", Value: ""}},
|
||||
NewSync(
|
||||
&Syncer{},
|
||||
&Mapper{},
|
||||
&testSharedModuleService{},
|
||||
&testRecordServicePersistSuccess{},
|
||||
&testUserService{},
|
||||
&testRoleService{}),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
for _, tc := range tcc {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var (
|
||||
ctx = context.Background()
|
||||
req = require.New(t)
|
||||
)
|
||||
|
||||
mm := &types.ModuleFieldMappingSet{}
|
||||
json.Unmarshal([]byte(tc.mappings), mm)
|
||||
|
||||
dp := &dataProcesser{
|
||||
ID: 1,
|
||||
ComposeModuleID: 1,
|
||||
ComposeNamespaceID: 1,
|
||||
NodeBaseURL: "",
|
||||
ModuleMappings: mm,
|
||||
ModuleMappingValues: tc.values,
|
||||
SyncService: tc.s,
|
||||
Node: &types.Node{},
|
||||
User: &st.User{},
|
||||
}
|
||||
|
||||
out, err := dp.Process(ctx, []byte(tc.payload))
|
||||
|
||||
if tc.err != "" {
|
||||
req.Equal(tc.err, err.Error())
|
||||
} else {
|
||||
req.NoError(err)
|
||||
}
|
||||
|
||||
req.Equal(tc.persisted, out.(dataProcesserResponse).Processed)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// create success
|
||||
func (s testRecordServicePersistSuccess) Create(_ context.Context, record *ct.Record) (*ct.Record, *ct.RecordValueErrorSet, error) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func (s testRecordServicePersistSuccess) Find(_ context.Context, filter ct.RecordFilter) (ct.RecordSet, ct.RecordFilter, error) {
|
||||
return ct.RecordSet{}, ct.RecordFilter{}, nil
|
||||
}
|
||||
|
||||
// update success
|
||||
func (s testRecordServiceUpdateSuccess) Update(_ context.Context, record *ct.Record) (*ct.Record, *ct.RecordValueErrorSet, error) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func (s testRecordServiceUpdateSuccess) Find(_ context.Context, filter ct.RecordFilter) (ct.RecordSet, ct.RecordFilter, error) {
|
||||
return ct.RecordSet{&ct.Record{ID: 2}}, ct.RecordFilter{}, nil
|
||||
}
|
||||
|
||||
// delete success
|
||||
func (s testRecordServiceDeleteSuccess) DeleteByID(_ context.Context, namespaceID, moduleID uint64, recordID ...uint64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s testRecordServiceDeleteSuccess) Find(_ context.Context, filter ct.RecordFilter) (ct.RecordSet, ct.RecordFilter, error) {
|
||||
return ct.RecordSet{&ct.Record{ID: 2, ModuleID: 2, NamespaceID: 2}}, ct.RecordFilter{}, nil
|
||||
}
|
||||
|
||||
// create error
|
||||
func (s testRecordServicePersistError) Create(_ context.Context, record *ct.Record) (*ct.Record, *ct.RecordValueErrorSet, error) {
|
||||
return nil, nil, fmt.Errorf("mocked error")
|
||||
}
|
||||
120
server/federation/service/processer_structure.go
Normal file
120
server/federation/service/processer_structure.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/federation/service/decoder"
|
||||
"github.com/cortezaproject/corteza/server/federation/types"
|
||||
st "github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
structureProcesser struct {
|
||||
SyncService *Sync
|
||||
Node *types.Node
|
||||
User *st.User
|
||||
}
|
||||
|
||||
structureProcesserResponse struct {
|
||||
ModuleID uint64
|
||||
Processed int
|
||||
}
|
||||
)
|
||||
|
||||
// Process gets the payload from syncer and
|
||||
// uses the decode package to decode the whole set, depending on
|
||||
// the filtering that was used (limit)
|
||||
func (dp *structureProcesser) Process(ctx context.Context, payload []byte) (ProcesserResponse, error) {
|
||||
processed := 0
|
||||
o, err := decoder.DecodeFederationModuleSync([]byte(payload))
|
||||
|
||||
if err != nil {
|
||||
return structureProcesserResponse{
|
||||
Processed: processed,
|
||||
}, err
|
||||
}
|
||||
|
||||
if len(o) == 0 {
|
||||
return structureProcesserResponse{
|
||||
Processed: processed,
|
||||
}, nil
|
||||
}
|
||||
|
||||
for _, em := range o {
|
||||
new := &types.SharedModule{
|
||||
NodeID: dp.Node.ID,
|
||||
ExternalFederationModuleID: em.ID,
|
||||
Fields: em.Fields,
|
||||
Handle: em.Handle,
|
||||
Name: em.Name,
|
||||
}
|
||||
|
||||
existing, err := dp.SyncService.LookupSharedModule(ctx, new)
|
||||
|
||||
if err != nil {
|
||||
return structureProcesserResponse{
|
||||
ModuleID: new.ExternalFederationModuleID,
|
||||
Processed: processed,
|
||||
}, err
|
||||
}
|
||||
|
||||
// create
|
||||
{
|
||||
if existing == nil {
|
||||
_, err := dp.SyncService.CreateSharedModule(ctx, new)
|
||||
|
||||
if err != nil {
|
||||
return structureProcesserResponse{
|
||||
ModuleID: new.ExternalFederationModuleID,
|
||||
Processed: processed,
|
||||
}, err
|
||||
}
|
||||
|
||||
processed++
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// update
|
||||
{
|
||||
canUpdate, err := dp.SyncService.CanUpdateSharedModule(ctx, new, existing)
|
||||
|
||||
if err != nil {
|
||||
return structureProcesserResponse{
|
||||
ModuleID: existing.ExternalFederationModuleID,
|
||||
Processed: processed,
|
||||
}, err
|
||||
}
|
||||
|
||||
// stop the sync for this module
|
||||
if !canUpdate {
|
||||
return structureProcesserResponse{
|
||||
ModuleID: new.ExternalFederationModuleID,
|
||||
Processed: processed,
|
||||
}, SharedModuleErrFederationSyncStructureChanged(&sharedModuleActionProps{module: existing})
|
||||
}
|
||||
|
||||
existing.Name = new.Name
|
||||
existing.Handle = new.Handle
|
||||
existing.Fields = new.Fields
|
||||
|
||||
_, err = dp.SyncService.UpdateSharedModule(ctx, existing)
|
||||
|
||||
if err != nil {
|
||||
return structureProcesserResponse{
|
||||
ModuleID: existing.ExternalFederationModuleID,
|
||||
Processed: processed,
|
||||
}, err
|
||||
}
|
||||
|
||||
processed++
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// use paging, but only one per-time
|
||||
return structureProcesserResponse{
|
||||
ModuleID: o[0].ID,
|
||||
Processed: processed,
|
||||
}, err
|
||||
}
|
||||
232
server/federation/service/processer_structure_test.go
Normal file
232
server/federation/service/processer_structure_test.go
Normal file
@@ -0,0 +1,232 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
cs "github.com/cortezaproject/corteza/server/compose/service"
|
||||
"github.com/cortezaproject/corteza/server/federation/types"
|
||||
ss "github.com/cortezaproject/corteza/server/system/service"
|
||||
st "github.com/cortezaproject/corteza/server/system/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type (
|
||||
testSyncStructureService struct{ Sync }
|
||||
testSyncStructureServiceCanUpdateFieldsError struct{ Sync }
|
||||
|
||||
testStructureSharedModuleService struct{ SharedModuleService }
|
||||
testStructureSharedModuleServiceError struct{ SharedModuleService }
|
||||
testStructureSharedModuleServiceCreateNewModule struct{ SharedModuleService }
|
||||
testStructureSharedModuleServiceCreateNewModuleErr struct{ SharedModuleService }
|
||||
testStructureSharedModuleServiceUpdateModule struct{ SharedModuleService }
|
||||
testStructureSharedModuleServiceUpdateModuleErr struct{ SharedModuleService }
|
||||
|
||||
testStructureRecordServicePersistSuccess struct{ cs.RecordService }
|
||||
testStructureUserService struct{ ss.UserService }
|
||||
testStructureRoleService struct{ ss.RoleService }
|
||||
)
|
||||
|
||||
func TestProcesserStructure_persist(t *testing.T) {
|
||||
|
||||
var (
|
||||
tcc = []struct {
|
||||
name string
|
||||
payload string
|
||||
persisted int
|
||||
err string
|
||||
s *Sync
|
||||
}{
|
||||
{
|
||||
"no modules from payload",
|
||||
`{"response": {"set": []}}`,
|
||||
0,
|
||||
"",
|
||||
NewSync(
|
||||
&Syncer{},
|
||||
&Mapper{},
|
||||
&testStructureSharedModuleService{},
|
||||
&testRecordServicePersistSuccess{},
|
||||
&testUserService{},
|
||||
&testRoleService{}),
|
||||
},
|
||||
{
|
||||
"validation error, no persist",
|
||||
`{"respon`,
|
||||
0,
|
||||
"unexpected end of JSON input",
|
||||
NewSync(
|
||||
&Syncer{},
|
||||
&Mapper{},
|
||||
&testStructureSharedModuleService{},
|
||||
&testRecordServicePersistSuccess{},
|
||||
&testUserService{},
|
||||
&testRoleService{}),
|
||||
},
|
||||
{
|
||||
"lookup existing module error",
|
||||
`{"response":{"set": [{"moduleID": "11", "nodeID": "21", "composeModuleID": "31", "composeNamespaceID":"32","handle":"handle","name":"name","fields":[]}]}}`,
|
||||
0,
|
||||
"db error",
|
||||
NewSync(
|
||||
&Syncer{},
|
||||
&Mapper{},
|
||||
&testStructureSharedModuleServiceError{},
|
||||
&testRecordServicePersistSuccess{},
|
||||
&testUserService{},
|
||||
&testRoleService{}),
|
||||
},
|
||||
{
|
||||
"create new module",
|
||||
`{"response":{"set": [{"moduleID": "11", "nodeID": "21", "composeModuleID": "31", "composeNamespaceID":"32","handle":"handle","name":"name","fields":[]}]}}`,
|
||||
1,
|
||||
"",
|
||||
NewSync(
|
||||
&Syncer{},
|
||||
&Mapper{},
|
||||
&testStructureSharedModuleServiceCreateNewModule{},
|
||||
&testRecordServicePersistSuccess{},
|
||||
&testUserService{},
|
||||
&testRoleService{}),
|
||||
},
|
||||
{
|
||||
"create new module error",
|
||||
`{"response":{"set": [{"moduleID": "11", "nodeID": "21", "composeModuleID": "31", "composeNamespaceID":"32","handle":"handle","name":"name","fields":[]}]}}`,
|
||||
0,
|
||||
"could not create new module",
|
||||
NewSync(
|
||||
&Syncer{},
|
||||
&Mapper{},
|
||||
&testStructureSharedModuleServiceCreateNewModuleErr{},
|
||||
&testRecordServicePersistSuccess{},
|
||||
&testUserService{},
|
||||
&testRoleService{}),
|
||||
},
|
||||
{
|
||||
"could not update module fields error",
|
||||
`{"response":{"set": [{"moduleID": "11", "nodeID": "21", "composeModuleID": "31", "composeNamespaceID":"32","handle":"handle","name":"name","fields":[]}]}}`,
|
||||
0,
|
||||
"module structure changed",
|
||||
NewSync(
|
||||
&Syncer{},
|
||||
&Mapper{},
|
||||
&testStructureSharedModuleServiceUpdateModule{},
|
||||
&testRecordServicePersistSuccess{},
|
||||
&testUserService{},
|
||||
&testRoleService{}),
|
||||
},
|
||||
{
|
||||
"update module fields success",
|
||||
`{"response":{"set": [{"moduleID": "11", "nodeID": "21", "composeModuleID": "31", "composeNamespaceID":"32","handle":"handle","name":"name","fields":[{"kind":"String","label":"label","isMulti":false,"name":"existingField"}]}]}}`,
|
||||
1,
|
||||
"",
|
||||
NewSync(
|
||||
&Syncer{},
|
||||
&Mapper{},
|
||||
&testStructureSharedModuleServiceUpdateModule{},
|
||||
&testRecordServicePersistSuccess{},
|
||||
&testUserService{},
|
||||
&testRoleService{}),
|
||||
},
|
||||
{
|
||||
"update module error",
|
||||
`{"response":{"set": [{"moduleID": "11", "nodeID": "21", "composeModuleID": "31", "composeNamespaceID":"32","handle":"handle","name":"name","fields":[]}]}}`,
|
||||
0,
|
||||
"could not update module",
|
||||
NewSync(
|
||||
&Syncer{},
|
||||
&Mapper{},
|
||||
&testStructureSharedModuleServiceUpdateModuleErr{},
|
||||
&testRecordServicePersistSuccess{},
|
||||
&testUserService{},
|
||||
&testRoleService{}),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
for _, tc := range tcc {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var (
|
||||
ctx = context.Background()
|
||||
req = require.New(t)
|
||||
)
|
||||
|
||||
dp := &structureProcesser{
|
||||
SyncService: tc.s,
|
||||
Node: &types.Node{},
|
||||
User: &st.User{},
|
||||
}
|
||||
|
||||
out, err := dp.Process(ctx, []byte(tc.payload))
|
||||
|
||||
if tc.err != "" {
|
||||
if err != nil {
|
||||
req.Equal(tc.err, err.Error())
|
||||
} else {
|
||||
req.Fail("err should not be nil")
|
||||
}
|
||||
} else {
|
||||
req.NoError(err)
|
||||
}
|
||||
|
||||
req.Equal(tc.persisted, out.(structureProcesserResponse).Processed)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s testStructureSharedModuleServiceError) Find(ctx context.Context, filter types.SharedModuleFilter) (types.SharedModuleSet, types.SharedModuleFilter, error) {
|
||||
return types.SharedModuleSet{}, types.SharedModuleFilter{}, fmt.Errorf("db error")
|
||||
}
|
||||
|
||||
func (s testStructureSharedModuleServiceCreateNewModule) Find(ctx context.Context, filter types.SharedModuleFilter) (types.SharedModuleSet, types.SharedModuleFilter, error) {
|
||||
return types.SharedModuleSet{}, types.SharedModuleFilter{}, nil
|
||||
}
|
||||
|
||||
func (s testStructureSharedModuleServiceCreateNewModule) Create(ctx context.Context, new *types.SharedModule) (*types.SharedModule, error) {
|
||||
return &types.SharedModule{}, nil
|
||||
}
|
||||
|
||||
func (s testStructureSharedModuleServiceCreateNewModuleErr) Find(ctx context.Context, filter types.SharedModuleFilter) (types.SharedModuleSet, types.SharedModuleFilter, error) {
|
||||
return types.SharedModuleSet{}, types.SharedModuleFilter{}, nil
|
||||
}
|
||||
|
||||
func (s testStructureSharedModuleServiceCreateNewModuleErr) Create(ctx context.Context, new *types.SharedModule) (*types.SharedModule, error) {
|
||||
return nil, fmt.Errorf("could not create new module")
|
||||
}
|
||||
|
||||
func (s testStructureSharedModuleServiceUpdateModule) Find(ctx context.Context, filter types.SharedModuleFilter) (types.SharedModuleSet, types.SharedModuleFilter, error) {
|
||||
return types.SharedModuleSet{
|
||||
&types.SharedModule{
|
||||
ID: 11,
|
||||
NodeID: 21,
|
||||
Handle: "handle",
|
||||
Name: "name",
|
||||
Fields: types.ModuleFieldSet{
|
||||
&types.ModuleField{
|
||||
Kind: "String",
|
||||
Label: "label",
|
||||
IsMulti: false,
|
||||
Name: "existingField",
|
||||
},
|
||||
},
|
||||
}},
|
||||
types.SharedModuleFilter{},
|
||||
nil
|
||||
}
|
||||
|
||||
func (s testStructureSharedModuleServiceUpdateModule) Update(ctx context.Context, updated *types.SharedModule) (*types.SharedModule, error) {
|
||||
return &types.SharedModule{}, nil
|
||||
}
|
||||
|
||||
func (s testStructureSharedModuleServiceUpdateModuleErr) Find(ctx context.Context, filter types.SharedModuleFilter) (types.SharedModuleSet, types.SharedModuleFilter, error) {
|
||||
return types.SharedModuleSet{&types.SharedModule{
|
||||
Fields: types.ModuleFieldSet{},
|
||||
}},
|
||||
types.SharedModuleFilter{},
|
||||
nil
|
||||
}
|
||||
|
||||
func (s testStructureSharedModuleServiceUpdateModuleErr) Update(ctx context.Context, updated *types.SharedModule) (*types.SharedModule, error) {
|
||||
return nil, fmt.Errorf("could not update module")
|
||||
}
|
||||
139
server/federation/service/service.go
Normal file
139
server/federation/service/service.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
cs "github.com/cortezaproject/corteza/server/compose/service"
|
||||
"github.com/cortezaproject/corteza/server/pkg/actionlog"
|
||||
"github.com/cortezaproject/corteza/server/pkg/auth"
|
||||
"github.com/cortezaproject/corteza/server/pkg/id"
|
||||
"github.com/cortezaproject/corteza/server/pkg/label"
|
||||
"github.com/cortezaproject/corteza/server/pkg/logger"
|
||||
"github.com/cortezaproject/corteza/server/pkg/options"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
ss "github.com/cortezaproject/corteza/server/system/service"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
"go.uber.org/zap"
|
||||
"time"
|
||||
labelTypes "github.com/cortezaproject/corteza/server/pkg/label/types"
|
||||
)
|
||||
|
||||
type (
|
||||
Config struct {
|
||||
ActionLog options.ActionLogOpt
|
||||
Storage options.ObjectStoreOpt
|
||||
Federation options.FederationOpt
|
||||
Server options.HttpServerOpt
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultStore store.Storer
|
||||
|
||||
DefaultLogger *zap.Logger
|
||||
|
||||
DefaultAccessControl *accessControl
|
||||
|
||||
// CurrentSettings represents current system settings
|
||||
CurrentSettings = &types.AppSettings{}
|
||||
|
||||
DefaultOptions options.FederationOpt
|
||||
|
||||
DefaultActionlog actionlog.Recorder
|
||||
|
||||
DefaultNode *node
|
||||
DefaultNodeSync NodeSyncService
|
||||
DefaultExposedModule ExposedModuleService
|
||||
DefaultSharedModule SharedModuleService
|
||||
DefaultModuleMapping ModuleMappingService
|
||||
|
||||
// wrapper around time.Now() that will aid service testing
|
||||
now = func() *time.Time {
|
||||
c := time.Now().Round(time.Second)
|
||||
return &c
|
||||
}
|
||||
|
||||
// wrapper around id.Next() that will aid service testing
|
||||
nextID = func() uint64 {
|
||||
return id.Next()
|
||||
}
|
||||
)
|
||||
|
||||
func Initialize(_ context.Context, log *zap.Logger, s store.Storer, c Config) (err error) {
|
||||
DefaultOptions = c.Federation
|
||||
|
||||
// we're doing conversion to avoid having
|
||||
// store interface exposed or generated inside app package
|
||||
DefaultStore = s
|
||||
|
||||
DefaultLogger = log.Named("service")
|
||||
|
||||
// @todo add healthcheck(s) for federation sync services
|
||||
|
||||
{
|
||||
tee := zap.NewNop()
|
||||
policy := actionlog.MakeProductionPolicy()
|
||||
|
||||
if !c.ActionLog.Enabled {
|
||||
policy = actionlog.MakeDisabledPolicy()
|
||||
} else if c.ActionLog.Debug {
|
||||
policy = actionlog.MakeDebugPolicy()
|
||||
tee = logger.MakeDebugLogger()
|
||||
}
|
||||
|
||||
DefaultActionlog = actionlog.NewService(DefaultStore, log, tee, policy)
|
||||
}
|
||||
|
||||
DefaultAccessControl = AccessControl(s)
|
||||
|
||||
DefaultNode = Node(
|
||||
DefaultStore,
|
||||
ss.DefaultUser,
|
||||
DefaultActionlog,
|
||||
func(ctx context.Context, i auth.Identifiable) (token []byte, err error) {
|
||||
return auth.TokenIssuer.Issue(
|
||||
ctx,
|
||||
auth.WithIdentity(i),
|
||||
auth.WithScope("api"),
|
||||
auth.WithAudience("federation"),
|
||||
)
|
||||
},
|
||||
c.Federation,
|
||||
c.Server,
|
||||
DefaultAccessControl,
|
||||
)
|
||||
DefaultNodeSync = NodeSync()
|
||||
DefaultExposedModule = ExposedModule()
|
||||
DefaultSharedModule = SharedModule()
|
||||
DefaultModuleMapping = ModuleMapping()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func Watchers(ctx context.Context) {
|
||||
DefaultLogger.Info("Starting federation - warning, this is still an experimental feature")
|
||||
|
||||
syncService := NewSync(
|
||||
&Syncer{},
|
||||
&Mapper{},
|
||||
DefaultSharedModule,
|
||||
cs.DefaultRecord,
|
||||
ss.DefaultUser,
|
||||
ss.DefaultRole)
|
||||
|
||||
syncStructure := WorkerStructure(syncService, DefaultLogger)
|
||||
syncData := WorkerData(syncService, DefaultLogger)
|
||||
|
||||
go syncStructure.Watch(
|
||||
ctx,
|
||||
DefaultOptions.StructureMonitorInterval,
|
||||
DefaultOptions.StructurePageSize)
|
||||
|
||||
go syncData.Watch(
|
||||
ctx,
|
||||
DefaultOptions.DataMonitorInterval,
|
||||
DefaultOptions.DataPageSize)
|
||||
}
|
||||
|
||||
func AddFederationLabel(entity label.LabeledResource, key string, value string) {
|
||||
entity.SetLabel(key, labelTypes.LabelValue{Val: value})
|
||||
}
|
||||
171
server/federation/service/shared_module.go
Normal file
171
server/federation/service/shared_module.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/cortezaproject/corteza/server/pkg/errors"
|
||||
|
||||
composeService "github.com/cortezaproject/corteza/server/compose/service"
|
||||
"github.com/cortezaproject/corteza/server/federation/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/actionlog"
|
||||
"github.com/cortezaproject/corteza/server/pkg/auth"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
)
|
||||
|
||||
type (
|
||||
sharedModule struct {
|
||||
node node
|
||||
ac sharedModuleAccessController
|
||||
compose composeService.ModuleService
|
||||
store store.Storer
|
||||
actionlog actionlog.Recorder
|
||||
}
|
||||
|
||||
sharedModuleAccessController interface {
|
||||
CanCreateModuleOnNode(ctx context.Context, r *types.Node) bool
|
||||
}
|
||||
|
||||
SharedModuleService interface {
|
||||
Create(ctx context.Context, new *types.SharedModule) (*types.SharedModule, error)
|
||||
Update(ctx context.Context, updated *types.SharedModule) (*types.SharedModule, error)
|
||||
Find(ctx context.Context, filter types.SharedModuleFilter) (types.SharedModuleSet, types.SharedModuleFilter, error)
|
||||
FindByID(ctx context.Context, nodeID uint64, moduleID uint64) (*types.SharedModule, error)
|
||||
}
|
||||
)
|
||||
|
||||
func SharedModule() *sharedModule {
|
||||
return &sharedModule{
|
||||
ac: DefaultAccessControl,
|
||||
node: *DefaultNode,
|
||||
compose: composeService.DefaultModule,
|
||||
store: DefaultStore,
|
||||
actionlog: DefaultActionlog,
|
||||
}
|
||||
}
|
||||
|
||||
func (svc sharedModule) FindByID(ctx context.Context, nodeID uint64, moduleID uint64) (module *types.SharedModule, err error) {
|
||||
err = func() error {
|
||||
if module, err = loadSharedModule(ctx, svc.store, nodeID, moduleID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
return module, err
|
||||
}
|
||||
|
||||
func (svc sharedModule) Create(ctx context.Context, new *types.SharedModule) (*types.SharedModule, error) {
|
||||
var (
|
||||
aProps = &sharedModuleActionProps{changed: new}
|
||||
)
|
||||
|
||||
err := store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
|
||||
var (
|
||||
node *types.Node
|
||||
)
|
||||
|
||||
if node, err = svc.node.FindByID(ctx, new.NodeID); err != nil {
|
||||
return SharedModuleErrNodeNotFound()
|
||||
}
|
||||
|
||||
if !svc.ac.CanCreateModuleOnNode(ctx, node) {
|
||||
return SharedModuleErrNotAllowedToCreate()
|
||||
}
|
||||
|
||||
if err = svc.uniqueCheck(ctx, new); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
new.ID = nextID()
|
||||
new.CreatedAt = *now()
|
||||
new.CreatedBy = auth.GetIdentityFromContext(ctx).Identity()
|
||||
|
||||
// check if Fields can be unmarshaled to the fields structure
|
||||
if new.Fields != nil {
|
||||
}
|
||||
|
||||
aProps.setModule(new)
|
||||
|
||||
if err = store.CreateFederationSharedModule(ctx, s, new); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return new, svc.recordAction(ctx, aProps, SharedModuleActionCreate, err)
|
||||
}
|
||||
|
||||
func (svc sharedModule) Update(ctx context.Context, updated *types.SharedModule) (*types.SharedModule, error) {
|
||||
var (
|
||||
aProps = &sharedModuleActionProps{module: updated}
|
||||
)
|
||||
|
||||
err := store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
|
||||
updated.UpdatedAt = now()
|
||||
updated.UpdatedBy = auth.GetIdentityFromContext(ctx).Identity()
|
||||
|
||||
aProps.setModule(updated)
|
||||
|
||||
if _, err = svc.node.FindByID(ctx, updated.NodeID); err != nil {
|
||||
return SharedModuleErrNodeNotFound()
|
||||
}
|
||||
|
||||
if err = store.UpdateFederationSharedModule(ctx, s, updated); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return updated, svc.recordAction(ctx, aProps, SharedModuleActionUpdate, err)
|
||||
}
|
||||
|
||||
func (svc sharedModule) uniqueCheck(ctx context.Context, m *types.SharedModule) (err error) {
|
||||
f := types.SharedModuleFilter{
|
||||
NodeID: m.NodeID,
|
||||
Handle: m.Handle,
|
||||
Name: m.Name,
|
||||
}
|
||||
|
||||
if set, _, err := store.SearchFederationSharedModules(ctx, svc.store, f); len(set) > 0 && err == nil {
|
||||
return SharedModuleErrNotUnique()
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc sharedModule) Find(ctx context.Context, filter types.SharedModuleFilter) (set types.SharedModuleSet, f types.SharedModuleFilter, err error) {
|
||||
var (
|
||||
aProps = &sharedModuleActionProps{filter: &filter}
|
||||
)
|
||||
|
||||
err = func() error {
|
||||
if set, f, err = store.SearchFederationSharedModules(ctx, svc.store, filter); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
return set, f, svc.recordAction(ctx, aProps, SharedModuleActionSearch, err)
|
||||
}
|
||||
|
||||
func loadSharedModule(ctx context.Context, s store.FederationSharedModules, nodeID, ID uint64) (res *types.SharedModule, err error) {
|
||||
if ID == 0 || nodeID == 0 {
|
||||
return nil, SharedModuleErrInvalidID()
|
||||
}
|
||||
|
||||
if res, err = store.LookupFederationSharedModuleByID(ctx, s, ID); errors.IsNotFound(err) {
|
||||
err = SharedModuleErrNotFound()
|
||||
}
|
||||
|
||||
if err == nil && nodeID != res.NodeID {
|
||||
// Make sure chart belongs to the right namespace
|
||||
return nil, SharedModuleErrNotFound()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
717
server/federation/service/shared_module_actions.gen.go
generated
Normal file
717
server/federation/service/shared_module_actions.gen.go
generated
Normal file
@@ -0,0 +1,717 @@
|
||||
package service
|
||||
|
||||
// This file is auto-generated.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
//
|
||||
// Definitions file that controls how this file is generated:
|
||||
// federation/service/shared_module_actions.yaml
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/cortezaproject/corteza/server/federation/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/actionlog"
|
||||
"github.com/cortezaproject/corteza/server/pkg/errors"
|
||||
"github.com/cortezaproject/corteza/server/pkg/locale"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
sharedModuleActionProps struct {
|
||||
module *types.SharedModule
|
||||
changed *types.SharedModule
|
||||
filter *types.SharedModuleFilter
|
||||
node *types.Node
|
||||
}
|
||||
|
||||
sharedModuleAction struct {
|
||||
timestamp time.Time
|
||||
resource string
|
||||
action string
|
||||
log string
|
||||
severity actionlog.Severity
|
||||
|
||||
// prefix for error when action fails
|
||||
errorMessage string
|
||||
|
||||
props *sharedModuleActionProps
|
||||
}
|
||||
|
||||
sharedModuleLogMetaKey struct{}
|
||||
sharedModulePropsMetaKey struct{}
|
||||
)
|
||||
|
||||
var (
|
||||
// just a placeholder to cover template cases w/o fmt package use
|
||||
_ = fmt.Println
|
||||
)
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Props methods
|
||||
// setModule updates sharedModuleActionProps's module
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p *sharedModuleActionProps) setModule(module *types.SharedModule) *sharedModuleActionProps {
|
||||
p.module = module
|
||||
return p
|
||||
}
|
||||
|
||||
// setChanged updates sharedModuleActionProps's changed
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p *sharedModuleActionProps) setChanged(changed *types.SharedModule) *sharedModuleActionProps {
|
||||
p.changed = changed
|
||||
return p
|
||||
}
|
||||
|
||||
// setFilter updates sharedModuleActionProps's filter
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p *sharedModuleActionProps) setFilter(filter *types.SharedModuleFilter) *sharedModuleActionProps {
|
||||
p.filter = filter
|
||||
return p
|
||||
}
|
||||
|
||||
// setNode updates sharedModuleActionProps's node
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p *sharedModuleActionProps) setNode(node *types.Node) *sharedModuleActionProps {
|
||||
p.node = node
|
||||
return p
|
||||
}
|
||||
|
||||
// Serialize converts sharedModuleActionProps to actionlog.Meta
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p sharedModuleActionProps) Serialize() actionlog.Meta {
|
||||
var (
|
||||
m = make(actionlog.Meta)
|
||||
)
|
||||
|
||||
if p.module != nil {
|
||||
m.Set("module.ID", p.module.ID, true)
|
||||
}
|
||||
if p.changed != nil {
|
||||
m.Set("changed.ID", p.changed.ID, true)
|
||||
}
|
||||
if p.filter != nil {
|
||||
m.Set("filter.query", p.filter.Query, true)
|
||||
m.Set("filter.sort", p.filter.Sort, true)
|
||||
m.Set("filter.limit", p.filter.Limit, true)
|
||||
}
|
||||
if p.node != nil {
|
||||
m.Set("node.ID", p.node.ID, true)
|
||||
m.Set("node.Name", p.node.Name, true)
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// tr translates string and replaces meta value placeholder with values
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p sharedModuleActionProps) Format(in string, err error) string {
|
||||
var (
|
||||
pairs = []string{"{{err}}"}
|
||||
// first non-empty string
|
||||
fns = func(ii ...interface{}) string {
|
||||
for _, i := range ii {
|
||||
if s := fmt.Sprintf("%v", i); len(s) > 0 {
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
pairs = append(pairs, err.Error())
|
||||
} else {
|
||||
pairs = append(pairs, "nil")
|
||||
}
|
||||
|
||||
if p.module != nil {
|
||||
// replacement for "{{module}}" (in order how fields are defined)
|
||||
pairs = append(
|
||||
pairs,
|
||||
"{{module}}",
|
||||
fns(
|
||||
p.module.ID,
|
||||
),
|
||||
)
|
||||
pairs = append(pairs, "{{module.ID}}", fns(p.module.ID))
|
||||
}
|
||||
|
||||
if p.changed != nil {
|
||||
// replacement for "{{changed}}" (in order how fields are defined)
|
||||
pairs = append(
|
||||
pairs,
|
||||
"{{changed}}",
|
||||
fns(
|
||||
p.changed.ID,
|
||||
),
|
||||
)
|
||||
pairs = append(pairs, "{{changed.ID}}", fns(p.changed.ID))
|
||||
}
|
||||
|
||||
if p.filter != nil {
|
||||
// replacement for "{{filter}}" (in order how fields are defined)
|
||||
pairs = append(
|
||||
pairs,
|
||||
"{{filter}}",
|
||||
fns(
|
||||
p.filter.Query,
|
||||
p.filter.Sort,
|
||||
p.filter.Limit,
|
||||
),
|
||||
)
|
||||
pairs = append(pairs, "{{filter.query}}", fns(p.filter.Query))
|
||||
pairs = append(pairs, "{{filter.sort}}", fns(p.filter.Sort))
|
||||
pairs = append(pairs, "{{filter.limit}}", fns(p.filter.Limit))
|
||||
}
|
||||
|
||||
if p.node != nil {
|
||||
// replacement for "{{node}}" (in order how fields are defined)
|
||||
pairs = append(
|
||||
pairs,
|
||||
"{{node}}",
|
||||
fns(
|
||||
p.node.ID,
|
||||
p.node.Name,
|
||||
),
|
||||
)
|
||||
pairs = append(pairs, "{{node.ID}}", fns(p.node.ID))
|
||||
pairs = append(pairs, "{{node.Name}}", fns(p.node.Name))
|
||||
}
|
||||
return strings.NewReplacer(pairs...).Replace(in)
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Action methods
|
||||
|
||||
// String returns loggable description as string
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (a *sharedModuleAction) String() string {
|
||||
var props = &sharedModuleActionProps{}
|
||||
|
||||
if a.props != nil {
|
||||
props = a.props
|
||||
}
|
||||
|
||||
return props.Format(a.log, nil)
|
||||
}
|
||||
|
||||
func (e *sharedModuleAction) ToAction() *actionlog.Action {
|
||||
return &actionlog.Action{
|
||||
Resource: e.resource,
|
||||
Action: e.action,
|
||||
Severity: e.severity,
|
||||
Description: e.String(),
|
||||
Meta: e.props.Serialize(),
|
||||
}
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Action constructors
|
||||
|
||||
// SharedModuleActionSearch returns "federation:shared_module.search" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func SharedModuleActionSearch(props ...*sharedModuleActionProps) *sharedModuleAction {
|
||||
a := &sharedModuleAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "federation:shared_module",
|
||||
action: "search",
|
||||
log: "searched for modules",
|
||||
severity: actionlog.Info,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// SharedModuleActionLookup returns "federation:shared_module.lookup" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func SharedModuleActionLookup(props ...*sharedModuleActionProps) *sharedModuleAction {
|
||||
a := &sharedModuleAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "federation:shared_module",
|
||||
action: "lookup",
|
||||
log: "looked-up for a {{module}}",
|
||||
severity: actionlog.Info,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// SharedModuleActionCreate returns "federation:shared_module.create" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func SharedModuleActionCreate(props ...*sharedModuleActionProps) *sharedModuleAction {
|
||||
a := &sharedModuleAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "federation:shared_module",
|
||||
action: "create",
|
||||
log: "created {{module}}",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// SharedModuleActionUpdate returns "federation:shared_module.update" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func SharedModuleActionUpdate(props ...*sharedModuleActionProps) *sharedModuleAction {
|
||||
a := &sharedModuleAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "federation:shared_module",
|
||||
action: "update",
|
||||
log: "updated {{module}}",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// SharedModuleActionDelete returns "federation:shared_module.delete" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func SharedModuleActionDelete(props ...*sharedModuleActionProps) *sharedModuleAction {
|
||||
a := &sharedModuleAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "federation:shared_module",
|
||||
action: "delete",
|
||||
log: "deleted {{module}}",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// SharedModuleActionUndelete returns "federation:shared_module.undelete" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func SharedModuleActionUndelete(props ...*sharedModuleActionProps) *sharedModuleAction {
|
||||
a := &sharedModuleAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "federation:shared_module",
|
||||
action: "undelete",
|
||||
log: "undeleted {{module}}",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Error constructors
|
||||
|
||||
// SharedModuleErrGeneric returns "federation:shared_module.generic" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func SharedModuleErrGeneric(mm ...*sharedModuleActionProps) *errors.Error {
|
||||
var p = &sharedModuleActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("failed to complete request due to internal error", nil),
|
||||
|
||||
errors.Meta("type", "generic"),
|
||||
errors.Meta("resource", "federation:shared_module"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(sharedModuleLogMetaKey{}, "{err}"),
|
||||
errors.Meta(sharedModulePropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "shared-module.errors.generic"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// SharedModuleErrNotFound returns "federation:shared_module.notFound" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func SharedModuleErrNotFound(mm ...*sharedModuleActionProps) *errors.Error {
|
||||
var p = &sharedModuleActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("module does not exist", nil),
|
||||
|
||||
errors.Meta("type", "notFound"),
|
||||
errors.Meta("resource", "federation:shared_module"),
|
||||
|
||||
errors.Meta(sharedModulePropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "shared-module.errors.notFound"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// SharedModuleErrInvalidID returns "federation:shared_module.invalidID" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func SharedModuleErrInvalidID(mm ...*sharedModuleActionProps) *errors.Error {
|
||||
var p = &sharedModuleActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("invalid ID", nil),
|
||||
|
||||
errors.Meta("type", "invalidID"),
|
||||
errors.Meta("resource", "federation:shared_module"),
|
||||
|
||||
errors.Meta(sharedModulePropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "shared-module.errors.invalidID"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// SharedModuleErrStaleData returns "federation:shared_module.staleData" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func SharedModuleErrStaleData(mm ...*sharedModuleActionProps) *errors.Error {
|
||||
var p = &sharedModuleActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("module was modified by someone else or by a workflow after you've opened it. Please refresh to see the latest updated version", nil),
|
||||
|
||||
errors.Meta("type", "staleData"),
|
||||
errors.Meta("resource", "federation:shared_module"),
|
||||
|
||||
errors.Meta(sharedModulePropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "shared-module.errors.staleData"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// SharedModuleErrFederationSyncStructureChanged returns "federation:shared_module.federationSyncStructureChanged" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func SharedModuleErrFederationSyncStructureChanged(mm ...*sharedModuleActionProps) *errors.Error {
|
||||
var p = &sharedModuleActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("module structure changed", nil),
|
||||
|
||||
errors.Meta("type", "federationSyncStructureChanged"),
|
||||
errors.Meta("resource", "federation:shared_module"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(sharedModuleLogMetaKey{}, "could not update shared module, structure different"),
|
||||
errors.Meta(sharedModulePropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "shared-module.errors.federationSyncStructureChanged"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// SharedModuleErrNotUnique returns "federation:shared_module.notUnique" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func SharedModuleErrNotUnique(mm ...*sharedModuleActionProps) *errors.Error {
|
||||
var p = &sharedModuleActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("node not unique", nil),
|
||||
|
||||
errors.Meta("type", "notUnique"),
|
||||
errors.Meta("resource", "federation:shared_module"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(sharedModuleLogMetaKey{}, "used duplicate node TODO"),
|
||||
errors.Meta(sharedModulePropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "shared-module.errors.notUnique"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// SharedModuleErrNodeNotFound returns "federation:shared_module.nodeNotFound" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func SharedModuleErrNodeNotFound(mm ...*sharedModuleActionProps) *errors.Error {
|
||||
var p = &sharedModuleActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("node does not exist", nil),
|
||||
|
||||
errors.Meta("type", "nodeNotFound"),
|
||||
errors.Meta("resource", "federation:shared_module"),
|
||||
|
||||
errors.Meta(sharedModulePropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "shared-module.errors.nodeNotFound"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// SharedModuleErrNotAllowedToCreate returns "federation:shared_module.notAllowedToCreate" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func SharedModuleErrNotAllowedToCreate(mm ...*sharedModuleActionProps) *errors.Error {
|
||||
var p = &sharedModuleActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("not allowed to create modules", nil),
|
||||
|
||||
errors.Meta("type", "notAllowedToCreate"),
|
||||
errors.Meta("resource", "federation:shared_module"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(sharedModuleLogMetaKey{}, "could not create modules; insufficient permissions"),
|
||||
errors.Meta(sharedModulePropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "shared-module.errors.notAllowedToCreate"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// SharedModuleErrNotAllowedToManage returns "federation:shared_module.notAllowedToManage" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func SharedModuleErrNotAllowedToManage(mm ...*sharedModuleActionProps) *errors.Error {
|
||||
var p = &sharedModuleActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("not allowed to manage this module", nil),
|
||||
|
||||
errors.Meta("type", "notAllowedToManage"),
|
||||
errors.Meta("resource", "federation:shared_module"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(sharedModuleLogMetaKey{}, "could not read {{module}}; insufficient permissions"),
|
||||
errors.Meta(sharedModulePropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "shared-module.errors.notAllowedToManage"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// SharedModuleErrNotAllowedToMap returns "federation:shared_module.notAllowedToMap" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func SharedModuleErrNotAllowedToMap(mm ...*sharedModuleActionProps) *errors.Error {
|
||||
var p = &sharedModuleActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("not allowed to map this module", nil),
|
||||
|
||||
errors.Meta("type", "notAllowedToMap"),
|
||||
errors.Meta("resource", "federation:shared_module"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(sharedModuleLogMetaKey{}, "could not map {{module}}; insufficient permissions"),
|
||||
errors.Meta(sharedModulePropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "shared-module.errors.notAllowedToMap"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
|
||||
// recordAction is a service helper function wraps function that can return error
|
||||
//
|
||||
// It will wrap unrecognized/internal errors with generic errors.
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (svc sharedModule) recordAction(ctx context.Context, props *sharedModuleActionProps, actionFn func(...*sharedModuleActionProps) *sharedModuleAction, err error) error {
|
||||
if svc.actionlog == nil || actionFn == nil {
|
||||
// action log disabled or no action fn passed, return error as-is
|
||||
return err
|
||||
} else if err == nil {
|
||||
// action completed w/o error, record it
|
||||
svc.actionlog.Record(ctx, actionFn(props).ToAction())
|
||||
return nil
|
||||
}
|
||||
|
||||
a := actionFn(props).ToAction()
|
||||
|
||||
// Extracting error information and recording it as action
|
||||
a.Error = err.Error()
|
||||
|
||||
switch c := err.(type) {
|
||||
case *errors.Error:
|
||||
m := c.Meta()
|
||||
|
||||
a.Error = err.Error()
|
||||
a.Severity = actionlog.Severity(m.AsInt("severity"))
|
||||
a.Description = props.Format(m.AsString(sharedModuleLogMetaKey{}), err)
|
||||
|
||||
if p, has := m[sharedModulePropsMetaKey{}]; has {
|
||||
a.Meta = p.(*sharedModuleActionProps).Serialize()
|
||||
}
|
||||
|
||||
svc.actionlog.Record(ctx, a)
|
||||
default:
|
||||
svc.actionlog.Record(ctx, a)
|
||||
}
|
||||
|
||||
// Original error is passed on
|
||||
return err
|
||||
}
|
||||
86
server/federation/service/shared_module_actions.yaml
Normal file
86
server/federation/service/shared_module_actions.yaml
Normal file
@@ -0,0 +1,86 @@
|
||||
# List of loggable service actions
|
||||
|
||||
resource: federation:shared_module
|
||||
service: sharedModule
|
||||
|
||||
# Default sensitivity for actions
|
||||
defaultActionSeverity: notice
|
||||
|
||||
# default severity for errors
|
||||
defaultErrorSeverity: error
|
||||
|
||||
import:
|
||||
- github.com/cortezaproject/corteza/server/federation/types
|
||||
|
||||
props:
|
||||
- name: module
|
||||
type: "*types.SharedModule"
|
||||
fields: [ ID ]
|
||||
- name: changed
|
||||
type: "*types.SharedModule"
|
||||
fields: [ ID ]
|
||||
- name: filter
|
||||
type: "*types.SharedModuleFilter"
|
||||
fields: [ query, sort, limit ]
|
||||
- name: node
|
||||
type: "*types.Node"
|
||||
fields: [ ID, Name ]
|
||||
|
||||
actions:
|
||||
- action: search
|
||||
log: "searched for modules"
|
||||
severity: info
|
||||
|
||||
- action: lookup
|
||||
log: "looked-up for a {{module}}"
|
||||
severity: info
|
||||
|
||||
- action: create
|
||||
log: "created {{module}}"
|
||||
|
||||
- action: update
|
||||
log: "updated {{module}}"
|
||||
|
||||
- action: delete
|
||||
log: "deleted {{module}}"
|
||||
|
||||
- action: undelete
|
||||
log: "undeleted {{module}}"
|
||||
|
||||
errors:
|
||||
- error: notFound
|
||||
message: "module does not exist"
|
||||
severity: warning
|
||||
|
||||
- error: invalidID
|
||||
message: "invalid ID"
|
||||
severity: warning
|
||||
|
||||
- error: staleData
|
||||
message: "module was modified by someone else or by a workflow after you've opened it. Please refresh to see the latest updated version"
|
||||
severity: warning
|
||||
|
||||
- error: federationSyncStructureChanged
|
||||
message: "module structure changed"
|
||||
log: "could not update shared module, structure different"
|
||||
|
||||
- error: notUnique
|
||||
message: "node not unique"
|
||||
log: "used duplicate node TODO"
|
||||
severity: warning
|
||||
|
||||
- error: nodeNotFound
|
||||
message: "node does not exist"
|
||||
severity: warning
|
||||
|
||||
- error: notAllowedToCreate
|
||||
message: "not allowed to create modules"
|
||||
log: "could not create modules; insufficient permissions"
|
||||
|
||||
- error: notAllowedToManage
|
||||
message: "not allowed to manage this module"
|
||||
log: "could not read {{module}}; insufficient permissions"
|
||||
|
||||
- error: notAllowedToMap
|
||||
message: "not allowed to map this module"
|
||||
log: "could not map {{module}}; insufficient permissions"
|
||||
228
server/federation/service/sync.go
Normal file
228
server/federation/service/sync.go
Normal file
@@ -0,0 +1,228 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
cs "github.com/cortezaproject/corteza/server/compose/service"
|
||||
ct "github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/federation/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/auth"
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
ss "github.com/cortezaproject/corteza/server/system/service"
|
||||
st "github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
Sync struct {
|
||||
syncer *Syncer
|
||||
mapper *Mapper
|
||||
sharedModuleService SharedModuleService
|
||||
composeRecordService cs.RecordService
|
||||
systemUserService ss.UserService
|
||||
systemRoleService ss.RoleService
|
||||
}
|
||||
)
|
||||
|
||||
func NewSync(s *Syncer, m *Mapper, sm SharedModuleService, cs cs.RecordService, us ss.UserService, rs ss.RoleService) *Sync {
|
||||
return &Sync{
|
||||
syncer: s,
|
||||
mapper: m,
|
||||
sharedModuleService: sm,
|
||||
composeRecordService: cs,
|
||||
systemUserService: us,
|
||||
systemRoleService: rs,
|
||||
}
|
||||
}
|
||||
|
||||
// CanUpdateSharedModule checks the origin and destination module
|
||||
// compatibility of fields
|
||||
// It currently checks if all of the fields are exactly the same
|
||||
// TODO - check if any of the newly missing fields are actually being used so a safe update
|
||||
// is possible
|
||||
func (s *Sync) CanUpdateSharedModule(ctx context.Context, new *types.SharedModule, existing *types.SharedModule) (bool, error) {
|
||||
// check for mapped fields
|
||||
var (
|
||||
fstr, f2str []byte
|
||||
err error
|
||||
)
|
||||
|
||||
if fstr, err = json.Marshal(new.Fields); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if f2str, err = json.Marshal(existing.Fields); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return string(fstr) == string(f2str), nil
|
||||
}
|
||||
|
||||
// ProcessPayload passes the payload to the syncer lib
|
||||
func (s *Sync) ProcessPayload(ctx context.Context, payload []byte, out chan Url, url types.SyncerURI, processer Processer) (ProcesserResponse, error) {
|
||||
ctx = auth.SetIdentityToContext(ctx, auth.FederationUser())
|
||||
|
||||
return s.syncer.Process(ctx, payload, out, url, processer)
|
||||
}
|
||||
|
||||
// QueueUrl passes the url to the syncer
|
||||
func (s *Sync) QueueUrl(url Url, out chan Url) {
|
||||
s.syncer.Queue(url, out)
|
||||
}
|
||||
|
||||
// FetchUrl passes the url to be fetched to the syncer
|
||||
func (s *Sync) FetchUrl(ctx context.Context, url string) (io.Reader, error) {
|
||||
return s.syncer.Fetch(ctx, url)
|
||||
}
|
||||
|
||||
// CreateRecord wraps the compose Record service Create
|
||||
func (s *Sync) CreateRecord(ctx context.Context, rec *ct.Record) (out *ct.Record, err error) {
|
||||
out, _, err = s.composeRecordService.Create(ctx, rec)
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateRecord wraps the compose Record service Update
|
||||
func (s *Sync) UpdateRecord(ctx context.Context, rec *ct.Record) (out *ct.Record, err error) {
|
||||
out, _, err = s.composeRecordService.Update(ctx, rec)
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteRecord wraps the compose Record service Update
|
||||
func (s *Sync) DeleteRecord(ctx context.Context, rec *ct.Record) error {
|
||||
return s.composeRecordService.DeleteByID(ctx, rec.NamespaceID, rec.ModuleID, rec.ID)
|
||||
}
|
||||
|
||||
// FindRecord find the record via federation label
|
||||
func (s *Sync) FindRecords(ctx context.Context, filter ct.RecordFilter) (set ct.RecordSet, err error) {
|
||||
set, _, err = s.composeRecordService.Find(ctx, filter)
|
||||
return
|
||||
}
|
||||
|
||||
// LookupSharedModule find the shared module if exists
|
||||
func (s *Sync) LookupSharedModule(ctx context.Context, new *types.SharedModule) (*types.SharedModule, error) {
|
||||
var sm *types.SharedModule
|
||||
|
||||
list, _, err := s.sharedModuleService.Find(ctx, types.SharedModuleFilter{
|
||||
NodeID: new.NodeID,
|
||||
ExternalFederationModuleID: new.ExternalFederationModuleID})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(list) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
sm = list[0]
|
||||
|
||||
return sm, nil
|
||||
}
|
||||
|
||||
// UpdateSharedModule wraps the federation SharedModule service Update
|
||||
func (s *Sync) UpdateSharedModule(ctx context.Context, updated *types.SharedModule) (*types.SharedModule, error) {
|
||||
return s.sharedModuleService.Update(ctx, updated)
|
||||
}
|
||||
|
||||
// CreateSharedModule wraps the federation SharedModule service Create
|
||||
func (s *Sync) CreateSharedModule(ctx context.Context, new *types.SharedModule) (*types.SharedModule, error) {
|
||||
return s.sharedModuleService.Create(ctx, new)
|
||||
}
|
||||
|
||||
// GetPairedNodes finds successfuly paired nodes
|
||||
func (s *Sync) GetPairedNodes(ctx context.Context) (types.NodeSet, error) {
|
||||
set, _, err := DefaultNode.Search(ctx, types.NodeFilter{Status: types.NodeStatusPaired})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return set, nil
|
||||
}
|
||||
|
||||
func (s *Sync) GetSharedModules(ctx context.Context, nodeID uint64) (types.SharedModuleSet, error) {
|
||||
set, _, err := DefaultSharedModule.Find(ctx, types.SharedModuleFilter{NodeID: nodeID})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
set, _ = set.Filter(func(sm *types.SharedModule) (bool, error) {
|
||||
return len(sm.Fields) > 0, nil
|
||||
})
|
||||
|
||||
return set, nil
|
||||
}
|
||||
|
||||
func (s *Sync) GetModuleMappings(ctx context.Context, moduleID uint64) (out *types.ModuleMapping, err error) {
|
||||
out, err = DefaultModuleMapping.FindByID(ctx, moduleID)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Sync) PrepareModuleMappings(ctx context.Context, mappings *types.ModuleMapping) (ct.RecordValueSet, error) {
|
||||
return s.mapper.Prepare((*mappings).FieldMapping), nil
|
||||
}
|
||||
|
||||
func (s *Sync) GetLastStructureSyncStatus(ctx context.Context, nodeID, externalFederationModuleID uint64) (sync types.NodeSync, err error) {
|
||||
var list types.NodeSyncSet
|
||||
|
||||
list, _, err = DefaultNodeSync.Search(ctx, types.NodeSyncFilter{
|
||||
NodeID: nodeID,
|
||||
ModuleID: externalFederationModuleID,
|
||||
SyncType: types.NodeSyncTypeStructure,
|
||||
Sorting: filter.Sorting{
|
||||
Sort: filter.SortExprSet{
|
||||
&filter.SortExpr{Column: "time_of_action", Descending: true},
|
||||
},
|
||||
},
|
||||
Paging: filter.Paging{Limit: 1},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(list) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
return *list[0], nil
|
||||
}
|
||||
|
||||
func (s *Sync) GetLastSyncTime(ctx context.Context, nodeID uint64, syncType string) (*time.Time, error) {
|
||||
ns, err := DefaultNodeSync.LookupLastSuccessfulSync(ctx, nodeID, syncType)
|
||||
|
||||
if err != nil || ns == nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ns.TimeOfAction, nil
|
||||
}
|
||||
|
||||
// LoadUserWithRoles gets the federation user, that was
|
||||
// created at node pairing process
|
||||
func (s *Sync) LoadUserWithRoles(ctx context.Context, nodeID uint64) (*st.User, error) {
|
||||
var (
|
||||
u *st.User
|
||||
err error
|
||||
)
|
||||
|
||||
// get the federated user, associated for this node
|
||||
if u, err = s.systemUserService.FindByHandle(ctx, fmt.Sprintf("federation_%d", nodeID)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// attach the roles
|
||||
rr, _, err := s.systemRoleService.Find(ctx, st.RoleFilter{MemberID: u.ID})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u.SetRoles(rr.IDs()...)
|
||||
|
||||
return u, nil
|
||||
}
|
||||
18
server/federation/service/sync_worker.go
Normal file
18
server/federation/service/sync_worker.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
worker interface {
|
||||
PrepareForNodes(ctx context.Context, sync *Sync, urls chan Url)
|
||||
Watch(ctx context.Context, delay time.Duration, limit int)
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
defaultDelay = time.Second * 10
|
||||
defaultPage = 10
|
||||
)
|
||||
281
server/federation/service/sync_worker_data.go
Normal file
281
server/federation/service/sync_worker_data.go
Normal file
@@ -0,0 +1,281 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/federation/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/auth"
|
||||
"github.com/cortezaproject/corteza/server/pkg/logger"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type (
|
||||
syncWorkerData struct {
|
||||
syncService *Sync
|
||||
logger *zap.Logger
|
||||
delay time.Duration
|
||||
limit int
|
||||
}
|
||||
)
|
||||
|
||||
func WorkerData(sync *Sync, logger *zap.Logger) *syncWorkerData {
|
||||
return &syncWorkerData{
|
||||
syncService: sync,
|
||||
logger: logger,
|
||||
limit: defaultPage,
|
||||
delay: defaultDelay,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *syncWorkerData) queueUrl(url *types.SyncerURI, urls chan Url, meta Processer) {
|
||||
t := Url{
|
||||
Url: *url,
|
||||
Meta: meta,
|
||||
}
|
||||
|
||||
w.syncService.QueueUrl(t, urls)
|
||||
}
|
||||
|
||||
func (w *syncWorkerData) PrepareForNodes(ctx context.Context, urls chan Url) {
|
||||
|
||||
nodes, err := w.syncService.GetPairedNodes(ctx)
|
||||
|
||||
if err != nil {
|
||||
w.logger.Info("could not get paired nodes", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
// get all shared modules and their module mappings
|
||||
for _, n := range nodes {
|
||||
// get the user, associated for this node
|
||||
u, err := w.syncService.LoadUserWithRoles(ctx, n.ID)
|
||||
|
||||
if err != nil {
|
||||
w.logger.Info("could not preload federation user, skipping",
|
||||
logger.Uint64("nodeID", n.ID),
|
||||
zap.Error(err))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
set, err := w.syncService.GetSharedModules(ctx, n.ID)
|
||||
|
||||
if err != nil {
|
||||
w.logger.Info("could not get shared modules, skipping",
|
||||
logger.Uint64("nodeID", n.ID),
|
||||
zap.Error(err))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if len(set) == 0 {
|
||||
w.logger.Info("there are no valid shared modules, skipping",
|
||||
logger.Uint64("nodeID", n.ID),
|
||||
zap.Error(err))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// go through set and prepare module mappings for it
|
||||
for _, sm := range set {
|
||||
z := []zap.Field{
|
||||
logger.Uint64("nodeID", n.ID),
|
||||
logger.Uint64("moduleID", sm.ID),
|
||||
zap.String("host", n.BaseURL),
|
||||
zap.String("delay", w.delay.String()),
|
||||
zap.Int("pagesize", w.limit),
|
||||
}
|
||||
|
||||
// TODO: rename lastSyncStatus to lastSybc prolly!!!!
|
||||
|
||||
// if the last sync was error'd, log and skip
|
||||
lastSyncStatus, err := w.syncService.GetLastStructureSyncStatus(ctx, n.ID, sm.ExternalFederationModuleID)
|
||||
if err != nil {
|
||||
w.logger.Info("could not get last sync status, skipping", z...)
|
||||
continue
|
||||
}
|
||||
|
||||
if lastSyncStatus.SyncStatus == types.NodeSyncStatusError {
|
||||
w.logger.Info("last structure sync was not complete, admin resolve needed, skipping", z...)
|
||||
continue
|
||||
}
|
||||
|
||||
mappings, _ := w.syncService.GetModuleMappings(ctx, sm.ID)
|
||||
|
||||
if mappings == nil {
|
||||
w.logger.Info("could not prepare module mappings for shared module, skipping", z...)
|
||||
continue
|
||||
}
|
||||
|
||||
mappingValues, err := w.syncService.PrepareModuleMappings(ctx, mappings)
|
||||
|
||||
if err != nil || mappingValues == nil {
|
||||
w.logger.Info("could not prepare module mappings for shared module, skipping", z...)
|
||||
continue
|
||||
}
|
||||
|
||||
// get the last sync per-node
|
||||
lastSync, _ := w.syncService.GetLastSyncTime(ctx, n.ID, types.NodeSyncTypeData)
|
||||
// todo this is where we get the last sync value. prolly the issue could be on the fetch!
|
||||
|
||||
basePath := fmt.Sprintf("/nodes/%d/modules/%d/records/", n.SharedNodeID, sm.ExternalFederationModuleID)
|
||||
|
||||
if lastSync != nil {
|
||||
z = append(z, zap.Time("lastSync", *lastSync))
|
||||
}
|
||||
|
||||
w.logger.Info("starting data sync", z...)
|
||||
|
||||
url := types.SyncerURI{
|
||||
BaseURL: n.BaseURL,
|
||||
Path: basePath,
|
||||
Limit: w.limit,
|
||||
LastSync: lastSync,
|
||||
}
|
||||
|
||||
processer := &dataProcesser{
|
||||
ID: sm.ExternalFederationModuleID,
|
||||
ComposeModuleID: mappings.ComposeModuleID,
|
||||
ComposeNamespaceID: mappings.ComposeNamespaceID,
|
||||
ModuleMappings: &mappings.FieldMapping,
|
||||
ModuleMappingValues: &mappingValues,
|
||||
SyncService: w.syncService,
|
||||
User: u,
|
||||
Node: n,
|
||||
}
|
||||
|
||||
go w.queueUrl(&url, urls, processer)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (w *syncWorkerData) Watch(ctx context.Context, delay time.Duration, limit int) {
|
||||
var (
|
||||
urls = make(chan Url, 100)
|
||||
payloads = make(chan Payload, 10)
|
||||
|
||||
countProcess = 0
|
||||
)
|
||||
|
||||
w.delay = delay
|
||||
w.limit = limit
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
ctx = auth.SetIdentityToContext(ctx, auth.FederationUser())
|
||||
|
||||
ticker := time.NewTicker(delay)
|
||||
|
||||
w.PrepareForNodes(ctx, urls)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// do any cleanup here
|
||||
w.logger.Info("stopping sync", zap.Int("processed", countProcess))
|
||||
return
|
||||
case <-ticker.C:
|
||||
// do the whole process again
|
||||
w.PrepareForNodes(ctx, urls)
|
||||
case url := <-urls:
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// do any cleanup here
|
||||
w.logger.Info("stopping sync", zap.Int("processed", countProcess))
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
s, _ := url.Url.String()
|
||||
meta := url.Meta.(*dataProcesser)
|
||||
|
||||
// use the authToken from node pairing
|
||||
ctx = context.WithValue(ctx, FederationUserToken, meta.Node.AuthToken)
|
||||
|
||||
// TODO: Implement logic to handle last sync time before sending request
|
||||
|
||||
responseBody, err := w.syncService.FetchUrl(ctx, s)
|
||||
|
||||
if err != nil {
|
||||
w.logger.Error("could not fetch data from url, skipping",
|
||||
zap.Error(err),
|
||||
zap.String("url", s),
|
||||
logger.Uint64("nodeID", meta.Node.ID),
|
||||
zap.String("host", meta.Node.BaseURL))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
spayload := Payload{
|
||||
Payload: responseBody,
|
||||
Meta: url.Meta,
|
||||
}
|
||||
|
||||
payloads <- spayload
|
||||
case p := <-payloads:
|
||||
meta := p.Meta.(*dataProcesser)
|
||||
body, err := io.ReadAll(p.Payload)
|
||||
|
||||
// handle error
|
||||
if err != nil {
|
||||
w.logger.Error("could not read data from synced url, skipping",
|
||||
zap.Error(err),
|
||||
logger.Uint64("nodeID", meta.Node.ID),
|
||||
zap.String("host", meta.Node.BaseURL))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
basePath := fmt.Sprintf("/nodes/%d/modules/%d/records/", meta.Node.SharedNodeID, meta.ID)
|
||||
|
||||
u := types.SyncerURI{
|
||||
BaseURL: meta.NodeBaseURL,
|
||||
Path: basePath,
|
||||
Limit: w.limit,
|
||||
}
|
||||
|
||||
// the base path is okay
|
||||
processed, errProcess := w.syncService.ProcessPayload(ctx, body, urls, u, meta)
|
||||
|
||||
if response, ok := processed.(dataProcesserResponse); ok {
|
||||
countProcess += response.Processed
|
||||
} else {
|
||||
w.logger.Error("unexpected response type", zap.Any("response", processed))
|
||||
}
|
||||
|
||||
// error raised before the actual persist process
|
||||
// ignore
|
||||
syncStatus := types.NodeSyncStatusSuccess
|
||||
if errProcess != nil {
|
||||
syncStatus = types.NodeSyncStatusError
|
||||
}
|
||||
|
||||
new := &types.NodeSync{
|
||||
NodeID: meta.Node.ID,
|
||||
ModuleID: meta.ID,
|
||||
SyncStatus: syncStatus,
|
||||
SyncType: types.NodeSyncTypeData,
|
||||
TimeOfAction: time.Now().UTC(),
|
||||
}
|
||||
|
||||
new, err = DefaultNodeSync.Create(ctx, new)
|
||||
|
||||
if err != nil {
|
||||
w.logger.Info("could not update sync status", zap.Error(err))
|
||||
}
|
||||
|
||||
if errProcess != nil {
|
||||
w.logger.Info("error on persisting structure", zap.Error(errProcess))
|
||||
} else {
|
||||
w.logger.Info("processed objects",
|
||||
zap.Int("processed", processed.(dataProcesserResponse).Processed),
|
||||
logger.Uint64("nodeID", meta.Node.ID))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
230
server/federation/service/sync_worker_structure.go
Normal file
230
server/federation/service/sync_worker_structure.go
Normal file
@@ -0,0 +1,230 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/federation/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/auth"
|
||||
"github.com/cortezaproject/corteza/server/pkg/logger"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type (
|
||||
syncWorkerStructure struct {
|
||||
syncService *Sync
|
||||
logger *zap.Logger
|
||||
delay time.Duration
|
||||
limit int
|
||||
}
|
||||
)
|
||||
|
||||
func WorkerStructure(sync *Sync, logger *zap.Logger) *syncWorkerStructure {
|
||||
return &syncWorkerStructure{
|
||||
syncService: sync,
|
||||
logger: logger,
|
||||
limit: defaultPage,
|
||||
delay: defaultDelay,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *syncWorkerStructure) queueUrl(url *types.SyncerURI, urls chan Url, meta Processer) {
|
||||
t := Url{
|
||||
Url: *url,
|
||||
Meta: meta,
|
||||
}
|
||||
|
||||
w.syncService.QueueUrl(t, urls)
|
||||
}
|
||||
|
||||
func (w *syncWorkerStructure) PrepareForNodes(ctx context.Context, urls chan Url) {
|
||||
nodes, err := w.syncService.GetPairedNodes(ctx)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// get all shared modules and their module mappings
|
||||
for _, n := range nodes {
|
||||
// get the user, associated for this node
|
||||
u, err := w.syncService.LoadUserWithRoles(ctx, n.ID)
|
||||
|
||||
if err != nil {
|
||||
w.logger.Info("could not preload federation user, skipping",
|
||||
logger.Uint64("nodeID", n.ID),
|
||||
zap.Error(err))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// get the last sync per-node
|
||||
lastSync, _ := w.syncService.GetLastSyncTime(ctx, n.ID, types.NodeSyncTypeStructure)
|
||||
basePath := fmt.Sprintf("/nodes/%d/modules/exposed/", n.SharedNodeID)
|
||||
|
||||
z := []zap.Field{
|
||||
logger.Uint64("nodeID", n.ID),
|
||||
zap.String("host", n.BaseURL),
|
||||
zap.String("delay", w.delay.String()),
|
||||
zap.Int("pagesize", w.limit),
|
||||
}
|
||||
|
||||
if lastSync != nil {
|
||||
z = append(z, zap.Time("lastSync", *lastSync))
|
||||
}
|
||||
|
||||
w.logger.Info("starting structure sync", z...)
|
||||
|
||||
url := types.SyncerURI{
|
||||
BaseURL: n.BaseURL,
|
||||
Path: basePath,
|
||||
Limit: w.limit,
|
||||
LastSync: lastSync,
|
||||
}
|
||||
|
||||
processer := &structureProcesser{
|
||||
SyncService: w.syncService,
|
||||
User: u,
|
||||
Node: n,
|
||||
}
|
||||
|
||||
go w.queueUrl(&url, urls, processer)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *syncWorkerStructure) Watch(ctx context.Context, delay time.Duration, limit int) {
|
||||
var (
|
||||
urls = make(chan Url, 100)
|
||||
payloads = make(chan Payload, 10)
|
||||
|
||||
countProcess = 0
|
||||
)
|
||||
|
||||
w.delay = delay
|
||||
w.limit = limit
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
ctx = auth.SetIdentityToContext(ctx, auth.FederationUser())
|
||||
|
||||
ticker := time.NewTicker(delay)
|
||||
|
||||
w.PrepareForNodes(ctx, urls)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// do any cleanup here
|
||||
w.logger.Info("stopping sync", zap.Int("processed", countProcess))
|
||||
return
|
||||
case <-ticker.C:
|
||||
// do the whole process again
|
||||
w.PrepareForNodes(ctx, urls)
|
||||
case url := <-urls:
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// do any cleanup here
|
||||
w.logger.Info("stopping sync", zap.Int("processed", countProcess))
|
||||
return
|
||||
default:
|
||||
}
|
||||
s, _ := url.Url.String()
|
||||
meta := url.Meta.(*structureProcesser)
|
||||
|
||||
// use the authToken from node pairing
|
||||
ctx = context.WithValue(ctx, FederationUserToken, meta.Node.AuthToken)
|
||||
|
||||
responseBody, err := w.syncService.FetchUrl(ctx, s)
|
||||
|
||||
if err != nil {
|
||||
w.logger.Error("could not fetch data from url, skipping",
|
||||
zap.Error(err),
|
||||
zap.String("url", s),
|
||||
logger.Uint64("nodeID", meta.Node.ID),
|
||||
zap.String("host", meta.Node.BaseURL))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
spayload := Payload{
|
||||
Payload: responseBody,
|
||||
Meta: url.Meta,
|
||||
}
|
||||
|
||||
payloads <- spayload
|
||||
case p := <-payloads:
|
||||
meta := p.Meta.(*structureProcesser)
|
||||
body, err := ioutil.ReadAll(p.Payload)
|
||||
|
||||
// handle error
|
||||
if err != nil {
|
||||
w.logger.Error("could not read data from synced url, skipping",
|
||||
zap.Error(err),
|
||||
logger.Uint64("nodeID", meta.Node.ID),
|
||||
zap.String("host", meta.Node.BaseURL))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
basePath := fmt.Sprintf("/nodes/%d/modules/exposed/", meta.Node.SharedNodeID)
|
||||
|
||||
u := types.SyncerURI{
|
||||
BaseURL: p.Meta.(*structureProcesser).Node.BaseURL,
|
||||
Path: basePath,
|
||||
Limit: w.limit,
|
||||
}
|
||||
|
||||
processed, errProcess := w.syncService.ProcessPayload(ctx, body, urls, u, meta)
|
||||
|
||||
var processedModID uint64
|
||||
switch v := processed.(type) {
|
||||
case structureProcesserResponse:
|
||||
processedModID = v.ModuleID
|
||||
countProcess += v.Processed
|
||||
case int:
|
||||
countProcess += v
|
||||
}
|
||||
|
||||
// error raised before the actual persist process
|
||||
// ignore
|
||||
if moduleID := processedModID; moduleID == 0 {
|
||||
w.logger.Info("no structure change from last change, skipping",
|
||||
logger.Uint64("nodeID", meta.Node.ID),
|
||||
zap.String("host", meta.Node.BaseURL))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
syncStatus := types.NodeSyncStatusSuccess
|
||||
if errProcess != nil {
|
||||
syncStatus = types.NodeSyncStatusError
|
||||
}
|
||||
|
||||
new := &types.NodeSync{
|
||||
NodeID: meta.Node.ID,
|
||||
ModuleID: processedModID,
|
||||
SyncStatus: syncStatus,
|
||||
SyncType: types.NodeSyncTypeStructure,
|
||||
TimeOfAction: time.Now().UTC(),
|
||||
}
|
||||
|
||||
new, err = DefaultNodeSync.Create(ctx, new)
|
||||
|
||||
if err != nil {
|
||||
w.logger.Info("could not update sync status", zap.Error(err))
|
||||
}
|
||||
|
||||
if errProcess != nil {
|
||||
w.logger.Info("error on handling payload", zap.Error(errProcess))
|
||||
} else {
|
||||
w.logger.Info("processed objects",
|
||||
zap.Int("processed", processed.(structureProcesserResponse).Processed),
|
||||
logger.Uint64("nodeID", meta.Node.ID),
|
||||
zap.String("host", meta.Node.BaseURL))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
102
server/federation/service/syncer.go
Normal file
102
server/federation/service/syncer.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/federation/types"
|
||||
)
|
||||
|
||||
type (
|
||||
Syncer struct {
|
||||
client http.Client
|
||||
}
|
||||
|
||||
Url struct {
|
||||
Url types.SyncerURI
|
||||
Meta Processer
|
||||
}
|
||||
Payload struct {
|
||||
Payload io.Reader
|
||||
Meta Processer
|
||||
}
|
||||
|
||||
AuxResponseSet struct {
|
||||
Response struct {
|
||||
Filter struct {
|
||||
NodeID string
|
||||
ComposeModuleID string
|
||||
Query string
|
||||
Limit int
|
||||
NextPage string
|
||||
PrevPage string
|
||||
} `json:"filter"`
|
||||
} `json:"response"`
|
||||
}
|
||||
)
|
||||
|
||||
const FederationUserToken string = "authToken"
|
||||
|
||||
func (h *Syncer) Queue(url Url, out chan Url) {
|
||||
out <- url
|
||||
}
|
||||
|
||||
func (h *Syncer) Fetch(ctx context.Context, url string) (io.Reader, error) {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if authToken := ctx.Value(FederationUserToken); authToken != nil {
|
||||
req.Header.Add("Authorization", `Bearer `+authToken.(string))
|
||||
}
|
||||
|
||||
req.Header.Add("Accept", "application/json")
|
||||
|
||||
resp, err := h.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, errors.New(fmt.Sprintf("invalid return status: %d", resp.StatusCode))
|
||||
}
|
||||
|
||||
return resp.Body, nil
|
||||
}
|
||||
|
||||
func (h *Syncer) Process(ctx context.Context, payload []byte, out chan Url, url types.SyncerURI, processer Processer) (ProcesserResponse, error) {
|
||||
aux, err := h.ParseHeader(ctx, payload)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if aux.Response.Filter.NextPage != "" {
|
||||
url.NextPage = aux.Response.Filter.NextPage
|
||||
|
||||
out <- Url{
|
||||
Url: url,
|
||||
Meta: processer,
|
||||
}
|
||||
}
|
||||
return processer.Process(ctx, payload)
|
||||
}
|
||||
|
||||
func (h *Syncer) ParseHeader(ctx context.Context, payload []byte) (aux AuxResponseSet, err error) {
|
||||
err = json.Unmarshal(payload, &aux)
|
||||
return
|
||||
}
|
||||
|
||||
func NewSyncer() *Syncer {
|
||||
return &Syncer{
|
||||
client: http.Client{
|
||||
Timeout: time.Duration(3) * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
161
server/federation/service/syncer_test.go
Normal file
161
server/federation/service/syncer_test.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/federation/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type (
|
||||
RoundTripFunc func(req *http.Request) *http.Response
|
||||
|
||||
testProcesser struct{}
|
||||
)
|
||||
|
||||
func (p *testProcesser) Process(ctx context.Context, payload []byte) (ProcesserResponse, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func TestSyncer_process(t *testing.T) {
|
||||
var (
|
||||
req = require.New(t)
|
||||
syncer = &Syncer{}
|
||||
|
||||
ctx = context.Background()
|
||||
)
|
||||
|
||||
c := make(chan Url, 2)
|
||||
u := types.SyncerURI{
|
||||
Limit: 10,
|
||||
NextPage: "123",
|
||||
}
|
||||
tp := &testProcesser{}
|
||||
|
||||
go syncer.Process(ctx, []byte(`{"response":{"filter":{"limit":1, "nextPage":"456"}}}`), c, u, tp)
|
||||
|
||||
select {
|
||||
case url := <-c:
|
||||
req.Equal(url.Url.NextPage, "456")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncer_parseHeader(t *testing.T) {
|
||||
var (
|
||||
req = require.New(t)
|
||||
syncer = NewSyncer()
|
||||
|
||||
ctx = context.Background()
|
||||
)
|
||||
|
||||
c := make(chan Url, 2)
|
||||
u := types.SyncerURI{
|
||||
Limit: 10,
|
||||
NextPage: "123",
|
||||
}
|
||||
tp := &testProcesser{}
|
||||
|
||||
n, err := syncer.Process(ctx, []byte(`{"response":{"filt`), c, u, tp)
|
||||
|
||||
req.EqualError(err, "unexpected end of JSON input")
|
||||
req.Equal(n, 0)
|
||||
}
|
||||
|
||||
func TestSyncer_response(t *testing.T) {
|
||||
var (
|
||||
req = require.New(t)
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
expect string
|
||||
syncer *Syncer
|
||||
ctx context.Context
|
||||
}{
|
||||
{
|
||||
name: "non 200 request",
|
||||
url: "http://example.ltd",
|
||||
syncer: &Syncer{
|
||||
client: *NewHttpClient(func(req *http.Request) *http.Response {
|
||||
return &http.Response{
|
||||
StatusCode: 500,
|
||||
Body: ioutil.NopCloser(bytes.NewBufferString(`{"error":{"message":"common error response"}}`)),
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}),
|
||||
},
|
||||
ctx: context.Background(),
|
||||
expect: "invalid return status: 500",
|
||||
},
|
||||
{
|
||||
name: "invalid hostname",
|
||||
url: "http://examp le.ltd",
|
||||
syncer: &Syncer{
|
||||
client: *NewHttpClient(func(req *http.Request) *http.Response {
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: ioutil.NopCloser(bytes.NewBufferString(`{"error":{"message":"common error response"}}`)),
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}),
|
||||
},
|
||||
ctx: context.Background(),
|
||||
expect: "parse \"http://examp le.ltd\": invalid character \" \" in host name",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := tt.syncer.Fetch(tt.ctx, tt.url)
|
||||
req.EqualError(err, tt.expect)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestSyncer_authTokenCorrectlySet(t *testing.T) {
|
||||
var (
|
||||
req = require.New(t)
|
||||
expectedAuthToken = "TEST_JWT_TOKEN"
|
||||
actualAuthToken = ""
|
||||
)
|
||||
|
||||
ctx := context.WithValue(
|
||||
context.Background(),
|
||||
FederationUserToken,
|
||||
expectedAuthToken)
|
||||
|
||||
syncer := &Syncer{
|
||||
client: *NewHttpClient(func(r *http.Request) *http.Response {
|
||||
|
||||
actualAuthToken = r.Header.Get("Authorization")
|
||||
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: ioutil.NopCloser(bytes.NewBufferString("OK")),
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
_, err := syncer.Fetch(ctx, "http://example.ltd")
|
||||
|
||||
req.Equal("Bearer "+expectedAuthToken, actualAuthToken)
|
||||
req.NoError(err)
|
||||
}
|
||||
|
||||
func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return f(req), nil
|
||||
}
|
||||
|
||||
func NewHttpClient(rt RoundTripFunc) *http.Client {
|
||||
return &http.Client{
|
||||
Transport: RoundTripFunc(rt),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user