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:
113
server/pkg/envoy/store/api_gateway.go
Normal file
113
server/pkg/envoy/store/api_gateway.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
apiGateway struct {
|
||||
cfg *EncoderConfig
|
||||
|
||||
res *resource.APIGateway
|
||||
gwr *types.ApigwRoute
|
||||
ff types.ApigwFilterSet
|
||||
|
||||
ux *userIndex
|
||||
}
|
||||
apiGatewaySet []*apiGateway
|
||||
|
||||
apiGatewayFilter struct {
|
||||
cfg *EncoderConfig
|
||||
|
||||
res *resource.APIGatewayFilter
|
||||
tr *types.ApigwFilter
|
||||
}
|
||||
apiGatewayFilterSet []*apiGatewayFilter
|
||||
)
|
||||
|
||||
// mergeAPIGateways merges b into a, prioritising a
|
||||
func mergeAPIGateways(a, b *types.ApigwRoute) *types.ApigwRoute {
|
||||
c := a
|
||||
|
||||
if c.Endpoint == "" {
|
||||
c.Endpoint = b.Endpoint
|
||||
}
|
||||
if c.Method == "" {
|
||||
c.Method = b.Method
|
||||
}
|
||||
|
||||
c.Enabled = b.Enabled
|
||||
|
||||
if c.Group == 0 {
|
||||
c.Group = b.Group
|
||||
}
|
||||
|
||||
c.Meta = b.Meta
|
||||
|
||||
if c.CreatedBy == 0 {
|
||||
c.CreatedBy = b.CreatedBy
|
||||
}
|
||||
if c.UpdatedBy == 0 {
|
||||
c.UpdatedBy = b.UpdatedBy
|
||||
}
|
||||
if c.DeletedBy == 0 {
|
||||
c.DeletedBy = b.DeletedBy
|
||||
}
|
||||
|
||||
if c.CreatedAt.IsZero() {
|
||||
c.CreatedAt = b.CreatedAt
|
||||
}
|
||||
|
||||
if c.UpdatedAt == nil {
|
||||
c.UpdatedAt = b.UpdatedAt
|
||||
}
|
||||
|
||||
if c.DeletedAt == nil {
|
||||
c.DeletedAt = b.DeletedAt
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// findAPIGateway looks for the workflow in the resources & the store
|
||||
//
|
||||
// Provided resources are prioritized.
|
||||
func findAPIGateway(ctx context.Context, s store.Storer, rr resource.InterfaceSet, ii resource.Identifiers) (wf *types.ApigwRoute, err error) {
|
||||
wf = resource.FindAPIGateway(rr, ii)
|
||||
if wf != nil {
|
||||
return wf, nil
|
||||
}
|
||||
|
||||
return findAPIGatewayStore(ctx, s, makeGenericFilter(ii))
|
||||
}
|
||||
|
||||
// findAPIGatewayStore looks for the workflow in the store
|
||||
func findAPIGatewayStore(ctx context.Context, s store.Storer, gf genericFilter) (wf *types.ApigwRoute, err error) {
|
||||
if gf.id > 0 {
|
||||
wf, err = store.LookupApigwRouteByID(ctx, s, gf.id)
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if wf != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, i := range gf.identifiers {
|
||||
wf, err = store.LookupApigwRouteByEndpoint(ctx, s, i)
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if wf != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
215
server/pkg/envoy/store/api_gateway_marshal.go
Normal file
215
server/pkg/envoy/store/api_gateway_marshal.go
Normal file
@@ -0,0 +1,215 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
func newAPIGatewayFromResource(res *resource.APIGateway, cfg *EncoderConfig) resourceState {
|
||||
return &apiGateway{
|
||||
cfg: mergeConfig(cfg, res.Config()),
|
||||
|
||||
res: res,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *apiGateway) Prepare(ctx context.Context, pl *payload) (err error) {
|
||||
err = n.prepareRoute(ctx, pl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return n.prepareFilters(ctx, pl)
|
||||
}
|
||||
|
||||
func (n *apiGateway) prepareRoute(ctx context.Context, pl *payload) (err error) {
|
||||
// Reset old identifiers
|
||||
n.res.Res.ID = 0
|
||||
|
||||
// Try to get the original workflow
|
||||
n.gwr, err = findAPIGatewayStore(ctx, pl.s, makeGenericFilter(n.res.Identifiers()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if n.gwr != nil {
|
||||
n.res.Res.ID = n.gwr.ID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *apiGateway) prepareFilters(ctx context.Context, pl *payload) (err error) {
|
||||
if n.gwr == nil || n.gwr.ID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reset old identifiers
|
||||
for _, rf := range n.res.Filters {
|
||||
rf.Res.ID = 0
|
||||
}
|
||||
|
||||
// Try to find any related filters for this route
|
||||
tt, _, err := store.SearchApigwFilters(ctx, pl.s, types.ApigwFilterFilter{
|
||||
RouteID: n.gwr.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.ff = tt
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *apiGateway) Encode(ctx context.Context, pl *payload) (err error) {
|
||||
err = n.encodeRoute(ctx, pl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return n.encodeFilters(ctx, pl)
|
||||
}
|
||||
|
||||
func (n *apiGateway) encodeRoute(ctx context.Context, pl *payload) (err error) {
|
||||
res := n.res.Res
|
||||
exists := n.gwr != nil && n.gwr.ID > 0
|
||||
|
||||
// Determine the ID
|
||||
if res.ID <= 0 && exists {
|
||||
res.ID = n.gwr.ID
|
||||
}
|
||||
if res.ID <= 0 {
|
||||
res.ID = NextID()
|
||||
}
|
||||
|
||||
// Sys users
|
||||
us, err := resolveUserstamps(ctx, pl.s, pl.state.ParentResources, n.res.Userstamps())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ts := n.res.Timestamps()
|
||||
if ts != nil {
|
||||
if ts.CreatedAt != nil {
|
||||
res.CreatedAt = *ts.CreatedAt.T
|
||||
} else {
|
||||
res.CreatedAt = *now()
|
||||
}
|
||||
if ts.UpdatedAt != nil {
|
||||
res.UpdatedAt = ts.UpdatedAt.T
|
||||
}
|
||||
if ts.DeletedAt != nil {
|
||||
res.DeletedAt = ts.DeletedAt.T
|
||||
}
|
||||
}
|
||||
|
||||
res.CreatedBy = pl.invokerID
|
||||
if us != nil {
|
||||
if us.CreatedBy != nil {
|
||||
res.CreatedBy = us.CreatedBy.UserID
|
||||
}
|
||||
if us.UpdatedBy != nil {
|
||||
res.UpdatedBy = us.UpdatedBy.UserID
|
||||
}
|
||||
if us.DeletedBy != nil {
|
||||
res.DeletedBy = us.DeletedBy.UserID
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate the resource skip expression
|
||||
// @todo expand available parameters; similar implementation to automation/types/record@Dict
|
||||
if skip, err := basicSkipEval(ctx, n.cfg, !exists); err != nil {
|
||||
return err
|
||||
} else if skip {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a fresh workflow
|
||||
if !exists {
|
||||
return store.CreateApigwRoute(ctx, pl.s, res)
|
||||
}
|
||||
|
||||
// Update existing workflow
|
||||
switch n.cfg.OnExisting {
|
||||
case resource.Skip:
|
||||
return nil
|
||||
|
||||
case resource.MergeLeft:
|
||||
res = mergeAPIGateways(n.gwr, res)
|
||||
|
||||
case resource.MergeRight:
|
||||
res = mergeAPIGateways(res, n.gwr)
|
||||
}
|
||||
|
||||
err = store.UpdateApigwRoute(ctx, pl.s, res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.res.Res = res
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *apiGateway) encodeFilters(ctx context.Context, pl *payload) (err error) {
|
||||
exists := len(n.ff) > 0
|
||||
ff := make([]*types.ApigwFilter, 0, len(n.res.Filters))
|
||||
|
||||
for _, rf := range n.res.Filters {
|
||||
res := rf.Res
|
||||
res.Route = n.res.Res.ID
|
||||
res.ID = NextID()
|
||||
|
||||
// Sys users
|
||||
us, err := resolveUserstamps(ctx, pl.s, pl.state.ParentResources, rf.Userstamps())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ts := rf.Timestamps()
|
||||
if ts != nil {
|
||||
if ts.CreatedAt != nil {
|
||||
res.CreatedAt = *ts.CreatedAt.T
|
||||
} else {
|
||||
res.CreatedAt = *now()
|
||||
}
|
||||
if ts.UpdatedAt != nil {
|
||||
res.UpdatedAt = ts.UpdatedAt.T
|
||||
}
|
||||
if ts.DeletedAt != nil {
|
||||
res.DeletedAt = ts.DeletedAt.T
|
||||
}
|
||||
}
|
||||
res.CreatedBy = pl.invokerID
|
||||
if us != nil {
|
||||
if us.CreatedBy != nil {
|
||||
res.CreatedBy = us.CreatedBy.UserID
|
||||
}
|
||||
if us.UpdatedBy != nil {
|
||||
res.UpdatedBy = us.UpdatedBy.UserID
|
||||
}
|
||||
if us.DeletedBy != nil {
|
||||
res.DeletedBy = us.DeletedBy.UserID
|
||||
}
|
||||
}
|
||||
|
||||
ff = append(ff, res)
|
||||
}
|
||||
|
||||
// Create a fresh workflow
|
||||
if !exists {
|
||||
return store.CreateApigwFilter(ctx, pl.s, ff...)
|
||||
}
|
||||
|
||||
// If these filters already exist and we wish to modify them,
|
||||
// remove the old ones and create new ones
|
||||
switch n.cfg.OnExisting {
|
||||
case resource.Skip,
|
||||
resource.MergeLeft:
|
||||
return nil
|
||||
}
|
||||
|
||||
err = store.DeleteApigwFilter(ctx, pl.s, n.ff...)
|
||||
return store.CreateApigwFilter(ctx, pl.s, ff...)
|
||||
}
|
||||
30
server/pkg/envoy/store/api_gateway_unmarshal.go
Normal file
30
server/pkg/envoy/store/api_gateway_unmarshal.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
func newAPIGateway(gwr *types.ApigwRoute, ff types.ApigwFilterSet, ux *userIndex) *apiGateway {
|
||||
return &apiGateway{
|
||||
gwr: gwr,
|
||||
ff: ff,
|
||||
|
||||
ux: ux,
|
||||
}
|
||||
}
|
||||
|
||||
func (awf *apiGateway) MarshalEnvoy() ([]resource.Interface, error) {
|
||||
rs := resource.NewAPIGateway(awf.gwr)
|
||||
syncUserStamps(rs.Userstamps(), awf.ux)
|
||||
|
||||
for _, f := range awf.ff {
|
||||
rt := rs.AddGatewayFilter(f)
|
||||
syncUserStamps(rt.Userstamps(), awf.ux)
|
||||
}
|
||||
|
||||
return envoy.CollectNodes(
|
||||
rs,
|
||||
)
|
||||
}
|
||||
90
server/pkg/envoy/store/application.go
Normal file
90
server/pkg/envoy/store/application.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
application struct {
|
||||
cfg *EncoderConfig
|
||||
|
||||
res *resource.Application
|
||||
app *types.Application
|
||||
|
||||
ux *userIndex
|
||||
}
|
||||
)
|
||||
|
||||
// mergeApplications merges b into a, prioritising a
|
||||
func mergeApplications(a, b *types.Application) *types.Application {
|
||||
c := *a
|
||||
|
||||
if c.Name == "" {
|
||||
c.Name = b.Name
|
||||
}
|
||||
c.OwnerID = b.OwnerID
|
||||
|
||||
if c.CreatedAt.IsZero() {
|
||||
c.CreatedAt = b.CreatedAt
|
||||
}
|
||||
if c.UpdatedAt == nil {
|
||||
c.UpdatedAt = b.UpdatedAt
|
||||
}
|
||||
if c.DeletedAt == nil {
|
||||
c.DeletedAt = b.DeletedAt
|
||||
}
|
||||
|
||||
// I'll just compare the entire struct for now
|
||||
if c.Unify == nil || *c.Unify == (types.ApplicationUnify{}) {
|
||||
c.Unify = b.Unify
|
||||
}
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
// findApplication looks for the app in the resources & the store
|
||||
//
|
||||
// Provided resources are prioritized.
|
||||
func findApplication(ctx context.Context, s store.Storer, rr resource.InterfaceSet, ii resource.Identifiers) (ap *types.Application, err error) {
|
||||
ap = resource.FindApplication(rr, ii)
|
||||
if ap != nil {
|
||||
return ap, nil
|
||||
}
|
||||
|
||||
return findApplicationStore(ctx, s, makeGenericFilter(ii))
|
||||
}
|
||||
|
||||
// findApplicationStore looks for the app in the store
|
||||
func findApplicationStore(ctx context.Context, s store.Storer, gf genericFilter) (ap *types.Application, err error) {
|
||||
if gf.id > 0 {
|
||||
ap, err = store.LookupApplicationByID(ctx, s, gf.id)
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ap != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, i := range gf.identifiers {
|
||||
var aa types.ApplicationSet
|
||||
aa, _, err = store.SearchApplications(ctx, s, types.ApplicationFilter{Name: i})
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
if len(aa) > 1 {
|
||||
return nil, resourceErrIdentifierNotUnique(i)
|
||||
}
|
||||
if len(aa) == 1 {
|
||||
ap = aa[0]
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
108
server/pkg/envoy/store/application_marshal.go
Normal file
108
server/pkg/envoy/store/application_marshal.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
func NewApplicationFromResource(res *resource.Application, cfg *EncoderConfig) resourceState {
|
||||
return &application{
|
||||
cfg: mergeConfig(cfg, res.Config()),
|
||||
res: res,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *application) Prepare(ctx context.Context, pl *payload) (err error) {
|
||||
// Get the existing app
|
||||
n.app, err = findApplicationStore(ctx, pl.s, makeGenericFilter(n.res.Identifiers()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if n.app != nil {
|
||||
n.res.Res.ID = n.app.ID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *application) Encode(ctx context.Context, pl *payload) (err error) {
|
||||
res := n.res.Res
|
||||
exists := n.app != nil && n.app.ID > 0
|
||||
|
||||
// Determine the ID
|
||||
if res.ID <= 0 && exists {
|
||||
res.ID = n.app.ID
|
||||
}
|
||||
if res.ID <= 0 {
|
||||
res.ID = NextID()
|
||||
}
|
||||
|
||||
// Sys users
|
||||
us, err := resolveUserstamps(ctx, pl.s, pl.state.ParentResources, n.res.Userstamps())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Unify
|
||||
if res.Unify == nil {
|
||||
res.Unify = &types.ApplicationUnify{}
|
||||
}
|
||||
|
||||
ts := n.res.Timestamps()
|
||||
if ts != nil {
|
||||
if ts.CreatedAt != nil {
|
||||
res.CreatedAt = *ts.CreatedAt.T
|
||||
} else {
|
||||
res.CreatedAt = *now()
|
||||
}
|
||||
if ts.UpdatedAt != nil {
|
||||
res.UpdatedAt = ts.UpdatedAt.T
|
||||
}
|
||||
if ts.DeletedAt != nil {
|
||||
res.DeletedAt = ts.DeletedAt.T
|
||||
}
|
||||
}
|
||||
|
||||
if us != nil {
|
||||
if us.OwnedBy != nil {
|
||||
res.OwnerID = us.OwnedBy.UserID
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate the resource skip expression
|
||||
// @todo expand available parameters; similar implementation to compose/types/record@Dict
|
||||
if skip, err := basicSkipEval(ctx, n.cfg, !exists); err != nil {
|
||||
return err
|
||||
} else if skip {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create fresh application
|
||||
if !exists {
|
||||
return store.CreateApplication(ctx, pl.s, res)
|
||||
}
|
||||
|
||||
// Update existing application
|
||||
switch n.cfg.OnExisting {
|
||||
case resource.Skip:
|
||||
return nil
|
||||
|
||||
case resource.MergeLeft:
|
||||
res = mergeApplications(n.app, res)
|
||||
|
||||
case resource.MergeRight:
|
||||
res = mergeApplications(res, n.app)
|
||||
}
|
||||
|
||||
err = store.UpdateApplication(ctx, pl.s, res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.res.Res = res
|
||||
return nil
|
||||
}
|
||||
54
server/pkg/envoy/store/application_test.go
Normal file
54
server/pkg/envoy/store/application_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestApplication_Merger(t *testing.T) {
|
||||
req := require.New(t)
|
||||
|
||||
ub := &types.ApplicationUnify{
|
||||
Config: "config",
|
||||
Icon: "icon",
|
||||
}
|
||||
|
||||
now := time.Time{}
|
||||
nowP := &time.Time{}
|
||||
|
||||
empty := &types.Application{}
|
||||
full := &types.Application{
|
||||
Name: "name",
|
||||
OwnerID: 1,
|
||||
Enabled: true,
|
||||
Unify: ub,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: nowP,
|
||||
DeletedAt: nowP,
|
||||
}
|
||||
|
||||
t.Run("merge on empty", func(t *testing.T) {
|
||||
c := mergeApplications(empty, full)
|
||||
req.Equal("name", c.Name)
|
||||
req.Equal(uint64(1), c.OwnerID)
|
||||
req.False(c.Enabled)
|
||||
req.Equal(ub, c.Unify)
|
||||
req.Equal(now, c.CreatedAt)
|
||||
req.Equal(nowP, c.UpdatedAt)
|
||||
req.Equal(nowP, c.DeletedAt)
|
||||
})
|
||||
|
||||
t.Run("merge with empty", func(t *testing.T) {
|
||||
c := mergeApplications(full, empty)
|
||||
req.Equal("name", c.Name)
|
||||
req.Equal(uint64(0), c.OwnerID)
|
||||
req.True(c.Enabled)
|
||||
req.Equal(ub, c.Unify)
|
||||
req.Equal(now, c.CreatedAt)
|
||||
req.Equal(nowP, c.UpdatedAt)
|
||||
req.Equal(nowP, c.DeletedAt)
|
||||
})
|
||||
}
|
||||
23
server/pkg/envoy/store/application_unmarshal.go
Normal file
23
server/pkg/envoy/store/application_unmarshal.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
func newApplication(app *types.Application, ux *userIndex) *application {
|
||||
return &application{
|
||||
app: app,
|
||||
ux: ux,
|
||||
}
|
||||
}
|
||||
|
||||
func (app *application) MarshalEnvoy() ([]resource.Interface, error) {
|
||||
rs := resource.NewApplication(app.app)
|
||||
syncUserStamps(rs.Userstamps(), app.ux)
|
||||
|
||||
return envoy.CollectNodes(
|
||||
rs,
|
||||
)
|
||||
}
|
||||
176
server/pkg/envoy/store/automation.go
Normal file
176
server/pkg/envoy/store/automation.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/automation/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/cortezaproject/corteza/server/pkg/id"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
)
|
||||
|
||||
type (
|
||||
automationWorkflowFilter types.WorkflowFilter
|
||||
|
||||
automationStore interface {
|
||||
store.AutomationWorkflows
|
||||
store.AutomationTriggers
|
||||
}
|
||||
|
||||
automationDecoder struct {
|
||||
resourceID []uint64
|
||||
ux *userIndex
|
||||
}
|
||||
)
|
||||
|
||||
func newAutomationDecoder(ux *userIndex) *automationDecoder {
|
||||
return &automationDecoder{
|
||||
resourceID: make([]uint64, 0, 200),
|
||||
ux: ux,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *automationDecoder) decodeWorkflows(ctx context.Context, s automationStore, ff []*automationWorkflowFilter) *auxRsp {
|
||||
mm := make([]envoy.Marshaller, 0, 100)
|
||||
if ff == nil {
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
var nn types.WorkflowSet
|
||||
var fn types.WorkflowFilter
|
||||
var err error
|
||||
|
||||
for _, f := range ff {
|
||||
aux := *f
|
||||
|
||||
if aux.Limit == 0 {
|
||||
aux.Limit = 1000
|
||||
}
|
||||
|
||||
for {
|
||||
nn, fn, err = s.SearchAutomationWorkflows(ctx, types.WorkflowFilter(aux))
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
for _, n := range nn {
|
||||
// Index users
|
||||
err = d.ux.add(
|
||||
ctx,
|
||||
n.RunAs,
|
||||
n.OwnedBy,
|
||||
n.CreatedBy,
|
||||
n.UpdatedBy,
|
||||
n.DeletedBy,
|
||||
)
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
tt, _, err := s.SearchAutomationTriggers(ctx, types.TriggerFilter{
|
||||
WorkflowID: id.Strings(n.ID),
|
||||
Disabled: filter.StateInclusive,
|
||||
})
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
for _, t := range tt {
|
||||
// Index users
|
||||
err = d.ux.add(
|
||||
ctx,
|
||||
t.OwnedBy,
|
||||
t.CreatedBy,
|
||||
t.UpdatedBy,
|
||||
t.DeletedBy,
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
mm = append(mm, newAutomationWorkflow(n, tt, d.ux))
|
||||
d.resourceID = append(d.resourceID, n.ID)
|
||||
}
|
||||
|
||||
if fn.NextPage != nil {
|
||||
aux.PageCursor = fn.NextPage
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
func (df *DecodeFilter) automationFromResource(rr ...string) *DecodeFilter {
|
||||
for _, r := range rr {
|
||||
if !strings.HasPrefix(r, "automation") {
|
||||
continue
|
||||
}
|
||||
|
||||
id := ""
|
||||
if strings.Count(r, ":") == 2 && !strings.HasSuffix(r, "*") {
|
||||
// There is an identifier
|
||||
aux := strings.Split(r, ":")
|
||||
|
||||
id = aux[len(aux)-1]
|
||||
r = strings.Join(aux[:len(aux)-1], ":")
|
||||
}
|
||||
|
||||
switch strings.ToLower(r) {
|
||||
case "automation:workflow":
|
||||
df = df.AutomationWorkflows(&types.WorkflowFilter{
|
||||
Query: id,
|
||||
Disabled: filter.StateInclusive,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return df
|
||||
}
|
||||
|
||||
func (df *DecodeFilter) automationFromRef(rr ...*resource.Ref) *DecodeFilter {
|
||||
for _, r := range rr {
|
||||
if strings.Index(r.ResourceType, "automation") < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
switch r.ResourceType {
|
||||
case types.WorkflowResourceType:
|
||||
for _, i := range r.Identifiers.StringSlice() {
|
||||
df = df.AutomationWorkflows(&types.WorkflowFilter{
|
||||
Query: i,
|
||||
Disabled: filter.StateInclusive,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return df
|
||||
}
|
||||
|
||||
// AutomationWorkflows adds a new WorkflowFilter
|
||||
func (df *DecodeFilter) AutomationWorkflows(f *types.WorkflowFilter) *DecodeFilter {
|
||||
if df.automationWorkflow == nil {
|
||||
df.automationWorkflow = make([]*automationWorkflowFilter, 0, 1)
|
||||
}
|
||||
df.automationWorkflow = append(df.automationWorkflow, (*automationWorkflowFilter)(f))
|
||||
return df
|
||||
}
|
||||
138
server/pkg/envoy/store/automation_workflow.go
Normal file
138
server/pkg/envoy/store/automation_workflow.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/automation/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
)
|
||||
|
||||
type (
|
||||
automationWorkflow struct {
|
||||
cfg *EncoderConfig
|
||||
|
||||
res *resource.AutomationWorkflow
|
||||
wf *types.Workflow
|
||||
tt types.TriggerSet
|
||||
|
||||
ux *userIndex
|
||||
}
|
||||
automationWorkflowSet []*automationWorkflow
|
||||
|
||||
automationTrigger struct {
|
||||
cfg *EncoderConfig
|
||||
|
||||
res *resource.AutomationTrigger
|
||||
tr *types.Trigger
|
||||
}
|
||||
automationTriggerSet []*automationTrigger
|
||||
)
|
||||
|
||||
// mergeAutomationWorkflows merges b into a, prioritising a
|
||||
func mergeAutomationWorkflows(a, b *types.Workflow) *types.Workflow {
|
||||
c := a
|
||||
|
||||
if c.Handle == "" {
|
||||
c.Handle = b.Handle
|
||||
}
|
||||
if c.Meta == nil {
|
||||
c.Meta = b.Meta
|
||||
}
|
||||
|
||||
if c.Scope == nil {
|
||||
c.Scope = b.Scope
|
||||
}
|
||||
if c.Steps == nil {
|
||||
c.Steps = b.Steps
|
||||
}
|
||||
if c.Paths == nil {
|
||||
c.Paths = b.Paths
|
||||
}
|
||||
|
||||
if c.RunAs == 0 {
|
||||
c.RunAs = b.RunAs
|
||||
}
|
||||
if c.OwnedBy == 0 {
|
||||
c.OwnedBy = b.OwnedBy
|
||||
}
|
||||
if c.CreatedBy == 0 {
|
||||
c.CreatedBy = b.CreatedBy
|
||||
}
|
||||
if c.UpdatedBy == 0 {
|
||||
c.UpdatedBy = b.UpdatedBy
|
||||
}
|
||||
if c.DeletedBy == 0 {
|
||||
c.DeletedBy = b.DeletedBy
|
||||
}
|
||||
|
||||
if c.CreatedAt.IsZero() {
|
||||
c.CreatedAt = b.CreatedAt
|
||||
}
|
||||
|
||||
if c.UpdatedAt == nil {
|
||||
c.UpdatedAt = b.UpdatedAt
|
||||
}
|
||||
|
||||
if c.DeletedAt == nil {
|
||||
c.DeletedAt = b.DeletedAt
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// findAutomationWorkflow looks for the workflow in the resources & the store
|
||||
//
|
||||
// Provided resources are prioritized.
|
||||
func findAutomationWorkflow(ctx context.Context, s store.Storer, rr resource.InterfaceSet, ii resource.Identifiers) (wf *types.Workflow, err error) {
|
||||
wf = resource.FindAutomationWorkflow(rr, ii)
|
||||
if wf != nil {
|
||||
return wf, nil
|
||||
}
|
||||
|
||||
return findAutomationWorkflowStore(ctx, s, makeGenericFilter(ii))
|
||||
}
|
||||
|
||||
// findAutomationWorkflowStore looks for the workflow in the store
|
||||
func findAutomationWorkflowStore(ctx context.Context, s store.Storer, gf genericFilter) (wf *types.Workflow, err error) {
|
||||
if gf.id > 0 {
|
||||
wf, err = store.LookupAutomationWorkflowByID(ctx, s, gf.id)
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if wf != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, i := range gf.identifiers {
|
||||
wf, err = store.LookupAutomationWorkflowByHandle(ctx, s, i)
|
||||
if err == store.ErrNotFound {
|
||||
var nn types.WorkflowSet
|
||||
nn, _, err = store.SearchAutomationWorkflows(ctx, s, types.WorkflowFilter{
|
||||
Query: i,
|
||||
Paging: filter.Paging{
|
||||
Limit: 2,
|
||||
},
|
||||
})
|
||||
if len(nn) > 1 {
|
||||
return nil, resourceErrIdentifierNotUnique(i)
|
||||
}
|
||||
if len(nn) == 1 {
|
||||
wf = nn[0]
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if wf != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
239
server/pkg/envoy/store/automation_workflow_marshal.go
Normal file
239
server/pkg/envoy/store/automation_workflow_marshal.go
Normal file
@@ -0,0 +1,239 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/automation/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/cortezaproject/corteza/server/pkg/id"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
)
|
||||
|
||||
func newAutomationWorkflowFromResource(res *resource.AutomationWorkflow, cfg *EncoderConfig) resourceState {
|
||||
return &automationWorkflow{
|
||||
cfg: mergeConfig(cfg, res.Config()),
|
||||
|
||||
res: res,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *automationWorkflow) Prepare(ctx context.Context, pl *payload) (err error) {
|
||||
err = n.prepareWorkflows(ctx, pl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return n.prepareTriggers(ctx, pl)
|
||||
}
|
||||
|
||||
func (n *automationWorkflow) prepareWorkflows(ctx context.Context, pl *payload) (err error) {
|
||||
// Reset old identifiers
|
||||
n.res.Res.ID = 0
|
||||
|
||||
// Try to get the original workflow
|
||||
n.wf, err = findAutomationWorkflowStore(ctx, pl.s, makeGenericFilter(n.res.Identifiers()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if n.wf != nil {
|
||||
n.res.Res.ID = n.wf.ID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *automationWorkflow) prepareTriggers(ctx context.Context, pl *payload) (err error) {
|
||||
if n.wf == nil || n.wf.ID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reset old identifiers
|
||||
for _, t := range n.res.Triggers {
|
||||
t.Res.ID = 0
|
||||
t.Res.WorkflowID = 0
|
||||
}
|
||||
|
||||
// Try to find any related triggers for this workflow
|
||||
tt, _, err := store.SearchAutomationTriggers(ctx, pl.s, types.TriggerFilter{
|
||||
WorkflowID: id.Strings(n.wf.ID),
|
||||
Disabled: filter.StateInclusive,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.tt = tt
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *automationWorkflow) Encode(ctx context.Context, pl *payload) (err error) {
|
||||
err = n.encodeWorkflow(ctx, pl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return n.encodeTriggers(ctx, pl)
|
||||
}
|
||||
|
||||
func (n *automationWorkflow) encodeWorkflow(ctx context.Context, pl *payload) (err error) {
|
||||
res := n.res.Res
|
||||
exists := n.wf != nil && n.wf.ID > 0
|
||||
|
||||
// Determine the ID
|
||||
if res.ID <= 0 && exists {
|
||||
res.ID = n.wf.ID
|
||||
}
|
||||
if res.ID <= 0 {
|
||||
res.ID = NextID()
|
||||
}
|
||||
|
||||
// Sys users
|
||||
us, err := resolveUserstamps(ctx, pl.s, pl.state.ParentResources, n.res.Userstamps())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ts := n.res.Timestamps()
|
||||
if ts != nil {
|
||||
if ts.CreatedAt != nil {
|
||||
res.CreatedAt = *ts.CreatedAt.T
|
||||
} else {
|
||||
res.CreatedAt = *now()
|
||||
}
|
||||
if ts.UpdatedAt != nil {
|
||||
res.UpdatedAt = ts.UpdatedAt.T
|
||||
}
|
||||
if ts.DeletedAt != nil {
|
||||
res.DeletedAt = ts.DeletedAt.T
|
||||
}
|
||||
}
|
||||
|
||||
res.CreatedBy = pl.invokerID
|
||||
if us != nil {
|
||||
if us.OwnedBy != nil {
|
||||
res.OwnedBy = us.OwnedBy.UserID
|
||||
}
|
||||
if us.CreatedBy != nil {
|
||||
res.CreatedBy = us.CreatedBy.UserID
|
||||
}
|
||||
if us.UpdatedBy != nil {
|
||||
res.UpdatedBy = us.UpdatedBy.UserID
|
||||
}
|
||||
if us.DeletedBy != nil {
|
||||
res.DeletedBy = us.DeletedBy.UserID
|
||||
}
|
||||
if us.RunAs != nil {
|
||||
res.RunAs = us.RunAs.UserID
|
||||
}
|
||||
}
|
||||
|
||||
res.Steps = make(types.WorkflowStepSet, 0, 100)
|
||||
for _, sres := range n.res.Steps {
|
||||
res.Steps = append(res.Steps, sres.Res)
|
||||
}
|
||||
|
||||
res.Paths = make(types.WorkflowPathSet, 0, 100)
|
||||
for _, pres := range n.res.Paths {
|
||||
p := pres.Res
|
||||
res.Paths = append(res.Paths, p)
|
||||
}
|
||||
|
||||
// Evaluate the resource skip expression
|
||||
// @todo expand available parameters; similar implementation to automation/types/record@Dict
|
||||
if skip, err := basicSkipEval(ctx, n.cfg, !exists); err != nil {
|
||||
return err
|
||||
} else if skip {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a fresh workflow
|
||||
if !exists {
|
||||
return store.CreateAutomationWorkflow(ctx, pl.s, res)
|
||||
}
|
||||
|
||||
// Update existing workflow
|
||||
switch n.cfg.OnExisting {
|
||||
case resource.Skip:
|
||||
return nil
|
||||
|
||||
case resource.MergeLeft:
|
||||
res = mergeAutomationWorkflows(n.wf, res)
|
||||
|
||||
case resource.MergeRight:
|
||||
res = mergeAutomationWorkflows(res, n.wf)
|
||||
}
|
||||
|
||||
err = store.UpdateAutomationWorkflow(ctx, pl.s, res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.res.Res = res
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *automationWorkflow) encodeTriggers(ctx context.Context, pl *payload) (err error) {
|
||||
exists := len(n.tt) > 0
|
||||
rr := make([]*types.Trigger, 0, len(n.res.Triggers))
|
||||
|
||||
for _, tr := range n.res.Triggers {
|
||||
res := tr.Res
|
||||
res.WorkflowID = n.res.Res.ID
|
||||
res.ID = NextID()
|
||||
|
||||
// Sys users
|
||||
us, err := resolveUserstamps(ctx, pl.s, pl.state.ParentResources, tr.Userstamps())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ts := tr.Timestamps()
|
||||
if ts != nil {
|
||||
if ts.CreatedAt != nil {
|
||||
res.CreatedAt = *ts.CreatedAt.T
|
||||
} else {
|
||||
res.CreatedAt = *now()
|
||||
}
|
||||
if ts.UpdatedAt != nil {
|
||||
res.UpdatedAt = ts.UpdatedAt.T
|
||||
}
|
||||
if ts.DeletedAt != nil {
|
||||
res.DeletedAt = ts.DeletedAt.T
|
||||
}
|
||||
}
|
||||
res.CreatedBy = pl.invokerID
|
||||
if us != nil {
|
||||
if us.OwnedBy != nil {
|
||||
res.OwnedBy = us.OwnedBy.UserID
|
||||
}
|
||||
if us.CreatedBy != nil {
|
||||
res.CreatedBy = us.CreatedBy.UserID
|
||||
}
|
||||
if us.UpdatedBy != nil {
|
||||
res.UpdatedBy = us.UpdatedBy.UserID
|
||||
}
|
||||
if us.DeletedBy != nil {
|
||||
res.DeletedBy = us.DeletedBy.UserID
|
||||
}
|
||||
}
|
||||
|
||||
rr = append(rr, res)
|
||||
}
|
||||
|
||||
// Create a fresh workflow
|
||||
if !exists {
|
||||
return store.CreateAutomationTrigger(ctx, pl.s, rr...)
|
||||
}
|
||||
|
||||
// If these triggers already exist and we wish to modify them,
|
||||
// remove the old ones and create new ones
|
||||
switch n.cfg.OnExisting {
|
||||
case resource.Skip,
|
||||
resource.MergeLeft:
|
||||
return nil
|
||||
}
|
||||
|
||||
err = store.DeleteAutomationTrigger(ctx, pl.s, n.tt...)
|
||||
return store.CreateAutomationTrigger(ctx, pl.s, rr...)
|
||||
}
|
||||
38
server/pkg/envoy/store/automation_workflow_unmarshal.go
Normal file
38
server/pkg/envoy/store/automation_workflow_unmarshal.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/automation/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
)
|
||||
|
||||
func newAutomationWorkflow(wf *types.Workflow, tt types.TriggerSet, ux *userIndex) *automationWorkflow {
|
||||
return &automationWorkflow{
|
||||
wf: wf,
|
||||
tt: tt,
|
||||
|
||||
ux: ux,
|
||||
}
|
||||
}
|
||||
|
||||
func (awf *automationWorkflow) MarshalEnvoy() ([]resource.Interface, error) {
|
||||
rs := resource.NewAutomationWorkflow(awf.wf)
|
||||
syncUserStamps(rs.Userstamps(), awf.ux)
|
||||
|
||||
for _, t := range awf.tt {
|
||||
rt := rs.AddAutomationTrigger(t)
|
||||
syncUserStamps(rt.Userstamps(), awf.ux)
|
||||
}
|
||||
|
||||
for _, s := range awf.wf.Steps {
|
||||
rs.AddAutomationWorkflowStep(s)
|
||||
}
|
||||
|
||||
for _, p := range awf.wf.Paths {
|
||||
rs.AddAutomationWorkflowPath(p)
|
||||
}
|
||||
|
||||
return envoy.CollectNodes(
|
||||
rs,
|
||||
)
|
||||
}
|
||||
579
server/pkg/envoy/store/compose.go
Normal file
579
server/pkg/envoy/store/compose.go
Normal file
@@ -0,0 +1,579 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/dalutils"
|
||||
"github.com/cortezaproject/corteza/server/compose/service"
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/dal"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
stypes "github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
composeNamespaceFilter types.NamespaceFilter
|
||||
composeModuleFilter types.ModuleFilter
|
||||
composeRecordFilter types.RecordFilter
|
||||
composePageFilter types.PageFilter
|
||||
composeChartFilter types.ChartFilter
|
||||
|
||||
composeStore interface {
|
||||
store.ComposeAttachments
|
||||
store.ComposeCharts
|
||||
store.ComposeModuleFields
|
||||
store.ComposeModules
|
||||
store.ComposeNamespaces
|
||||
store.ComposePages
|
||||
}
|
||||
|
||||
dalService interface {
|
||||
Create(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, vv ...dal.ValueGetter) error
|
||||
Search(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, f filter.Filter) (dal.Iterator, error)
|
||||
Delete(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, pkv ...dal.ValueGetter) (err error)
|
||||
Update(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, pkv ...dal.ValueGetter) (err error)
|
||||
|
||||
ReplaceModel(ctx context.Context, currentAlts []*dal.Alteration, model *dal.Model) (newAlts []*dal.Alteration, err error)
|
||||
GetConnectionByID(uint64) *dal.ConnectionWrap
|
||||
}
|
||||
|
||||
composeDecoder struct {
|
||||
resourceID []uint64
|
||||
namespaceID []uint64
|
||||
|
||||
dal dalService
|
||||
}
|
||||
)
|
||||
|
||||
func newComposeDecoder(dal dalService) *composeDecoder {
|
||||
return &composeDecoder{
|
||||
resourceID: make([]uint64, 0, 200),
|
||||
namespaceID: make([]uint64, 0, 200),
|
||||
dal: dal,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *composeDecoder) decodeComposeNamespace(ctx context.Context, s composeStore, ff []*composeNamespaceFilter) *auxRsp {
|
||||
mm := make([]envoy.Marshaller, 0, 100)
|
||||
if ff == nil {
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
var nn types.NamespaceSet
|
||||
var fn types.NamespaceFilter
|
||||
var err error
|
||||
for _, f := range ff {
|
||||
aux := *f
|
||||
|
||||
if aux.Limit == 0 {
|
||||
aux.Limit = 1000
|
||||
}
|
||||
|
||||
for {
|
||||
nn, fn, err = s.SearchComposeNamespaces(ctx, types.NamespaceFilter(aux))
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
for _, n := range nn {
|
||||
d.namespaceID = append(d.namespaceID, n.ID)
|
||||
d.resourceID = append(d.resourceID, n.ID)
|
||||
|
||||
mm = append(mm, newComposeNamespace(n))
|
||||
}
|
||||
|
||||
if f.NextPage != nil {
|
||||
aux.PageCursor = fn.NextPage
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *composeDecoder) decodeComposeModule(ctx context.Context, s composeStore, ff []*composeModuleFilter) *auxRsp {
|
||||
mm := make([]envoy.Marshaller, 0, 100)
|
||||
if ff == nil {
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
if len(d.namespaceID) > 0 {
|
||||
ffNs := make([]*composeModuleFilter, 0, len(ff)+len(d.namespaceID))
|
||||
for _, nsID := range d.namespaceID {
|
||||
for _, f := range ff {
|
||||
fNs := *f
|
||||
fNs.NamespaceID = nsID
|
||||
ffNs = append(ffNs, &fNs)
|
||||
}
|
||||
}
|
||||
ff = ffNs
|
||||
}
|
||||
|
||||
var nn types.ModuleSet
|
||||
var fn types.ModuleFilter
|
||||
var err error
|
||||
for _, f := range ff {
|
||||
aux := *f
|
||||
|
||||
if aux.Limit == 0 {
|
||||
aux.Limit = 1000
|
||||
}
|
||||
|
||||
for {
|
||||
nn, fn, err = s.SearchComposeModules(ctx, types.ModuleFilter(aux))
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
for _, n := range nn {
|
||||
d.resourceID = append(d.resourceID, n.ID)
|
||||
|
||||
n.Fields, _, err = s.SearchComposeModuleFields(ctx, types.ModuleFieldFilter{
|
||||
ModuleID: []uint64{n.ID},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
mm = append(mm, newComposeModule(n))
|
||||
}
|
||||
|
||||
if f.NextPage != nil {
|
||||
aux.PageCursor = fn.NextPage
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *composeDecoder) decodeComposeRecord(ctx context.Context, s store.Storer, ff []*composeRecordFilter) *auxRsp {
|
||||
mm := make([]envoy.Marshaller, 0, 100)
|
||||
if ff == nil {
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
ac := service.AccessControl(s)
|
||||
|
||||
if len(d.namespaceID) > 0 {
|
||||
ffNs := make([]*composeRecordFilter, 0, len(ff)+len(d.namespaceID))
|
||||
for _, nsID := range d.namespaceID {
|
||||
for _, f := range ff {
|
||||
nn, _, err := s.SearchComposeModules(ctx, types.ModuleFilter{
|
||||
NamespaceID: nsID,
|
||||
})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, n := range nn {
|
||||
fNs := *f
|
||||
fNs.NamespaceID = nsID
|
||||
fNs.ModuleID = n.ID
|
||||
ffNs = append(ffNs, &fNs)
|
||||
}
|
||||
}
|
||||
}
|
||||
ff = ffNs
|
||||
}
|
||||
|
||||
// When decoding large amounts of records (milions) we can probably assume
|
||||
// that each system user exists somewhere in the record set.
|
||||
//
|
||||
// That said, it's probably cheeper to simply say that we probably need all of the
|
||||
// references to hold.
|
||||
// If a user is not there in the preparation step, we shouldn't fail straight away.
|
||||
//
|
||||
// @todo use some heuristic to determine when to just list all of the users
|
||||
// and when to preprocess the records to find out what users to get.
|
||||
|
||||
relUsers := make(resource.UserstampIndex)
|
||||
uu, _, err := store.SearchUsers(ctx, s, stypes.UserFilter{
|
||||
Paging: filter.Paging{
|
||||
Limit: 0,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
relUsers.Add(uu...)
|
||||
|
||||
mapValues := func(r *types.Record) map[string]resource.ComposeRecordRawValue {
|
||||
mod := r.GetModule()
|
||||
out := make(map[string]resource.ComposeRecordRawValue, len(r.Values))
|
||||
|
||||
for _, v := range r.Values {
|
||||
f := mod.Fields.FindByName(v.Name)
|
||||
if f == nil || !f.Multi {
|
||||
out[v.Name] = resource.ComposeRecordRawValue{
|
||||
Name: v.Name,
|
||||
Value: v.Value,
|
||||
}
|
||||
} else {
|
||||
if _, ok := out[v.Name]; !ok {
|
||||
out[v.Name] = resource.ComposeRecordRawValue{
|
||||
Name: v.Name,
|
||||
IsMulti: true,
|
||||
}
|
||||
}
|
||||
|
||||
vx := out[v.Name]
|
||||
vx.Values = append(vx.Values, v.Value)
|
||||
out[v.Name] = vx
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// Prepare a series of resource.ComposeRecord instances; one for each provided filter
|
||||
for _, f := range ff {
|
||||
aux := *f
|
||||
|
||||
if aux.Limit == 0 {
|
||||
aux.Limit = 1000
|
||||
}
|
||||
|
||||
mod, err := store.LookupComposeModuleByID(ctx, s, f.ModuleID)
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
ff, _, err := store.SearchComposeModuleFields(ctx, s, types.ModuleFieldFilter{ModuleID: []uint64{mod.ID}})
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
_ = ff.Walk(func(f *types.ModuleField) error {
|
||||
f.NamespaceID = mod.NamespaceID
|
||||
return nil
|
||||
})
|
||||
mod.Fields = ff
|
||||
|
||||
aux.Check = service.ComposeRecordFilterChecker(ctx, ac, mod)
|
||||
|
||||
// Refs
|
||||
auxRecord := &composeRecordAux{
|
||||
refMod: strconv.FormatUint(f.ModuleID, 10),
|
||||
relMod: mod,
|
||||
refNs: strconv.FormatUint(f.NamespaceID, 10),
|
||||
relUsers: relUsers,
|
||||
}
|
||||
|
||||
// Walker
|
||||
auxRecord.walker = func(cb func(r *resource.ComposeRecordRaw) error) error {
|
||||
var rr types.RecordSet
|
||||
var fn types.RecordFilter
|
||||
var err error
|
||||
|
||||
for {
|
||||
rr, fn, err = dalutils.ComposeRecordsList(ctx, d.dal, mod, types.RecordFilter(aux))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service.ComposeRecordFilterAC(ctx, ac, mod, rr...)
|
||||
|
||||
for _, n := range rr {
|
||||
// Create a raw record
|
||||
r := &resource.ComposeRecordRaw{
|
||||
ID: strconv.FormatUint(n.ID, 10),
|
||||
Values: mapValues(n),
|
||||
Ts: resource.MakeTimestampsCUDA(&n.CreatedAt, n.UpdatedAt, n.DeletedAt, nil),
|
||||
Us: resource.MakeUserstampsCUDO(n.CreatedBy, n.UpdatedBy, n.DeletedBy, n.OwnedBy),
|
||||
}
|
||||
|
||||
err = cb(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if fn.NextPage != nil {
|
||||
aux.PageCursor = fn.NextPage
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
mm = append(mm, newComposeRecordFromAux(auxRecord))
|
||||
|
||||
}
|
||||
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *composeDecoder) decodeComposePage(ctx context.Context, s composeStore, ff []*composePageFilter) *auxRsp {
|
||||
mm := make([]envoy.Marshaller, 0, 100)
|
||||
if ff == nil {
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
if len(d.namespaceID) > 0 {
|
||||
ffNs := make([]*composePageFilter, 0, len(ff)+len(d.namespaceID))
|
||||
for _, nsID := range d.namespaceID {
|
||||
for _, f := range ff {
|
||||
fNs := *f
|
||||
fNs.NamespaceID = nsID
|
||||
ffNs = append(ffNs, &fNs)
|
||||
}
|
||||
}
|
||||
ff = ffNs
|
||||
}
|
||||
|
||||
var nn types.PageSet
|
||||
var fn types.PageFilter
|
||||
var err error
|
||||
for _, f := range ff {
|
||||
aux := *f
|
||||
|
||||
if aux.Limit == 0 {
|
||||
aux.Limit = 1000
|
||||
}
|
||||
|
||||
for {
|
||||
nn, fn, err = s.SearchComposePages(ctx, types.PageFilter(aux))
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
for _, n := range nn {
|
||||
d.resourceID = append(d.resourceID, n.ID)
|
||||
|
||||
mm = append(mm, newComposePage(n))
|
||||
}
|
||||
|
||||
if fn.NextPage != nil {
|
||||
aux.PageCursor = fn.NextPage
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *composeDecoder) decodeComposeChart(ctx context.Context, s composeStore, ff []*composeChartFilter) *auxRsp {
|
||||
mm := make([]envoy.Marshaller, 0, 100)
|
||||
if ff == nil {
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
if len(d.namespaceID) > 0 {
|
||||
ffNs := make([]*composeChartFilter, 0, len(ff)+len(d.namespaceID))
|
||||
for _, nsID := range d.namespaceID {
|
||||
for _, f := range ff {
|
||||
fNs := *f
|
||||
fNs.NamespaceID = nsID
|
||||
ffNs = append(ffNs, &fNs)
|
||||
}
|
||||
}
|
||||
ff = ffNs
|
||||
}
|
||||
|
||||
var nn types.ChartSet
|
||||
var fn types.ChartFilter
|
||||
var err error
|
||||
for _, f := range ff {
|
||||
aux := *f
|
||||
|
||||
if aux.Limit == 0 {
|
||||
aux.Limit = 1000
|
||||
}
|
||||
|
||||
for {
|
||||
nn, fn, err = s.SearchComposeCharts(ctx, types.ChartFilter(aux))
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
for _, n := range nn {
|
||||
d.resourceID = append(d.resourceID, n.ID)
|
||||
|
||||
mm = append(mm, newComposeChart(n))
|
||||
}
|
||||
|
||||
if f.Limit > 0 {
|
||||
break
|
||||
} else if fn.NextPage != nil {
|
||||
aux.PageCursor = fn.NextPage
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
func (df *DecodeFilter) composeFromResource(rr ...string) *DecodeFilter {
|
||||
for _, r := range rr {
|
||||
if !strings.HasPrefix(r, "compose") {
|
||||
continue
|
||||
}
|
||||
|
||||
id := ""
|
||||
if strings.Count(r, ":") == 2 && !strings.HasSuffix(r, "*") {
|
||||
// There is an identifier
|
||||
aux := strings.Split(r, ":")
|
||||
|
||||
id = aux[len(aux)-1]
|
||||
r = strings.Join(aux[:len(aux)-1], ":")
|
||||
}
|
||||
|
||||
switch strings.ToLower(r) {
|
||||
|
||||
case "compose:namespace":
|
||||
df = df.ComposeNamespace(&types.NamespaceFilter{
|
||||
Query: id,
|
||||
})
|
||||
case "compose:module":
|
||||
df = df.ComposeModule(&types.ModuleFilter{
|
||||
Query: id,
|
||||
})
|
||||
case "compose:record":
|
||||
df = df.ComposeRecord(&types.RecordFilter{
|
||||
Query: id,
|
||||
})
|
||||
case "compose:page":
|
||||
df = df.ComposePage(&types.PageFilter{
|
||||
Query: id,
|
||||
})
|
||||
case "compose:chart":
|
||||
df = df.ComposeChart(&types.ChartFilter{
|
||||
Query: id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return df
|
||||
}
|
||||
|
||||
func (df *DecodeFilter) composeFromRef(rr ...*resource.Ref) *DecodeFilter {
|
||||
for _, r := range rr {
|
||||
if strings.Index(r.ResourceType, "compose") < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
switch r.ResourceType {
|
||||
case types.NamespaceResourceType:
|
||||
for _, i := range r.Identifiers.StringSlice() {
|
||||
df = df.ComposeNamespace(&types.NamespaceFilter{
|
||||
Query: i,
|
||||
})
|
||||
}
|
||||
case types.ModuleResourceType:
|
||||
for _, i := range r.Identifiers.StringSlice() {
|
||||
df = df.ComposeModule(&types.ModuleFilter{
|
||||
Query: i,
|
||||
})
|
||||
}
|
||||
case types.PageResourceType:
|
||||
for _, i := range r.Identifiers.StringSlice() {
|
||||
df = df.ComposePage(&types.PageFilter{
|
||||
Query: i,
|
||||
})
|
||||
}
|
||||
case types.ChartResourceType:
|
||||
for _, i := range r.Identifiers.StringSlice() {
|
||||
df = df.ComposeChart(&types.ChartFilter{
|
||||
Query: i,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return df
|
||||
}
|
||||
|
||||
// ComposeNamespace adds a new compose NamespaceFilter
|
||||
func (df *DecodeFilter) ComposeNamespace(f *types.NamespaceFilter) *DecodeFilter {
|
||||
if df.composeNamespace == nil {
|
||||
df.composeNamespace = make([]*composeNamespaceFilter, 0, 1)
|
||||
}
|
||||
df.composeNamespace = append(df.composeNamespace, (*composeNamespaceFilter)(f))
|
||||
return df
|
||||
}
|
||||
|
||||
// ComposeModule adds a new compose ModuleFilter
|
||||
func (df *DecodeFilter) ComposeModule(f *types.ModuleFilter) *DecodeFilter {
|
||||
if df.composeModule == nil {
|
||||
df.composeModule = make([]*composeModuleFilter, 0, 1)
|
||||
}
|
||||
df.composeModule = append(df.composeModule, (*composeModuleFilter)(f))
|
||||
return df
|
||||
}
|
||||
|
||||
// ComposeRecord adds a new compose RecordFilter
|
||||
func (df *DecodeFilter) ComposeRecord(f *types.RecordFilter) *DecodeFilter {
|
||||
if df.composeRecord == nil {
|
||||
df.composeRecord = make([]*composeRecordFilter, 0, 1)
|
||||
}
|
||||
df.composeRecord = append(df.composeRecord, (*composeRecordFilter)(f))
|
||||
return df
|
||||
}
|
||||
|
||||
// ComposePage adds a new compose PageFilter
|
||||
func (df *DecodeFilter) ComposePage(f *types.PageFilter) *DecodeFilter {
|
||||
if df.composePage == nil {
|
||||
df.composePage = make([]*composePageFilter, 0, 1)
|
||||
}
|
||||
df.composePage = append(df.composePage, (*composePageFilter)(f))
|
||||
return df
|
||||
}
|
||||
|
||||
// ComposeChart adds a new compose ChartFilter
|
||||
func (df *DecodeFilter) ComposeChart(f *types.ChartFilter) *DecodeFilter {
|
||||
if df.composeChart == nil {
|
||||
df.composeChart = make([]*composeChartFilter, 0, 1)
|
||||
}
|
||||
df.composeChart = append(df.composeChart, (*composeChartFilter)(f))
|
||||
return df
|
||||
}
|
||||
118
server/pkg/envoy/store/compose_chart.go
Normal file
118
server/pkg/envoy/store/compose_chart.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
)
|
||||
|
||||
type (
|
||||
composeChart struct {
|
||||
cfg *EncoderConfig
|
||||
|
||||
res *resource.ComposeChart
|
||||
chr *types.Chart
|
||||
|
||||
relNS *types.Namespace
|
||||
relMods types.ModuleSet
|
||||
}
|
||||
)
|
||||
|
||||
// mergeComposeChart merges b into a, prioritising a
|
||||
func mergeComposeChart(a, b *types.Chart) *types.Chart {
|
||||
c := *a
|
||||
|
||||
if c.Handle == "" {
|
||||
c.Handle = b.Handle
|
||||
}
|
||||
if c.Name == "" {
|
||||
c.Name = b.Name
|
||||
}
|
||||
c.NamespaceID = b.NamespaceID
|
||||
if c.CreatedAt.IsZero() {
|
||||
c.CreatedAt = b.CreatedAt
|
||||
}
|
||||
if c.UpdatedAt == nil {
|
||||
c.UpdatedAt = b.UpdatedAt
|
||||
}
|
||||
if c.DeletedAt == nil {
|
||||
c.DeletedAt = b.DeletedAt
|
||||
}
|
||||
|
||||
if len(c.Config.Reports) <= 0 {
|
||||
c.Config.Reports = b.Config.Reports
|
||||
}
|
||||
if c.Config.ColorScheme == "" {
|
||||
c.Config.ColorScheme = b.Config.ColorScheme
|
||||
}
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
// findComposeChart looks for the chart in the resources & the store
|
||||
//
|
||||
// Provided resources are prioritized.
|
||||
func findComposeChart(ctx context.Context, s store.Storer, nsID uint64, rr resource.InterfaceSet, ii resource.Identifiers) (ch *types.Chart, err error) {
|
||||
ch = resource.FindComposeChart(rr, ii)
|
||||
if ch != nil {
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
if nsID <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Go in the store
|
||||
return findComposeChartStore(ctx, s, nsID, makeGenericFilter(ii))
|
||||
}
|
||||
|
||||
// findComposeChartStore looks for the chart in the store
|
||||
func findComposeChartStore(ctx context.Context, s store.Storer, nsID uint64, gf genericFilter) (ch *types.Chart, err error) {
|
||||
if nsID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if gf.id > 0 {
|
||||
ch, err = store.LookupComposeChartByID(ctx, s, gf.id)
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ch != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, i := range gf.identifiers {
|
||||
ch, err = store.LookupComposeChartByNamespaceIDHandle(ctx, s, nsID, i)
|
||||
if err == store.ErrNotFound {
|
||||
var cc types.ChartSet
|
||||
cc, _, err = store.SearchComposeCharts(ctx, s, types.ChartFilter{
|
||||
NamespaceID: nsID,
|
||||
Name: i,
|
||||
Paging: filter.Paging{
|
||||
Limit: 2,
|
||||
},
|
||||
})
|
||||
if len(cc) > 1 {
|
||||
return nil, resourceErrIdentifierNotUnique(i)
|
||||
}
|
||||
if len(cc) == 1 {
|
||||
ch = cc[0]
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ch != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
140
server/pkg/envoy/store/compose_chart_marshal.go
Normal file
140
server/pkg/envoy/store/compose_chart_marshal.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
)
|
||||
|
||||
func newComposeChartFromResource(res *resource.ComposeChart, cfg *EncoderConfig) resourceState {
|
||||
return &composeChart{
|
||||
cfg: mergeConfig(cfg, res.Config()),
|
||||
|
||||
res: res,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *composeChart) Prepare(ctx context.Context, pl *payload) (err error) {
|
||||
// Reset old identifiers
|
||||
n.res.Res.ID = 0
|
||||
n.res.Res.NamespaceID = 0
|
||||
|
||||
// Get related namespace
|
||||
n.relNS, err = findComposeNamespace(ctx, pl.s, pl.state.ParentResources, n.res.RefNs.Identifiers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n.relNS == nil {
|
||||
return resource.ComposeNamespaceErrUnresolved(n.res.RefNs.Identifiers)
|
||||
}
|
||||
|
||||
// Get related modules
|
||||
n.relMods = make(types.ModuleSet, len(n.res.RefMods))
|
||||
for i, rMod := range n.res.RefMods {
|
||||
n.relMods[i], err = findComposeModule(ctx, pl.s, n.relNS.ID, pl.state.ParentResources, rMod.Identifiers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n.relMods[i] == nil {
|
||||
return resource.ComposeModuleErrUnresolved(rMod.Identifiers)
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get the original chart
|
||||
n.chr, err = findComposeChartStore(ctx, pl.s, n.relNS.ID, makeGenericFilter(n.res.Identifiers()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if n.chr != nil {
|
||||
n.res.Res.ID = n.chr.ID
|
||||
n.res.Res.NamespaceID = n.chr.NamespaceID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *composeChart) Encode(ctx context.Context, pl *payload) (err error) {
|
||||
res := n.res.Res
|
||||
exists := n.chr != nil && n.chr.ID > 0
|
||||
|
||||
// Determine the ID
|
||||
if res.ID <= 0 && exists {
|
||||
res.ID = n.chr.ID
|
||||
}
|
||||
if res.ID <= 0 {
|
||||
res.ID = NextID()
|
||||
}
|
||||
|
||||
ts := n.res.Timestamps()
|
||||
if ts != nil {
|
||||
if ts.CreatedAt != nil {
|
||||
res.CreatedAt = *ts.CreatedAt.T
|
||||
} else {
|
||||
res.CreatedAt = *now()
|
||||
}
|
||||
if ts.UpdatedAt != nil {
|
||||
res.UpdatedAt = ts.UpdatedAt.T
|
||||
}
|
||||
if ts.DeletedAt != nil {
|
||||
res.DeletedAt = ts.DeletedAt.T
|
||||
}
|
||||
}
|
||||
|
||||
// Namespace
|
||||
res.NamespaceID = n.relNS.ID
|
||||
if res.NamespaceID <= 0 {
|
||||
ns := resource.FindComposeNamespace(pl.state.ParentResources, n.res.RefNs.Identifiers)
|
||||
res.NamespaceID = ns.ID
|
||||
}
|
||||
if res.NamespaceID <= 0 {
|
||||
return resource.ComposeNamespaceErrUnresolved(n.res.RefNs.Identifiers)
|
||||
}
|
||||
|
||||
// Report modules
|
||||
for i, r := range res.Config.Reports {
|
||||
relMod := n.relMods[i]
|
||||
if relMod == nil {
|
||||
relMod = resource.FindComposeModule(pl.state.ParentResources, n.res.RefMods[i].Identifiers)
|
||||
}
|
||||
if relMod == nil || relMod.ID <= 0 {
|
||||
return resource.ComposeModuleErrUnresolved(n.res.RefMods[i].Identifiers)
|
||||
}
|
||||
|
||||
r.ModuleID = relMod.ID
|
||||
}
|
||||
|
||||
// Evaluate the resource skip expression
|
||||
// @todo expand available parameters; similar implementation to compose/types/record@Dict
|
||||
if skip, err := basicSkipEval(ctx, n.cfg, !exists); err != nil {
|
||||
return err
|
||||
} else if skip {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create fresh chart
|
||||
if !exists {
|
||||
return store.CreateComposeChart(ctx, pl.s, res)
|
||||
}
|
||||
|
||||
// Update existing chart
|
||||
switch n.cfg.OnExisting {
|
||||
case resource.Skip:
|
||||
return nil
|
||||
|
||||
case resource.MergeLeft:
|
||||
res = mergeComposeChart(n.chr, res)
|
||||
|
||||
case resource.MergeRight:
|
||||
res = mergeComposeChart(res, n.chr)
|
||||
}
|
||||
|
||||
err = store.UpdateComposeChart(ctx, pl.s, res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.res.Res = res
|
||||
return nil
|
||||
}
|
||||
54
server/pkg/envoy/store/compose_chart_test.go
Normal file
54
server/pkg/envoy/store/compose_chart_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestComposeChart_Merger(t *testing.T) {
|
||||
req := require.New(t)
|
||||
|
||||
now := time.Time{}
|
||||
nowP := &time.Time{}
|
||||
|
||||
empty := &types.Chart{}
|
||||
full := &types.Chart{
|
||||
Handle: "handle",
|
||||
Name: "name",
|
||||
Config: types.ChartConfig{
|
||||
Reports: []*types.ChartConfigReport{&types.ChartConfigReport{}},
|
||||
ColorScheme: "colorScheme",
|
||||
},
|
||||
NamespaceID: 1,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: nowP,
|
||||
DeletedAt: nowP,
|
||||
}
|
||||
|
||||
t.Run("merge on empty", func(t *testing.T) {
|
||||
c := mergeComposeChart(empty, full)
|
||||
req.Equal("name", c.Name)
|
||||
req.Equal("handle", c.Handle)
|
||||
req.Len(c.Config.Reports, 1)
|
||||
req.Equal("colorScheme", c.Config.ColorScheme)
|
||||
req.Equal(uint64(1), c.NamespaceID)
|
||||
req.Equal(now, c.CreatedAt)
|
||||
req.Equal(nowP, c.UpdatedAt)
|
||||
req.Equal(nowP, c.DeletedAt)
|
||||
})
|
||||
|
||||
t.Run("merge with empty", func(t *testing.T) {
|
||||
c := mergeComposeChart(full, empty)
|
||||
req.Equal("name", c.Name)
|
||||
req.Equal("handle", c.Handle)
|
||||
req.Len(c.Config.Reports, 1)
|
||||
req.Equal("colorScheme", c.Config.ColorScheme)
|
||||
req.Equal(uint64(0), c.NamespaceID)
|
||||
req.Equal(now, c.CreatedAt)
|
||||
req.Equal(nowP, c.UpdatedAt)
|
||||
req.Equal(nowP, c.DeletedAt)
|
||||
})
|
||||
}
|
||||
25
server/pkg/envoy/store/compose_chart_unmarshal.go
Normal file
25
server/pkg/envoy/store/compose_chart_unmarshal.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
)
|
||||
|
||||
func newComposeChart(chr *types.Chart) *composeChart {
|
||||
return &composeChart{
|
||||
chr: chr,
|
||||
}
|
||||
}
|
||||
|
||||
func (chr *composeChart) MarshalEnvoy() ([]resource.Interface, error) {
|
||||
refNs := resource.MakeNamespaceRef(chr.chr.NamespaceID, "", "")
|
||||
refMods := make(resource.RefSet, 0, 2)
|
||||
for _, r := range chr.chr.Config.Reports {
|
||||
refMods = append(refMods, resource.MakeModuleRef(r.ModuleID, "", ""))
|
||||
}
|
||||
|
||||
return envoy.CollectNodes(
|
||||
resource.NewComposeChart(chr.chr, refNs, refMods),
|
||||
)
|
||||
}
|
||||
210
server/pkg/envoy/store/compose_module.go
Normal file
210
server/pkg/envoy/store/compose_module.go
Normal file
@@ -0,0 +1,210 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
)
|
||||
|
||||
type (
|
||||
composeModule struct {
|
||||
cfg *EncoderConfig
|
||||
|
||||
res *resource.ComposeModule
|
||||
mod *types.Module
|
||||
|
||||
relNS *types.Namespace
|
||||
recFields map[string]uint64
|
||||
userFields map[string]uint64
|
||||
}
|
||||
)
|
||||
|
||||
// mergeComposeModuleFields merges b into a, prioritising a
|
||||
func mergeComposeModuleFields(a, b types.ModuleFieldSet) types.ModuleFieldSet {
|
||||
ff := a.Clone()
|
||||
missing := make(types.ModuleFieldSet, 0, len(b))
|
||||
|
||||
for _, fb := range b {
|
||||
for _, fa := range ff {
|
||||
if fb.ID != fa.ID && fb.Name != fa.Name {
|
||||
continue
|
||||
}
|
||||
|
||||
fa.ModuleID = fb.ModuleID
|
||||
fa.Place = fb.Place
|
||||
if fa.Kind == "" {
|
||||
fa.Kind = fb.Kind
|
||||
}
|
||||
if fa.Name == "" {
|
||||
fa.Name = fb.Name
|
||||
}
|
||||
if fa.Label == "" {
|
||||
fa.Label = fb.Label
|
||||
}
|
||||
if fa.Options == nil {
|
||||
fa.Options = fb.Options
|
||||
}
|
||||
if fa.DefaultValue == nil {
|
||||
fa.DefaultValue = fb.DefaultValue
|
||||
}
|
||||
if fa.Expressions.ValueExpr == "" {
|
||||
fa.Expressions.ValueExpr = fb.Expressions.ValueExpr
|
||||
}
|
||||
if len(fa.Expressions.Sanitizers) == 0 {
|
||||
fa.Expressions.Sanitizers = fb.Expressions.Sanitizers
|
||||
}
|
||||
if len(fa.Expressions.Validators) == 0 {
|
||||
fa.Expressions.Validators = fb.Expressions.Validators
|
||||
}
|
||||
if len(fa.Expressions.Formatters) == 0 {
|
||||
fa.Expressions.Formatters = fb.Expressions.Formatters
|
||||
}
|
||||
|
||||
if fa.CreatedAt.IsZero() {
|
||||
fa.CreatedAt = fb.CreatedAt
|
||||
}
|
||||
if fa.UpdatedAt == nil {
|
||||
fa.UpdatedAt = fb.UpdatedAt
|
||||
}
|
||||
if fa.DeletedAt == nil {
|
||||
fa.DeletedAt = fb.DeletedAt
|
||||
}
|
||||
|
||||
goto out
|
||||
}
|
||||
missing = append(missing, fb)
|
||||
out:
|
||||
}
|
||||
|
||||
ff = append(ff, missing...)
|
||||
|
||||
return ff
|
||||
}
|
||||
|
||||
// mergeComposeModule merges b into a, prioritising a
|
||||
func mergeComposeModule(a, b *types.Module) *types.Module {
|
||||
c := a.Clone()
|
||||
|
||||
if c.Handle == "" {
|
||||
c.Handle = b.Handle
|
||||
}
|
||||
if c.Name == "" {
|
||||
c.Name = b.Name
|
||||
}
|
||||
c.NamespaceID = b.NamespaceID
|
||||
|
||||
if c.CreatedAt.IsZero() {
|
||||
c.CreatedAt = b.CreatedAt
|
||||
}
|
||||
if c.UpdatedAt == nil {
|
||||
c.UpdatedAt = b.UpdatedAt
|
||||
}
|
||||
if c.DeletedAt == nil {
|
||||
c.DeletedAt = b.DeletedAt
|
||||
}
|
||||
|
||||
// I'll just compare the entire struct for now
|
||||
if c.Meta == nil {
|
||||
c.Meta = b.Meta
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// findComposeModule looks for the chart in the resources & the store
|
||||
//
|
||||
// Provided resources are prioritized.
|
||||
func findComposeModule(ctx context.Context, s store.Storer, nsID uint64, rr resource.InterfaceSet, ii resource.Identifiers) (mod *types.Module, err error) {
|
||||
mod = resource.FindComposeModule(rr, ii)
|
||||
if mod != nil {
|
||||
return mod, nil
|
||||
}
|
||||
|
||||
if nsID <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Go in the store
|
||||
return findComposeModuleStore(ctx, s, nsID, makeGenericFilter(ii))
|
||||
}
|
||||
|
||||
// findComposeModuleStore looks for the module in the store
|
||||
func findComposeModuleStore(ctx context.Context, s store.Storer, nsID uint64, gf genericFilter) (mod *types.Module, err error) {
|
||||
if nsID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if gf.id > 0 {
|
||||
mod, err = store.LookupComposeModuleByID(ctx, s, gf.id)
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if mod != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, i := range gf.identifiers {
|
||||
mod, err = store.LookupComposeModuleByNamespaceIDHandle(ctx, s, nsID, i)
|
||||
if err == store.ErrNotFound {
|
||||
var mm types.ModuleSet
|
||||
mm, _, err = store.SearchComposeModules(ctx, s, types.ModuleFilter{
|
||||
NamespaceID: nsID,
|
||||
Name: i,
|
||||
Paging: filter.Paging{
|
||||
Limit: 2,
|
||||
},
|
||||
})
|
||||
if len(mm) > 1 {
|
||||
return nil, resourceErrIdentifierNotUnique(i)
|
||||
}
|
||||
if len(mm) == 1 {
|
||||
mod = mm[0]
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if mod != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// findComposeModuleFieldsStore looks for the module fields in the store
|
||||
func findComposeModuleFieldsStore(ctx context.Context, s store.Storer, mod *types.Module) (types.ModuleFieldSet, error) {
|
||||
if mod.ID <= 0 {
|
||||
return mod.Fields, nil
|
||||
}
|
||||
|
||||
ff, _, err := store.SearchComposeModuleFields(ctx, s, types.ModuleFieldFilter{
|
||||
ModuleID: []uint64{mod.ID},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_ = ff.Walk(func(f *types.ModuleField) error {
|
||||
f.NamespaceID = mod.NamespaceID
|
||||
return nil
|
||||
})
|
||||
|
||||
return ff, nil
|
||||
}
|
||||
|
||||
func composeModuleErrUnresolvedRecordField(ii resource.Identifiers) error {
|
||||
return fmt.Errorf("record module field unresolved %v", ii.StringSlice())
|
||||
}
|
||||
|
||||
func composeModuleErrUnresolvedUserField(ii resource.Identifiers) error {
|
||||
return fmt.Errorf("user module field unresolved %v", ii.StringSlice())
|
||||
}
|
||||
356
server/pkg/envoy/store/compose_module_marshal.go
Normal file
356
server/pkg/envoy/store/compose_module_marshal.go
Normal file
@@ -0,0 +1,356 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/service"
|
||||
"github.com/cortezaproject/corteza/server/pkg/dal"
|
||||
"github.com/cortezaproject/corteza/server/pkg/revisions"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
systemTypes "github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
func NewComposeModuleFromResource(res *resource.ComposeModule, cfg *EncoderConfig) resourceState {
|
||||
return &composeModule{
|
||||
cfg: mergeConfig(cfg, res.Config()),
|
||||
|
||||
res: res,
|
||||
|
||||
recFields: make(map[string]uint64),
|
||||
userFields: make(map[string]uint64),
|
||||
}
|
||||
}
|
||||
|
||||
func (n *composeModule) Prepare(ctx context.Context, pl *payload) (err error) {
|
||||
// Reset old identifiers
|
||||
n.res.Res.ID = 0
|
||||
n.res.Res.NamespaceID = 0
|
||||
for _, rf := range n.res.ResFields {
|
||||
rf.Res.ID = 0
|
||||
rf.Res.ModuleID = 0
|
||||
rf.Res.NamespaceID = 0
|
||||
}
|
||||
|
||||
// Get related namespace
|
||||
n.relNS, err = findComposeNamespace(ctx, pl.s, pl.state.ParentResources, n.res.RefNs.Identifiers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n.relNS == nil {
|
||||
return resource.ComposeNamespaceErrUnresolved(n.res.RefNs.Identifiers)
|
||||
}
|
||||
|
||||
n.res.Res.NamespaceID = n.relNS.ID
|
||||
|
||||
// Get related record field modules
|
||||
for _, refMod := range n.res.RefMods {
|
||||
var mod *types.Module
|
||||
if n.relNS.ID > 0 {
|
||||
mod, err = findComposeModuleStore(ctx, pl.s, n.relNS.ID, makeGenericFilter(refMod.Identifiers))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if mod == nil {
|
||||
mod = resource.FindComposeModule(pl.state.ParentResources, refMod.Identifiers)
|
||||
}
|
||||
if mod == nil {
|
||||
return composeModuleErrUnresolvedRecordField(refMod.Identifiers)
|
||||
}
|
||||
|
||||
for _, i := range refMod.Identifiers {
|
||||
n.recFields[i] = mod.ID
|
||||
}
|
||||
}
|
||||
|
||||
// Get related user field roles
|
||||
for _, refRole := range n.res.RefRoles {
|
||||
var rl *systemTypes.Role
|
||||
rl, err = findRoleStore(ctx, pl.s, makeGenericFilter(refRole.Identifiers))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rl == nil {
|
||||
rl = resource.FindRole(pl.state.ParentResources, refRole.Identifiers)
|
||||
}
|
||||
if rl == nil {
|
||||
return composeModuleErrUnresolvedUserField(refRole.Identifiers)
|
||||
}
|
||||
|
||||
for _, i := range refRole.Identifiers {
|
||||
n.userFields[i] = rl.ID
|
||||
}
|
||||
}
|
||||
|
||||
// Can't do anything else, since the NS doesn't yet exist
|
||||
if n.relNS.ID <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try to get the original module
|
||||
n.mod, err = findComposeModuleStore(ctx, pl.s, n.relNS.ID, makeGenericFilter(n.res.Identifiers()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Nothing else to do
|
||||
if n.mod == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get the original module fields
|
||||
// These are used later for some merging logic
|
||||
n.mod.Fields, err = findComposeModuleFieldsStore(ctx, pl.s, n.mod)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if n.mod != nil {
|
||||
n.res.Res.ID = n.mod.ID
|
||||
n.res.Res.NamespaceID = n.mod.NamespaceID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *composeModule) Encode(ctx context.Context, pl *payload) (err error) {
|
||||
res := n.res.Res
|
||||
exists := n.mod != nil && n.mod.ID > 0
|
||||
|
||||
// Determine the ID
|
||||
if res.ID <= 0 && exists {
|
||||
res.ID = n.mod.ID
|
||||
}
|
||||
if res.ID <= 0 {
|
||||
res.ID = NextID()
|
||||
}
|
||||
|
||||
ns := n.relNS
|
||||
|
||||
res.NamespaceID = ns.ID
|
||||
if res.NamespaceID <= 0 {
|
||||
ns = resource.FindComposeNamespace(pl.state.ParentResources, n.res.RefNs.Identifiers)
|
||||
res.NamespaceID = ns.ID
|
||||
}
|
||||
if res.NamespaceID <= 0 {
|
||||
return resource.ComposeNamespaceErrUnresolved(n.res.RefNs.Identifiers)
|
||||
}
|
||||
|
||||
if pl.state.Conflicting {
|
||||
return nil
|
||||
}
|
||||
|
||||
ts := n.res.Timestamps()
|
||||
if ts != nil {
|
||||
if ts.CreatedAt != nil {
|
||||
res.CreatedAt = *ts.CreatedAt.T
|
||||
} else {
|
||||
res.CreatedAt = *now()
|
||||
}
|
||||
if ts.UpdatedAt != nil {
|
||||
res.UpdatedAt = ts.UpdatedAt.T
|
||||
}
|
||||
if ts.DeletedAt != nil {
|
||||
res.DeletedAt = ts.DeletedAt.T
|
||||
}
|
||||
}
|
||||
|
||||
// Fields
|
||||
var originalFields types.ModuleFieldSet
|
||||
if n.mod != nil && n.mod.Fields != nil {
|
||||
originalFields = n.mod.Fields
|
||||
} else {
|
||||
originalFields = make(types.ModuleFieldSet, 0)
|
||||
}
|
||||
|
||||
// Get max validatorID for later use
|
||||
vvID := make([]uint64, len(res.Fields))
|
||||
for i, f := range res.Fields {
|
||||
for _, v := range f.Expressions.Validators {
|
||||
if vvID[i] < v.ValidatorID {
|
||||
vvID[i] = v.ValidatorID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i, f := range res.Fields {
|
||||
of := originalFields.FindByName(f.Name)
|
||||
if of != nil {
|
||||
f.ID = of.ID
|
||||
} else {
|
||||
f.ID = NextID()
|
||||
}
|
||||
f.ModuleID = res.ID
|
||||
f.Place = i
|
||||
f.DeletedAt = nil
|
||||
f.CreatedAt = *now()
|
||||
|
||||
// Assure validatorIDs
|
||||
for j, v := range f.Expressions.Validators {
|
||||
if v.ValidatorID == 0 {
|
||||
vvID[i] += 1
|
||||
v.ValidatorID = vvID[i]
|
||||
|
||||
f.Expressions.Validators[j] = v
|
||||
}
|
||||
}
|
||||
|
||||
if f.Options != nil && f.Kind == "Record" {
|
||||
refMod := f.Options.String("module")
|
||||
if refMod == "" {
|
||||
refMod = f.Options.String("moduleID")
|
||||
}
|
||||
modID := n.recFields[refMod]
|
||||
if modID <= 0 {
|
||||
ii := resource.MakeIdentifiers(refMod)
|
||||
mod := resource.FindComposeModule(pl.state.ParentResources, ii)
|
||||
if mod == nil || mod.ID <= 0 {
|
||||
return composeModuleErrUnresolvedRecordField(ii)
|
||||
}
|
||||
modID = mod.ID
|
||||
}
|
||||
|
||||
f.Options["moduleID"] = strconv.FormatUint(modID, 10)
|
||||
delete(f.Options, "module")
|
||||
}
|
||||
|
||||
if f.Options != nil && f.Kind == "User" {
|
||||
roles := resource.ComposeModuleFieldExtractUserFieldRoles(f.Options["roles"])
|
||||
if len(roles) == 0 {
|
||||
roles = resource.ComposeModuleFieldExtractUserFieldRoles(f.Options["role"])
|
||||
}
|
||||
if len(roles) == 0 {
|
||||
roles = resource.ComposeModuleFieldExtractUserFieldRoles(f.Options["roleID"])
|
||||
}
|
||||
|
||||
var out []string
|
||||
for _, r := range roles {
|
||||
roleID := n.userFields[r]
|
||||
if roleID <= 0 {
|
||||
ii := resource.MakeIdentifiers(r)
|
||||
role := resource.FindRole(pl.state.ParentResources, ii)
|
||||
if role == nil || role.ID == 0 {
|
||||
return composeModuleErrUnresolvedUserField(ii)
|
||||
}
|
||||
roleID = role.ID
|
||||
}
|
||||
|
||||
out = append(out, strconv.FormatUint(roleID, 10))
|
||||
}
|
||||
|
||||
f.Options["roles"] = out
|
||||
delete(f.Options, "role")
|
||||
delete(f.Options, "roleID")
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate the resource skip expression
|
||||
// @todo expand available parameters; similar implementation to compose/types/record@Dict
|
||||
if skip, err := basicSkipEval(ctx, n.cfg, !exists); err != nil {
|
||||
return err
|
||||
} else if skip {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a fresh module
|
||||
if !exists {
|
||||
err = store.CreateComposeModule(ctx, pl.s, res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = store.CreateComposeModuleField(ctx, pl.s, res.Fields...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// Update existing module
|
||||
switch n.cfg.OnExisting {
|
||||
case resource.Skip:
|
||||
return nil
|
||||
|
||||
case resource.MergeLeft:
|
||||
res = mergeComposeModule(n.mod, res)
|
||||
res.Fields = mergeComposeModuleFields(n.mod.Fields, res.Fields)
|
||||
|
||||
case resource.MergeRight:
|
||||
res = mergeComposeModule(res, n.mod)
|
||||
res.Fields = mergeComposeModuleFields(res.Fields, n.mod.Fields)
|
||||
}
|
||||
|
||||
err = store.UpdateComposeModule(ctx, pl.s, res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = store.DeleteComposeModuleField(ctx, pl.s, n.mod.Fields...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = store.CreateComposeModuleField(ctx, pl.s, res.Fields...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.res.Res = res
|
||||
}
|
||||
|
||||
var (
|
||||
model *dal.Model
|
||||
con = pl.dal.GetConnectionByID(0)
|
||||
)
|
||||
|
||||
// convert module to model and assume compose_record for default ident
|
||||
if model, err = service.ModuleToModel(ns, res, "compose_record"); err != nil {
|
||||
return
|
||||
}
|
||||
model.ConnectionID = con.ID
|
||||
|
||||
// @note code copied from the service/module.go
|
||||
|
||||
// Set base constraints
|
||||
if model.Ident == "compose_record" {
|
||||
model.Constraints = map[string][]any{
|
||||
"moduleID": {res.ID},
|
||||
"namespaceID": {res.NamespaceID},
|
||||
}
|
||||
}
|
||||
|
||||
// replace all partition replacement pairs
|
||||
rpl := strings.NewReplacer("{{module}}", res.Handle, "{{namespace}}", ns.Slug)
|
||||
model.Ident = rpl.Replace(model.Ident)
|
||||
|
||||
// @todo validate ident with connection's ident validator
|
||||
|
||||
// Create the revisions model if enabled
|
||||
if res.Config.RecordRevisions.Enabled {
|
||||
rModel := revisions.Model()
|
||||
|
||||
// reuse the connection from the module
|
||||
rModel.ConnectionID = con.ID
|
||||
rModel.Resource = model.Resource
|
||||
rModel.ResourceID = NextID()
|
||||
|
||||
if rModel.Ident = res.Config.RecordRevisions.Ident; rModel.Ident == "" {
|
||||
rModel.Ident = "compose_record_revisions"
|
||||
}
|
||||
|
||||
rModel.Ident = rpl.Replace(rModel.Ident)
|
||||
|
||||
// @todo validate ident with connection's ident validator
|
||||
|
||||
if _, err = pl.dal.ReplaceModel(ctx, nil, rModel); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if _, err = pl.dal.ReplaceModel(ctx, nil, model); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
111
server/pkg/envoy/store/compose_module_test.go
Normal file
111
server/pkg/envoy/store/compose_module_test.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
st "github.com/jmoiron/sqlx/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestComposeModule_Merger(t *testing.T) {
|
||||
req := require.New(t)
|
||||
|
||||
now := time.Time{}
|
||||
nowP := &time.Time{}
|
||||
|
||||
empty := &types.Module{}
|
||||
full := &types.Module{
|
||||
Handle: "handle",
|
||||
Name: "name",
|
||||
Meta: st.JSONText{},
|
||||
NamespaceID: 1,
|
||||
|
||||
CreatedAt: now,
|
||||
UpdatedAt: nowP,
|
||||
DeletedAt: nowP,
|
||||
}
|
||||
|
||||
t.Run("merge on empty", func(t *testing.T) {
|
||||
c := mergeComposeModule(empty, full)
|
||||
req.Equal("name", c.Name)
|
||||
req.Equal("handle", c.Handle)
|
||||
req.NotNil(c.Meta)
|
||||
req.Equal(uint64(1), c.NamespaceID)
|
||||
req.Equal(now, c.CreatedAt)
|
||||
req.Equal(nowP, c.UpdatedAt)
|
||||
req.Equal(nowP, c.DeletedAt)
|
||||
})
|
||||
|
||||
t.Run("merge with empty", func(t *testing.T) {
|
||||
c := mergeComposeModule(full, empty)
|
||||
req.Equal("name", c.Name)
|
||||
req.Equal("handle", c.Handle)
|
||||
req.NotNil(c.Meta)
|
||||
req.Equal(uint64(0), c.NamespaceID)
|
||||
req.Equal(now, c.CreatedAt)
|
||||
req.Equal(nowP, c.UpdatedAt)
|
||||
req.Equal(nowP, c.DeletedAt)
|
||||
})
|
||||
}
|
||||
|
||||
func TestComposeModuleField_Merger(t *testing.T) {
|
||||
req := require.New(t)
|
||||
|
||||
now := time.Time{}
|
||||
nowP := &time.Time{}
|
||||
|
||||
empty := types.ModuleFieldSet{}
|
||||
full := types.ModuleFieldSet{
|
||||
&types.ModuleField{
|
||||
ModuleID: 1,
|
||||
Place: 2,
|
||||
Kind: "kind",
|
||||
Name: "name",
|
||||
Label: "label",
|
||||
Options: types.ModuleFieldOptions{},
|
||||
DefaultValue: types.RecordValueSet{},
|
||||
Expressions: types.ModuleFieldExpr{},
|
||||
CreatedAt: now,
|
||||
UpdatedAt: nowP,
|
||||
DeletedAt: nowP,
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("merge on empty", func(t *testing.T) {
|
||||
cc := mergeComposeModuleFields(empty, full)
|
||||
req.Len(cc, 1)
|
||||
c := cc[0]
|
||||
|
||||
req.Equal(uint64(1), c.ModuleID)
|
||||
req.Equal(2, c.Place)
|
||||
req.Equal("kind", c.Kind)
|
||||
req.Equal("name", c.Name)
|
||||
req.Equal("label", c.Label)
|
||||
req.NotNil(c.Options)
|
||||
req.NotNil(c.DefaultValue)
|
||||
req.NotNil(c.Expressions)
|
||||
req.Equal(now, c.CreatedAt)
|
||||
req.Equal(nowP, c.UpdatedAt)
|
||||
req.Equal(nowP, c.DeletedAt)
|
||||
})
|
||||
|
||||
t.Run("merge with empty", func(t *testing.T) {
|
||||
cc := mergeComposeModuleFields(full, empty)
|
||||
req.Len(cc, 1)
|
||||
c := cc[0]
|
||||
|
||||
req.Equal(uint64(1), c.ModuleID)
|
||||
req.Equal(2, c.Place)
|
||||
req.Equal("kind", c.Kind)
|
||||
req.Equal("name", c.Name)
|
||||
req.Equal("label", c.Label)
|
||||
req.NotNil(c.Options)
|
||||
req.NotNil(c.DefaultValue)
|
||||
req.NotNil(c.Expressions)
|
||||
req.Equal(now, c.CreatedAt)
|
||||
req.Equal(nowP, c.UpdatedAt)
|
||||
req.Equal(nowP, c.DeletedAt)
|
||||
})
|
||||
}
|
||||
28
server/pkg/envoy/store/compose_module_unmarshal.go
Normal file
28
server/pkg/envoy/store/compose_module_unmarshal.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
)
|
||||
|
||||
func newComposeModule(mod *types.Module) *composeModule {
|
||||
return &composeModule{
|
||||
mod: mod,
|
||||
}
|
||||
}
|
||||
|
||||
func (mod *composeModule) MarshalEnvoy() ([]resource.Interface, error) {
|
||||
refNs := resource.MakeNamespaceRef(mod.mod.NamespaceID, "", "")
|
||||
refMod := resource.MakeModuleRef(mod.mod.ID, mod.mod.Handle, mod.mod.Name)
|
||||
|
||||
rMod := resource.NewComposeModule(mod.mod, refNs)
|
||||
for _, f := range mod.mod.Fields {
|
||||
r := resource.NewComposeModuleField(f, refNs, refMod)
|
||||
rMod.AddField(r)
|
||||
}
|
||||
|
||||
return envoy.CollectNodes(
|
||||
rMod,
|
||||
)
|
||||
}
|
||||
105
server/pkg/envoy/store/compose_namespace.go
Normal file
105
server/pkg/envoy/store/compose_namespace.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
)
|
||||
|
||||
type (
|
||||
composeNamespace struct {
|
||||
cfg *EncoderConfig
|
||||
|
||||
res *resource.ComposeNamespace
|
||||
ns *types.Namespace
|
||||
}
|
||||
|
||||
composeNamespaceSet []*composeNamespace
|
||||
)
|
||||
|
||||
// mergeComposeNamespaces merges b into a, prioritising a
|
||||
func mergeComposeNamespaces(a, b *types.Namespace) *types.Namespace {
|
||||
c := a.Clone()
|
||||
|
||||
if c.Name == "" {
|
||||
c.Name = b.Name
|
||||
}
|
||||
if c.Slug == "" {
|
||||
c.Slug = b.Slug
|
||||
}
|
||||
|
||||
if c.CreatedAt.IsZero() {
|
||||
c.CreatedAt = b.CreatedAt
|
||||
}
|
||||
if c.UpdatedAt == nil {
|
||||
c.UpdatedAt = b.UpdatedAt
|
||||
}
|
||||
if c.DeletedAt == nil {
|
||||
c.DeletedAt = b.DeletedAt
|
||||
}
|
||||
|
||||
// I'll just compare the entire struct for now
|
||||
if c.Meta == (types.NamespaceMeta{}) {
|
||||
c.Meta = b.Meta
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// findComposeNamespace looks for the namespace in the resources & the store
|
||||
//
|
||||
// Provided resources are prioritized.
|
||||
func findComposeNamespace(ctx context.Context, s store.Storer, rr resource.InterfaceSet, ii resource.Identifiers) (ns *types.Namespace, err error) {
|
||||
ns = resource.FindComposeNamespace(rr, ii)
|
||||
if ns != nil {
|
||||
return ns, nil
|
||||
}
|
||||
|
||||
return findComposeNamespaceStore(ctx, s, makeGenericFilter(ii))
|
||||
}
|
||||
|
||||
// findComposeNamespaceStore looks for the namespace in the store
|
||||
func findComposeNamespaceStore(ctx context.Context, s store.Storer, gf genericFilter) (ns *types.Namespace, err error) {
|
||||
if gf.id > 0 {
|
||||
ns, err = store.LookupComposeNamespaceByID(ctx, s, gf.id)
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ns != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, i := range gf.identifiers {
|
||||
ns, err = store.LookupComposeNamespaceBySlug(ctx, s, i)
|
||||
if err == store.ErrNotFound {
|
||||
var nn types.NamespaceSet
|
||||
nn, _, err = store.SearchComposeNamespaces(ctx, s, types.NamespaceFilter{
|
||||
Name: i,
|
||||
Paging: filter.Paging{
|
||||
Limit: 2,
|
||||
},
|
||||
})
|
||||
if len(nn) > 1 {
|
||||
return nil, resourceErrIdentifierNotUnique(i)
|
||||
}
|
||||
if len(nn) == 1 {
|
||||
ns = nn[0]
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ns != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
93
server/pkg/envoy/store/compose_namespace_marshal.go
Normal file
93
server/pkg/envoy/store/compose_namespace_marshal.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
)
|
||||
|
||||
func newComposeNamespaceFromResource(res *resource.ComposeNamespace, cfg *EncoderConfig) resourceState {
|
||||
return &composeNamespace{
|
||||
cfg: mergeConfig(cfg, res.Config()),
|
||||
|
||||
res: res,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *composeNamespace) Prepare(ctx context.Context, pl *payload) (err error) {
|
||||
// Reset old identifiers
|
||||
n.res.Res.ID = 0
|
||||
|
||||
// Try to get the original namespace
|
||||
n.ns, err = findComposeNamespaceStore(ctx, pl.s, makeGenericFilter(n.res.Identifiers()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if n.ns != nil {
|
||||
n.res.Res.ID = n.ns.ID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *composeNamespace) Encode(ctx context.Context, pl *payload) (err error) {
|
||||
res := n.res.Res
|
||||
exists := n.ns != nil && n.ns.ID > 0
|
||||
|
||||
// Determine the ID
|
||||
if res.ID <= 0 && exists {
|
||||
res.ID = n.ns.ID
|
||||
}
|
||||
if res.ID <= 0 {
|
||||
res.ID = NextID()
|
||||
}
|
||||
|
||||
ts := n.res.Timestamps()
|
||||
if ts != nil {
|
||||
if ts.CreatedAt != nil {
|
||||
res.CreatedAt = *ts.CreatedAt.T
|
||||
} else {
|
||||
res.CreatedAt = *now()
|
||||
}
|
||||
if ts.UpdatedAt != nil {
|
||||
res.UpdatedAt = ts.UpdatedAt.T
|
||||
}
|
||||
if ts.DeletedAt != nil {
|
||||
res.DeletedAt = ts.DeletedAt.T
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate the resource skip expression
|
||||
// @todo expand available parameters; similar implementation to compose/types/record@Dict
|
||||
if skip, err := basicSkipEval(ctx, n.cfg, !exists); err != nil {
|
||||
return err
|
||||
} else if skip {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a fresh namespace
|
||||
if !exists {
|
||||
return store.CreateComposeNamespace(ctx, pl.s, res)
|
||||
}
|
||||
|
||||
// Update existing namespace
|
||||
switch n.cfg.OnExisting {
|
||||
case resource.Skip:
|
||||
return nil
|
||||
|
||||
case resource.MergeLeft:
|
||||
res = mergeComposeNamespaces(n.ns, res)
|
||||
|
||||
case resource.MergeRight:
|
||||
res = mergeComposeNamespaces(res, n.ns)
|
||||
}
|
||||
|
||||
err = store.UpdateComposeNamespace(ctx, pl.s, res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.res.Res = res
|
||||
return nil
|
||||
}
|
||||
53
server/pkg/envoy/store/compose_namespace_test.go
Normal file
53
server/pkg/envoy/store/compose_namespace_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestComposeNamespace_Merger(t *testing.T) {
|
||||
req := require.New(t)
|
||||
|
||||
now := time.Time{}
|
||||
nowP := &time.Time{}
|
||||
|
||||
empty := &types.Namespace{}
|
||||
full := &types.Namespace{
|
||||
Name: "name",
|
||||
Slug: "slug",
|
||||
Enabled: false,
|
||||
Meta: types.NamespaceMeta{
|
||||
Description: "dsc",
|
||||
Subtitle: "sub",
|
||||
},
|
||||
|
||||
CreatedAt: now,
|
||||
UpdatedAt: nowP,
|
||||
DeletedAt: nowP,
|
||||
}
|
||||
|
||||
t.Run("merge on empty", func(t *testing.T) {
|
||||
c := mergeComposeNamespaces(empty, full)
|
||||
req.Equal("name", c.Name)
|
||||
req.Equal("slug", c.Slug)
|
||||
req.Equal("dsc", c.Meta.Description)
|
||||
req.Equal("sub", c.Meta.Subtitle)
|
||||
req.Equal(now, c.CreatedAt)
|
||||
req.Equal(nowP, c.UpdatedAt)
|
||||
req.Equal(nowP, c.DeletedAt)
|
||||
})
|
||||
|
||||
t.Run("merge with empty", func(t *testing.T) {
|
||||
c := mergeComposeNamespaces(full, empty)
|
||||
req.Equal("name", c.Name)
|
||||
req.Equal("slug", c.Slug)
|
||||
req.Equal("dsc", c.Meta.Description)
|
||||
req.Equal("sub", c.Meta.Subtitle)
|
||||
req.Equal(now, c.CreatedAt)
|
||||
req.Equal(nowP, c.UpdatedAt)
|
||||
req.Equal(nowP, c.DeletedAt)
|
||||
})
|
||||
}
|
||||
19
server/pkg/envoy/store/compose_namespace_unmarshal.go
Normal file
19
server/pkg/envoy/store/compose_namespace_unmarshal.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
)
|
||||
|
||||
func newComposeNamespace(ns *types.Namespace) *composeNamespace {
|
||||
return &composeNamespace{
|
||||
ns: ns,
|
||||
}
|
||||
}
|
||||
|
||||
func (ns *composeNamespace) MarshalEnvoy() ([]resource.Interface, error) {
|
||||
return envoy.CollectNodes(
|
||||
resource.NewComposeNamespace(ns.ns),
|
||||
)
|
||||
}
|
||||
130
server/pkg/envoy/store/compose_page.go
Normal file
130
server/pkg/envoy/store/compose_page.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
atypes "github.com/cortezaproject/corteza/server/automation/types"
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
)
|
||||
|
||||
type (
|
||||
composePage struct {
|
||||
cfg *EncoderConfig
|
||||
|
||||
res *resource.ComposePage
|
||||
pg *types.Page
|
||||
|
||||
relNS *types.Namespace
|
||||
relMod *types.Module
|
||||
relParent *types.Page
|
||||
|
||||
relWfs map[string]*atypes.Workflow
|
||||
relMods map[string]*types.Module
|
||||
relCharts map[string]*types.Chart
|
||||
}
|
||||
)
|
||||
|
||||
// mergeComposePage merges b into a, prioritising a
|
||||
func mergeComposePage(a, b *types.Page) *types.Page {
|
||||
c := a.Clone()
|
||||
|
||||
c.SelfID = b.SelfID
|
||||
c.NamespaceID = b.NamespaceID
|
||||
c.ModuleID = b.ModuleID
|
||||
c.Weight = b.Weight
|
||||
if c.Handle == "" {
|
||||
c.Handle = b.Handle
|
||||
}
|
||||
if c.Title == "" {
|
||||
c.Title = b.Title
|
||||
}
|
||||
if c.Description == "" {
|
||||
c.Description = b.Description
|
||||
}
|
||||
if len(c.Blocks) <= 0 {
|
||||
c.Blocks = b.Blocks
|
||||
}
|
||||
if len(c.Children) <= 0 {
|
||||
c.Children = b.Children
|
||||
}
|
||||
|
||||
if c.CreatedAt.IsZero() {
|
||||
c.CreatedAt = b.CreatedAt
|
||||
}
|
||||
if c.UpdatedAt == nil {
|
||||
c.UpdatedAt = b.UpdatedAt
|
||||
}
|
||||
if c.DeletedAt == nil {
|
||||
c.DeletedAt = b.DeletedAt
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// findComposePage looks for the page in the resources & the store
|
||||
//
|
||||
// Provided resources are prioritized.
|
||||
func findComposePage(ctx context.Context, s store.Storer, nsID uint64, rr resource.InterfaceSet, ii resource.Identifiers) (pg *types.Page, err error) {
|
||||
pg = resource.FindComposePage(rr, ii)
|
||||
if pg != nil {
|
||||
return pg, nil
|
||||
}
|
||||
|
||||
if nsID <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Go in the store
|
||||
return findComposePageStore(ctx, s, nsID, makeGenericFilter(ii))
|
||||
}
|
||||
|
||||
// findComposePageStore looks for the page in the store
|
||||
func findComposePageStore(ctx context.Context, s store.Storer, nsID uint64, gf genericFilter) (pg *types.Page, err error) {
|
||||
if nsID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if gf.id > 0 {
|
||||
pg, err = store.LookupComposePageByID(ctx, s, gf.id)
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if pg != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, i := range gf.identifiers {
|
||||
pg, err = store.LookupComposePageByNamespaceIDHandle(ctx, s, nsID, i)
|
||||
if err == store.ErrNotFound {
|
||||
var pp types.PageSet
|
||||
pp, _, err = store.SearchComposePages(ctx, s, types.PageFilter{
|
||||
NamespaceID: nsID,
|
||||
Title: i,
|
||||
Paging: filter.Paging{
|
||||
Limit: 2,
|
||||
},
|
||||
})
|
||||
if len(pp) > 1 {
|
||||
return nil, resourceErrIdentifierNotUnique(i)
|
||||
}
|
||||
if len(pp) == 1 {
|
||||
pg = pp[0]
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if pg != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
369
server/pkg/envoy/store/compose_page_marshal.go
Normal file
369
server/pkg/envoy/store/compose_page_marshal.go
Normal file
@@ -0,0 +1,369 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
atypes "github.com/cortezaproject/corteza/server/automation/types"
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
)
|
||||
|
||||
func newComposePageFromResource(res *resource.ComposePage, cfg *EncoderConfig) resourceState {
|
||||
return &composePage{
|
||||
cfg: mergeConfig(cfg, res.Config()),
|
||||
|
||||
res: res,
|
||||
|
||||
relWfs: make(map[string]*atypes.Workflow),
|
||||
relMods: make(map[string]*types.Module),
|
||||
relCharts: make(map[string]*types.Chart),
|
||||
}
|
||||
}
|
||||
|
||||
func (n *composePage) Prepare(ctx context.Context, pl *payload) (err error) {
|
||||
// Reset old identifiers
|
||||
n.res.Res.ID = 0
|
||||
n.res.Res.SelfID = 0
|
||||
n.res.Res.NamespaceID = 0
|
||||
n.res.Res.ModuleID = 0
|
||||
|
||||
// Get related namespace
|
||||
n.relNS, err = findComposeNamespace(ctx, pl.s, pl.state.ParentResources, n.res.RefNs.Identifiers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n.relNS == nil {
|
||||
return resource.ComposeNamespaceErrUnresolved(n.res.RefNs.Identifiers)
|
||||
}
|
||||
|
||||
// Get related module
|
||||
// If this isn't a record page, there is no related module
|
||||
if n.res.RefMod != nil {
|
||||
n.relMod, err = findComposeModule(ctx, pl.s, n.relNS.ID, pl.state.ParentResources, n.res.RefMod.Identifiers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n.relMod == nil {
|
||||
return resource.ComposeModuleErrUnresolved(n.res.RefMod.Identifiers)
|
||||
}
|
||||
}
|
||||
|
||||
// Get parent page
|
||||
if n.res.RefParent != nil {
|
||||
n.relParent, err = findComposePage(ctx, pl.s, n.relNS.ID, pl.state.ParentResources, n.res.RefParent.Identifiers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n.relParent == nil {
|
||||
return resource.ComposePageErrUnresolved(n.res.RefParent.Identifiers)
|
||||
}
|
||||
}
|
||||
|
||||
// Get related workflows
|
||||
for _, wfr := range n.res.WfRefs {
|
||||
wf, err := findAutomationWorkflow(ctx, pl.s, pl.state.ParentResources, wfr.Identifiers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if wf == nil {
|
||||
return resource.AutomationWorkflowErrUnresolved(wfr.Identifiers)
|
||||
}
|
||||
for _, id := range wfr.Identifiers {
|
||||
n.relWfs[id] = wf
|
||||
}
|
||||
}
|
||||
|
||||
// Get other related modules
|
||||
for _, mr := range n.res.ModRefs {
|
||||
mod, err := findComposeModule(ctx, pl.s, n.relNS.ID, pl.state.ParentResources, mr.Identifiers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if mod == nil {
|
||||
return resource.ComposeModuleErrUnresolved(mr.Identifiers)
|
||||
}
|
||||
for _, id := range mr.Identifiers {
|
||||
n.relMods[id] = mod
|
||||
}
|
||||
}
|
||||
|
||||
// Get related charts
|
||||
for _, refChart := range n.res.RefCharts {
|
||||
chr, err := findComposeChart(ctx, pl.s, n.relNS.ID, pl.state.ParentResources, refChart.Identifiers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if chr == nil {
|
||||
return resource.ComposeChartErrUnresolved(refChart.Identifiers)
|
||||
}
|
||||
for _, id := range refChart.Identifiers {
|
||||
n.relCharts[id] = chr
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get the original page
|
||||
n.pg, err = findComposePageStore(ctx, pl.s, n.relNS.ID, makeGenericFilter(n.res.Identifiers()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if n.pg != nil {
|
||||
n.res.Res.ID = n.pg.ID
|
||||
n.res.Res.NamespaceID = n.pg.NamespaceID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *composePage) Encode(ctx context.Context, pl *payload) (err error) {
|
||||
res := n.res.Res
|
||||
exists := n.pg != nil && n.pg.ID > 0
|
||||
|
||||
// Determine the ID
|
||||
if res.ID <= 0 && exists {
|
||||
res.ID = n.pg.ID
|
||||
}
|
||||
if res.ID <= 0 {
|
||||
res.ID = NextID()
|
||||
}
|
||||
|
||||
ts := n.res.Timestamps()
|
||||
if ts != nil {
|
||||
if ts.CreatedAt != nil {
|
||||
res.CreatedAt = *ts.CreatedAt.T
|
||||
} else {
|
||||
res.CreatedAt = *now()
|
||||
}
|
||||
if ts.UpdatedAt != nil {
|
||||
res.UpdatedAt = ts.UpdatedAt.T
|
||||
}
|
||||
if ts.DeletedAt != nil {
|
||||
res.DeletedAt = ts.DeletedAt.T
|
||||
}
|
||||
}
|
||||
|
||||
// Namespace
|
||||
res.NamespaceID = n.relNS.ID
|
||||
if res.NamespaceID <= 0 {
|
||||
ns := resource.FindComposeNamespace(pl.state.ParentResources, n.res.RefNs.Identifiers)
|
||||
res.NamespaceID = ns.ID
|
||||
}
|
||||
|
||||
if res.NamespaceID <= 0 {
|
||||
return resource.ComposeNamespaceErrUnresolved(n.res.RefNs.Identifiers)
|
||||
}
|
||||
|
||||
// Module?
|
||||
if n.res.RefMod != nil {
|
||||
res.ModuleID = n.relMod.ID
|
||||
if res.ModuleID <= 0 {
|
||||
mod := resource.FindComposeModule(pl.state.ParentResources, n.res.RefMod.Identifiers)
|
||||
res.ModuleID = mod.ID
|
||||
}
|
||||
}
|
||||
|
||||
// Parent?
|
||||
if n.res.RefParent != nil {
|
||||
res.SelfID = n.relParent.ID
|
||||
if res.SelfID <= 0 {
|
||||
mod := resource.FindComposePage(pl.state.ParentResources, n.res.RefParent.Identifiers)
|
||||
res.SelfID = mod.ID
|
||||
}
|
||||
}
|
||||
|
||||
// Blocks
|
||||
getModID := func(id string) uint64 {
|
||||
mod := n.relMods[id]
|
||||
if mod == nil || mod.ID <= 0 {
|
||||
mod = resource.FindComposeModule(pl.state.ParentResources, resource.MakeIdentifiers(id))
|
||||
if mod == nil || mod.ID <= 0 {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
return mod.ID
|
||||
}
|
||||
getWfID := func(id string) uint64 {
|
||||
wf := n.relWfs[id]
|
||||
if wf == nil || wf.ID <= 0 {
|
||||
wf = resource.FindAutomationWorkflow(pl.state.ParentResources, resource.MakeIdentifiers(id))
|
||||
if wf == nil || wf.ID <= 0 {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
return wf.ID
|
||||
}
|
||||
|
||||
getChartID := func(id string) uint64 {
|
||||
chr := n.relCharts[id]
|
||||
if chr == nil || chr.ID <= 0 {
|
||||
chr = resource.FindComposeChart(pl.state.ParentResources, resource.MakeIdentifiers(id))
|
||||
if chr == nil || chr.ID <= 0 {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
return chr.ID
|
||||
}
|
||||
|
||||
// Quick utility to extract references from options
|
||||
ss := func(m map[string]interface{}, kk ...string) string {
|
||||
for _, k := range kk {
|
||||
if vr, has := m[k]; has {
|
||||
v, _ := vr.(string)
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Get max blockID for later use
|
||||
blockID := uint64(0)
|
||||
for _, b := range res.Blocks {
|
||||
if b.BlockID > blockID {
|
||||
blockID = b.BlockID
|
||||
}
|
||||
}
|
||||
|
||||
for i, b := range res.Blocks {
|
||||
|
||||
// Assure blockIDs
|
||||
if b.BlockID == 0 {
|
||||
blockID++
|
||||
b.BlockID = blockID
|
||||
res.Blocks[i] = b
|
||||
}
|
||||
|
||||
switch b.Kind {
|
||||
case "RecordList":
|
||||
id := ss(b.Options, "module", "moduleID")
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
mID := getModID(id)
|
||||
if mID <= 0 {
|
||||
return resource.ComposeModuleErrUnresolved(resource.MakeIdentifiers(id))
|
||||
}
|
||||
b.Options["moduleID"] = strconv.FormatUint(mID, 10)
|
||||
delete(b.Options, "module")
|
||||
|
||||
case "Automation":
|
||||
bb, _ := b.Options["buttons"].([]interface{})
|
||||
for _, b := range bb {
|
||||
button, _ := b.(map[string]interface{})
|
||||
id := ss(button, "workflow", "workflowID")
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
wfID := getWfID(id)
|
||||
if wfID <= 0 {
|
||||
return resource.AutomationWorkflowErrUnresolved(resource.MakeIdentifiers(id))
|
||||
}
|
||||
button["workflowID"] = strconv.FormatUint(wfID, 10)
|
||||
delete(button, "workflow")
|
||||
}
|
||||
|
||||
case "RecordOrganizer":
|
||||
id := ss(b.Options, "module", "moduleID")
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
mID := getModID(id)
|
||||
if mID <= 0 {
|
||||
return resource.ComposeModuleErrUnresolved(resource.MakeIdentifiers(id))
|
||||
}
|
||||
b.Options["moduleID"] = strconv.FormatUint(mID, 10)
|
||||
delete(b.Options, "module")
|
||||
|
||||
case "Calendar":
|
||||
ff, _ := b.Options["feeds"].([]interface{})
|
||||
for _, f := range ff {
|
||||
feed, _ := f.(map[string]interface{})
|
||||
fOpts, _ := (feed["options"]).(map[string]interface{})
|
||||
id := ss(fOpts, "module", "moduleID")
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
mID := getModID(id)
|
||||
if mID <= 0 {
|
||||
return resource.ComposeModuleErrUnresolved(resource.MakeIdentifiers(id))
|
||||
}
|
||||
fOpts["moduleID"] = strconv.FormatUint(mID, 10)
|
||||
delete(fOpts, "module")
|
||||
}
|
||||
|
||||
case "Chart":
|
||||
id := ss(b.Options, "chart", "chartID")
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
chrID := getChartID(id)
|
||||
if chrID == 0 {
|
||||
return resource.ComposeChartErrUnresolved(resource.MakeIdentifiers(id))
|
||||
}
|
||||
b.Options["chartID"] = strconv.FormatUint(chrID, 10)
|
||||
delete(b.Options, "chart")
|
||||
|
||||
case "Metric":
|
||||
mm, _ := b.Options["metrics"].([]interface{})
|
||||
for _, m := range mm {
|
||||
mops, _ := m.(map[string]interface{})
|
||||
id := ss(mops, "module", "moduleID")
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
mID := getModID(id)
|
||||
if mID <= 0 {
|
||||
return resource.ComposeModuleErrUnresolved(resource.MakeIdentifiers(id))
|
||||
}
|
||||
mops["moduleID"] = strconv.FormatUint(mID, 10)
|
||||
delete(mops, "module")
|
||||
|
||||
}
|
||||
|
||||
case "Comment":
|
||||
id := ss(b.Options, "module", "moduleID")
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
mID := getModID(id)
|
||||
if mID <= 0 {
|
||||
return resource.ComposeModuleErrUnresolved(resource.MakeIdentifiers(id))
|
||||
}
|
||||
b.Options["moduleID"] = strconv.FormatUint(mID, 10)
|
||||
delete(b.Options, "module")
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate the resource skip expression
|
||||
// @todo expand available parameters; similar implementation to compose/types/record@Dict
|
||||
if skip, err := basicSkipEval(ctx, n.cfg, !exists); err != nil {
|
||||
return err
|
||||
} else if skip {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a fresh page
|
||||
if !exists {
|
||||
return store.CreateComposePage(ctx, pl.s, res)
|
||||
}
|
||||
|
||||
// Update existing page
|
||||
switch n.cfg.OnExisting {
|
||||
case resource.Skip:
|
||||
return nil
|
||||
|
||||
case resource.MergeLeft:
|
||||
res = mergeComposePage(n.pg, res)
|
||||
|
||||
case resource.MergeRight:
|
||||
res = mergeComposePage(res, n.pg)
|
||||
}
|
||||
|
||||
err = store.UpdateComposePage(ctx, pl.s, res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.res.Res = res
|
||||
return nil
|
||||
}
|
||||
69
server/pkg/envoy/store/compose_page_test.go
Normal file
69
server/pkg/envoy/store/compose_page_test.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestComposePage_Merger(t *testing.T) {
|
||||
req := require.New(t)
|
||||
|
||||
now := time.Time{}
|
||||
nowP := &time.Time{}
|
||||
|
||||
empty := &types.Page{}
|
||||
full := &types.Page{
|
||||
SelfID: 1,
|
||||
NamespaceID: 2,
|
||||
ModuleID: 3,
|
||||
Handle: "handle",
|
||||
Title: "title",
|
||||
Description: "description",
|
||||
Blocks: types.PageBlocks{types.PageBlock{}},
|
||||
Children: types.PageSet{&types.Page{}},
|
||||
Weight: 4,
|
||||
|
||||
CreatedAt: now,
|
||||
UpdatedAt: nowP,
|
||||
DeletedAt: nowP,
|
||||
}
|
||||
|
||||
t.Run("merge on empty", func(t *testing.T) {
|
||||
c := mergeComposePage(empty, full)
|
||||
req.Equal(uint64(1), c.SelfID)
|
||||
req.Equal(uint64(2), c.NamespaceID)
|
||||
req.Equal(uint64(3), c.ModuleID)
|
||||
|
||||
req.Equal("handle", c.Handle)
|
||||
req.Equal("title", c.Title)
|
||||
req.Equal("description", c.Description)
|
||||
req.Len(c.Blocks, 1)
|
||||
req.Len(c.Children, 1)
|
||||
req.Equal(4, c.Weight)
|
||||
|
||||
req.Equal(now, c.CreatedAt)
|
||||
req.Equal(nowP, c.UpdatedAt)
|
||||
req.Equal(nowP, c.DeletedAt)
|
||||
})
|
||||
|
||||
t.Run("merge with empty", func(t *testing.T) {
|
||||
c := mergeComposePage(full, empty)
|
||||
req.Equal(uint64(0), c.SelfID)
|
||||
req.Equal(uint64(0), c.NamespaceID)
|
||||
req.Equal(uint64(0), c.ModuleID)
|
||||
|
||||
req.Equal("handle", c.Handle)
|
||||
req.Equal("title", c.Title)
|
||||
req.Equal("description", c.Description)
|
||||
req.Len(c.Blocks, 1)
|
||||
req.Len(c.Children, 1)
|
||||
req.Equal(0, c.Weight)
|
||||
|
||||
req.Equal(now, c.CreatedAt)
|
||||
req.Equal(nowP, c.UpdatedAt)
|
||||
req.Equal(nowP, c.DeletedAt)
|
||||
})
|
||||
}
|
||||
31
server/pkg/envoy/store/compose_page_unmarshal.go
Normal file
31
server/pkg/envoy/store/compose_page_unmarshal.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
)
|
||||
|
||||
func newComposePage(pg *types.Page) *composePage {
|
||||
return &composePage{
|
||||
pg: pg,
|
||||
}
|
||||
}
|
||||
|
||||
func (pg *composePage) MarshalEnvoy() ([]resource.Interface, error) {
|
||||
var (
|
||||
refNs = resource.MakeNamespaceRef(pg.pg.NamespaceID, "", "")
|
||||
refMod *resource.Ref
|
||||
refParent *resource.Ref
|
||||
)
|
||||
if pg.pg.ModuleID > 0 {
|
||||
refMod = resource.MakeModuleRef(pg.pg.ModuleID, "", "")
|
||||
}
|
||||
if pg.pg.SelfID > 0 {
|
||||
refParent = resource.MakePageRef(pg.pg.SelfID, "", "")
|
||||
}
|
||||
|
||||
return envoy.CollectNodes(
|
||||
resource.NewComposePage(pg.pg, refNs, refMod, refParent),
|
||||
)
|
||||
}
|
||||
35
server/pkg/envoy/store/compose_record.go
Normal file
35
server/pkg/envoy/store/compose_record.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
)
|
||||
|
||||
type (
|
||||
composeRecordAux struct {
|
||||
refMod string
|
||||
relMod *types.Module
|
||||
|
||||
refNs string
|
||||
relUsers resource.UserstampIndex
|
||||
walker resource.CrsWalker
|
||||
}
|
||||
|
||||
composeRecord struct {
|
||||
cfg *EncoderConfig
|
||||
|
||||
res *resource.ComposeRecord
|
||||
rec *composeRecordAux
|
||||
|
||||
relNS *types.Namespace
|
||||
relMod *types.Module
|
||||
|
||||
fieldModRef map[string]resource.Identifiers
|
||||
// module identifier -> record identifier -> recordID
|
||||
externalRef map[string]map[string]uint64
|
||||
recMap map[string]*types.Record
|
||||
|
||||
// Little helper flag for conditional encoding
|
||||
missing bool
|
||||
}
|
||||
)
|
||||
471
server/pkg/envoy/store/compose_record_marshal.go
Normal file
471
server/pkg/envoy/store/compose_record_marshal.go
Normal file
@@ -0,0 +1,471 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/dalutils"
|
||||
"github.com/cortezaproject/corteza/server/compose/service"
|
||||
"github.com/cortezaproject/corteza/server/compose/service/values"
|
||||
composeTypes "github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
systemTypes "github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
var (
|
||||
rvSanitizer = values.Sanitizer()
|
||||
rvValidator = values.Validator()
|
||||
rvFormatter = values.Formatter()
|
||||
)
|
||||
|
||||
func NewComposeRecordFromResource(res *resource.ComposeRecord, cfg *EncoderConfig) resourceState {
|
||||
return &composeRecord{
|
||||
cfg: cfg,
|
||||
fieldModRef: make(map[string]resource.Identifiers),
|
||||
externalRef: make(map[string]map[string]uint64),
|
||||
recMap: make(map[string]*composeTypes.Record),
|
||||
|
||||
res: res,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *composeRecord) Prepare(ctx context.Context, pl *payload) (err error) {
|
||||
// Get related namespace
|
||||
n.relNS, err = findComposeNamespace(ctx, pl.s, pl.state.ParentResources, n.res.RefNs.Identifiers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n.relNS == nil {
|
||||
return resource.ComposeNamespaceErrUnresolved(n.res.RefNs.Identifiers)
|
||||
}
|
||||
|
||||
n.missing = true
|
||||
n.relMod = resource.FindComposeModule(pl.state.ParentResources, n.res.RefMod.Identifiers)
|
||||
if n.relMod == nil && n.relNS.ID > 0 {
|
||||
n.relMod, err = findComposeModuleStore(ctx, pl.s, n.relNS.ID, makeGenericFilter(n.res.RefMod.Identifiers))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n.relMod != nil {
|
||||
// Preload existing fields
|
||||
n.relMod.Fields, err = findComposeModuleFieldsStore(ctx, pl.s, n.relMod)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if n.relMod == nil {
|
||||
return resource.ComposeModuleErrUnresolved(n.res.RefMod.Identifiers)
|
||||
}
|
||||
|
||||
// Preload sys users
|
||||
if n.res.UserFlakes == nil {
|
||||
n.res.UserFlakes = make(resource.UserstampIndex)
|
||||
}
|
||||
if len(n.res.UserFlakes) == 0 {
|
||||
// No users provided, let's try to fetch them
|
||||
uu, _, err := store.SearchUsers(ctx, pl.s, systemTypes.UserFilter{
|
||||
Paging: filter.Paging{
|
||||
Limit: 0,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n.res.UserFlakes.Add(uu...)
|
||||
}
|
||||
|
||||
// Add missing refs
|
||||
preloadRefs := make(resource.RefSet, 0, int(len(n.relMod.Fields)/2)+1)
|
||||
for _, f := range n.relMod.Fields {
|
||||
switch f.Kind {
|
||||
case "Record":
|
||||
refM := f.Options.String("module")
|
||||
if refM == "" {
|
||||
refM = f.Options.String("moduleID")
|
||||
}
|
||||
if refM != "" && refM != "0" {
|
||||
// Make a reference with that module's records
|
||||
ref := n.res.AddRef(composeTypes.RecordResourceType, refM).Constraint(n.res.RefNs)
|
||||
|
||||
n.fieldModRef[f.Name] = ref.Identifiers
|
||||
preloadRefs = append(preloadRefs, ref)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Can't do anything else, since the NS doesn't yet exist
|
||||
if n.relNS.ID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Preload potential references
|
||||
//
|
||||
// This is a fairly primitive approach, try to think of something a bit nicer
|
||||
for _, ref := range preloadRefs {
|
||||
mod, err := findComposeModuleStore(ctx, pl.s, n.relNS.ID, makeGenericFilter(ref.Identifiers))
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return err
|
||||
}
|
||||
auxMap := make(map[string]uint64)
|
||||
for _, i := range ref.Identifiers {
|
||||
n.externalRef[i] = auxMap
|
||||
}
|
||||
|
||||
// Preload all records
|
||||
rr, _, err := dalutils.ComposeRecordsList(ctx, pl.dal, mod, composeTypes.RecordFilter{
|
||||
ModuleID: mod.ID,
|
||||
NamespaceID: mod.NamespaceID,
|
||||
Paging: filter.Paging{
|
||||
Limit: 0,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, r := range rr {
|
||||
auxMap[strconv.FormatUint(r.ID, 10)] = r.ID
|
||||
}
|
||||
}
|
||||
|
||||
// Can't work with own record because the module doesn't yet exist
|
||||
if n.relMod.ID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Preload own records
|
||||
rr, _, err := dalutils.ComposeRecordsList(ctx, pl.dal, n.relMod, composeTypes.RecordFilter{
|
||||
ModuleID: n.relMod.ID,
|
||||
NamespaceID: n.relNS.ID,
|
||||
Paging: filter.Paging{
|
||||
Limit: 0,
|
||||
},
|
||||
})
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return err
|
||||
}
|
||||
n.missing = len(rr) == 0
|
||||
|
||||
// Map existing records so we can perform updates
|
||||
// Map to xref map for easier use later
|
||||
auxMap := make(map[string]uint64)
|
||||
for _, i := range n.res.RefMod.Identifiers {
|
||||
n.externalRef[i] = auxMap
|
||||
}
|
||||
for _, r := range rr {
|
||||
key := strconv.FormatUint(r.ID, 10)
|
||||
n.recMap[key] = r
|
||||
|
||||
// Map IDs to xref map
|
||||
auxMap[key] = r.ID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// @note composeRecord.Encode can raise an error in case of unresolved user dependencies.
|
||||
func (n *composeRecord) Encode(ctx context.Context, pl *payload) (err error) {
|
||||
// Namespace
|
||||
nsID := n.relNS.ID
|
||||
if nsID <= 0 {
|
||||
ns := resource.FindComposeNamespace(pl.state.ParentResources, n.res.RefNs.Identifiers)
|
||||
nsID = ns.ID
|
||||
}
|
||||
|
||||
// Module
|
||||
mod := n.relMod
|
||||
if mod.ID <= 0 {
|
||||
m := resource.FindComposeModule(pl.state.ParentResources, n.res.RefMod.Identifiers)
|
||||
mod.ID = m.ID
|
||||
}
|
||||
|
||||
// Aggregate all of the available users
|
||||
// ux will map { identifier: userID }
|
||||
ux := make(map[string]uint64)
|
||||
// Firstly the users provided by the record pl.state itself
|
||||
for _, ur := range n.res.UserFlakes {
|
||||
u := ur.U
|
||||
|
||||
ux[strconv.FormatUint(u.ID, 10)] = u.ID
|
||||
if u.Handle != "" {
|
||||
ux[u.Handle] = u.ID
|
||||
}
|
||||
if u.Name != "" {
|
||||
ux[u.Name] = u.ID
|
||||
}
|
||||
if u.Email != "" {
|
||||
ux[u.Email] = u.ID
|
||||
}
|
||||
}
|
||||
// Next all of the encoded users.
|
||||
// If identifiers overwrite eachother, that's fine.
|
||||
for _, ref := range pl.state.ParentResources {
|
||||
if ref.ResourceType() == systemTypes.UserResourceType {
|
||||
refUsr := ref.(*resource.User)
|
||||
for _, i := range refUsr.Identifiers() {
|
||||
ux[i] = refUsr.SysID()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Some pointing
|
||||
rm := n.recMap
|
||||
im := n.res.IDMap
|
||||
|
||||
getKey := func(i int, k string) string {
|
||||
if k == "" {
|
||||
return strconv.FormatInt(int64(i), 10)
|
||||
}
|
||||
|
||||
return k
|
||||
}
|
||||
|
||||
checkXRef := func(ii resource.Identifiers, ref string) (uint64, error) {
|
||||
var auxMap map[string]uint64
|
||||
for _, ri := range ii {
|
||||
if mp, ok := n.externalRef[ri]; ok {
|
||||
auxMap = mp
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if auxMap == nil || len(auxMap) == 0 {
|
||||
return 0, fmt.Errorf("referenced record not resolved: %s", resource.ComposeRecordErrUnresolved(resource.MakeIdentifiers(ref)))
|
||||
}
|
||||
|
||||
return auxMap[ref], nil
|
||||
}
|
||||
|
||||
i := -1
|
||||
return n.res.Walker(func(r *resource.ComposeRecordRaw) error {
|
||||
i++
|
||||
|
||||
// So we don't have to worry about nil
|
||||
cfg := r.Config
|
||||
if cfg == nil {
|
||||
cfg = &resource.EnvoyConfig{}
|
||||
}
|
||||
|
||||
if cfg.SkipIf != "" {
|
||||
evl, err := exprP.NewEvaluable(cfg.SkipIf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// @todo expand this
|
||||
skip, err := evl.EvalBool(ctx, map[string]interface{}{
|
||||
"missing": n.missing,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if skip {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
rec := &composeTypes.Record{
|
||||
ID: im[getKey(i, r.ID)],
|
||||
NamespaceID: nsID,
|
||||
ModuleID: mod.ID,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
exists := false
|
||||
var old *composeTypes.Record
|
||||
if r.ID != "" {
|
||||
old = rm[r.ID]
|
||||
exists = old != nil
|
||||
}
|
||||
|
||||
if rec.ID == 0 && exists {
|
||||
rec.ID = rm[r.ID].ID
|
||||
}
|
||||
if rec.ID == 0 {
|
||||
rec.ID = NextID()
|
||||
}
|
||||
|
||||
im[getKey(i, r.ID)] = rec.ID
|
||||
|
||||
if pl.state.Conflicting {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := func() error {
|
||||
// Timestamps
|
||||
if r.Ts != nil {
|
||||
if r.Ts.CreatedAt != nil {
|
||||
rec.CreatedAt = *r.Ts.CreatedAt.T
|
||||
} else {
|
||||
rec.CreatedAt = *now()
|
||||
}
|
||||
if r.Ts.UpdatedAt != nil {
|
||||
rec.UpdatedAt = r.Ts.UpdatedAt.T
|
||||
}
|
||||
if r.Ts.DeletedAt != nil {
|
||||
rec.DeletedAt = r.Ts.DeletedAt.T
|
||||
}
|
||||
}
|
||||
|
||||
// Userstamps
|
||||
rec.CreatedBy = pl.invokerID
|
||||
if r.Us != nil {
|
||||
if r.Us.CreatedBy != nil {
|
||||
rec.CreatedBy = ux[r.Us.CreatedBy.Ref]
|
||||
}
|
||||
if r.Us.UpdatedBy != nil {
|
||||
rec.UpdatedBy = ux[r.Us.UpdatedBy.Ref]
|
||||
}
|
||||
if r.Us.DeletedBy != nil {
|
||||
rec.DeletedBy = ux[r.Us.DeletedBy.Ref]
|
||||
}
|
||||
if r.Us.OwnedBy != nil {
|
||||
rec.OwnedBy = ux[r.Us.OwnedBy.Ref]
|
||||
}
|
||||
}
|
||||
|
||||
rec.OwnedBy = service.CalcRecordOwner(0, rec.OwnedBy, pl.invokerID)
|
||||
|
||||
rvs := make(composeTypes.RecordValueSet, 0, len(r.Values))
|
||||
for k, rr := range r.Values {
|
||||
v := rr.Value
|
||||
|
||||
rv := &composeTypes.RecordValue{
|
||||
RecordID: rec.ID,
|
||||
Name: k,
|
||||
Value: v,
|
||||
Updated: true,
|
||||
}
|
||||
|
||||
f := mod.Fields.FindByName(k)
|
||||
if f != nil && v != "" {
|
||||
switch f.Kind {
|
||||
case "User":
|
||||
uID := ux[v]
|
||||
if uID == 0 {
|
||||
return resource.UserErrUnresolved(resource.MakeIdentifiers(v))
|
||||
}
|
||||
rv.Value = strconv.FormatUint(uID, 10)
|
||||
rv.Ref = uID
|
||||
|
||||
case "Record":
|
||||
refIdentifiers, ok := n.fieldModRef[f.Name]
|
||||
if !ok {
|
||||
return fmt.Errorf("module field record reference not resoled: %s", f.Name)
|
||||
}
|
||||
|
||||
// if self...
|
||||
if n.res.RefMod.Identifiers.HasAny(refIdentifiers) {
|
||||
rID := im[v]
|
||||
|
||||
// Check if its in the store
|
||||
if rID == 0 {
|
||||
// Check if we have an xref
|
||||
rID, err = checkXRef(refIdentifiers, v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if rID == 0 {
|
||||
return resource.ComposeRecordErrUnresolved(resource.MakeIdentifiers(v))
|
||||
}
|
||||
rv.Value = strconv.FormatUint(rID, 10)
|
||||
rv.Ref = rID
|
||||
} else {
|
||||
// not self...
|
||||
rID := uint64(0)
|
||||
refRes := resource.FindComposeRecordResource(pl.state.ParentResources, refIdentifiers)
|
||||
|
||||
if refRes != nil {
|
||||
// check if parent has it
|
||||
rID = refRes.IDMap[v]
|
||||
}
|
||||
|
||||
if rID == 0 {
|
||||
// Check if we have an xref
|
||||
rID, err = checkXRef(refIdentifiers, v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if rID == 0 {
|
||||
return fmt.Errorf("referenced record not resolved: %s", resource.ComposeRecordErrUnresolved(resource.MakeIdentifiers(v)))
|
||||
}
|
||||
|
||||
rv.Value = strconv.FormatUint(rID, 10)
|
||||
rv.Ref = rID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rvs = append(rvs, rv)
|
||||
}
|
||||
|
||||
if err = service.RecordValueSanitization(mod, rvs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rec.Values = rvs
|
||||
rec.Values.SetUpdatedFlag(true)
|
||||
|
||||
// @todo owner?
|
||||
var (
|
||||
rve *composeTypes.RecordValueErrorSet
|
||||
|
||||
canAccessField = func(f *composeTypes.ModuleField) bool {
|
||||
return true
|
||||
}
|
||||
)
|
||||
|
||||
if old != nil {
|
||||
rec.Values = old.Values.Merge(mod.Fields, rec.Values, canAccessField)
|
||||
}
|
||||
|
||||
rec.Values, _ = rec.Values.Filter(func(v *composeTypes.RecordValue) (bool, error) {
|
||||
return mod.Fields.HasName(v.Name), nil
|
||||
})
|
||||
|
||||
rve = service.RecordValueUpdateOpCheck(ctx, nil, mod, rec.Values)
|
||||
if !rve.IsValid() {
|
||||
return rve
|
||||
}
|
||||
|
||||
rve = service.RecordPreparer(ctx, pl.s, rvSanitizer, rvValidator, rvFormatter, mod, rec)
|
||||
if !rve.IsValid() {
|
||||
return rve
|
||||
}
|
||||
|
||||
// Create a new record
|
||||
if !exists {
|
||||
err = dalutils.ComposeRecordCreate(ctx, pl.dal, mod, rec)
|
||||
return err
|
||||
}
|
||||
|
||||
// Update existing
|
||||
err = dalutils.ComposeRecordUpdate(ctx, pl.dal, mod, rec)
|
||||
return err
|
||||
}()
|
||||
|
||||
if n.cfg.Defer != nil {
|
||||
n.cfg.Defer()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if n.cfg.DeferNok != nil {
|
||||
return n.cfg.DeferNok(err)
|
||||
}
|
||||
return err
|
||||
} else if n.cfg.DeferOk != nil {
|
||||
n.cfg.DeferOk()
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
23
server/pkg/envoy/store/compose_record_unmarshal.go
Normal file
23
server/pkg/envoy/store/compose_record_unmarshal.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
)
|
||||
|
||||
func newComposeRecordFromAux(rec *composeRecordAux) *composeRecord {
|
||||
return &composeRecord{
|
||||
rec: rec,
|
||||
relMod: rec.relMod,
|
||||
}
|
||||
}
|
||||
|
||||
func (rec *composeRecord) MarshalEnvoy() ([]resource.Interface, error) {
|
||||
rr := resource.NewComposeRecordSet(rec.rec.walker, rec.rec.refNs, rec.rec.refMod)
|
||||
rr.SetUserFlakes(rec.rec.relUsers)
|
||||
rr.RelMod = rec.relMod
|
||||
|
||||
return envoy.CollectNodes(
|
||||
rr,
|
||||
)
|
||||
}
|
||||
211
server/pkg/envoy/store/decoder.go
Normal file
211
server/pkg/envoy/store/decoder.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/pkg/id"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
decoder struct {
|
||||
ux *userIndex
|
||||
}
|
||||
|
||||
DecodeFilter struct {
|
||||
|
||||
// Compose stuff
|
||||
composeNamespace []*composeNamespaceFilter
|
||||
composeModule []*composeModuleFilter
|
||||
composeRecord []*composeRecordFilter
|
||||
composePage []*composePageFilter
|
||||
composeChart []*composeChartFilter
|
||||
|
||||
// System stuff
|
||||
roles []*roleFilter
|
||||
users []*userFilter
|
||||
templates []*templateFilter
|
||||
applications []*applicationFilter
|
||||
apiGwRoutes []*apiGwRouteFilter
|
||||
reports []*reportFilter
|
||||
|
||||
settings []*settingFilter
|
||||
rbac []*rbacFilter
|
||||
resourceTranslations []*resourceTranslationFilter
|
||||
|
||||
// Automation stuff
|
||||
automationWorkflow []*automationWorkflowFilter
|
||||
}
|
||||
|
||||
// These two aux things let us simplify the code a little bit
|
||||
auxMarshaller []envoy.Marshaller
|
||||
auxRsp struct {
|
||||
mm []envoy.Marshaller
|
||||
err error
|
||||
}
|
||||
|
||||
// We'll use the userIndex to hold required users
|
||||
userIndex struct {
|
||||
users map[uint64]*types.User
|
||||
s store.Users
|
||||
}
|
||||
)
|
||||
|
||||
func Decoder() *decoder {
|
||||
return &decoder{}
|
||||
}
|
||||
|
||||
func NewDecodeFilter() *DecodeFilter {
|
||||
return &DecodeFilter{}
|
||||
}
|
||||
|
||||
func (df *DecodeFilter) FromResource(rr ...string) *DecodeFilter {
|
||||
df = df.systemFromResource(rr...)
|
||||
df = df.automationFromResource(rr...)
|
||||
df = df.composeFromResource(rr...)
|
||||
|
||||
return df
|
||||
}
|
||||
|
||||
func (df *DecodeFilter) FromRef(rr ...*resource.Ref) *DecodeFilter {
|
||||
df = df.systemFromRef(rr...)
|
||||
df = df.automationFromRef(rr...)
|
||||
df = df.composeFromRef(rr...)
|
||||
|
||||
return df
|
||||
}
|
||||
|
||||
func (aum auxMarshaller) MarshalEnvoy() ([]resource.Interface, error) {
|
||||
ii := make([]resource.Interface, 0, len(aum))
|
||||
for _, m := range aum {
|
||||
tmp, err := m.MarshalEnvoy()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Default locales for applicable resources
|
||||
for _, t := range tmp {
|
||||
if li, ok := t.(resource.LocaleInterface); ok {
|
||||
ll, err := li.EncodeTranslations()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, l := range ll {
|
||||
l.MarkDefault()
|
||||
ii = append(ii, l)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ii = append(ii, tmp...)
|
||||
}
|
||||
|
||||
return ii, nil
|
||||
}
|
||||
|
||||
// Decode decodes all of the things in the provided store
|
||||
func (d *decoder) Decode(ctx context.Context, s store.Storer, dal dalService, f *DecodeFilter) ([]resource.Interface, error) {
|
||||
mm := make(auxMarshaller, 0, 100)
|
||||
|
||||
if d.ux == nil {
|
||||
d.ux = &userIndex{
|
||||
users: make(map[uint64]*types.User),
|
||||
}
|
||||
}
|
||||
d.ux.s = s
|
||||
|
||||
pof := func(rr ...*auxRsp) (auxMarshaller, error) {
|
||||
mm := make(auxMarshaller, 0, 200)
|
||||
|
||||
for _, r := range rr {
|
||||
if r == nil {
|
||||
continue
|
||||
}
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
|
||||
mm = append(mm, r.mm...)
|
||||
}
|
||||
|
||||
return mm, nil
|
||||
}
|
||||
|
||||
compose := newComposeDecoder(dal)
|
||||
system := newSystemDecoder(d.ux)
|
||||
automation := newAutomationDecoder(d.ux)
|
||||
|
||||
mm, err := pof(
|
||||
compose.decodeComposeNamespace(ctx, s, f.composeNamespace),
|
||||
compose.decodeComposeModule(ctx, s, f.composeModule),
|
||||
compose.decodeComposeRecord(ctx, s, f.composeRecord),
|
||||
compose.decodeComposePage(ctx, s, f.composePage),
|
||||
compose.decodeComposeChart(ctx, s, f.composeChart),
|
||||
|
||||
system.decodeRoles(ctx, s, f.roles),
|
||||
system.decodeUsers(ctx, s, f.users),
|
||||
system.decodeTemplates(ctx, s, f.templates),
|
||||
system.decodeApplications(ctx, s, f.applications),
|
||||
system.decodeAPIGWRoutes(ctx, s, f.apiGwRoutes),
|
||||
system.decodeReports(ctx, s, f.reports),
|
||||
system.decodeSettings(ctx, s, f.settings),
|
||||
system.decodeResourceTranslation(ctx, s, f.resourceTranslations),
|
||||
|
||||
automation.decodeWorkflows(ctx, s, f.automationWorkflow),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f.allowRbacResource(compose.resourceID...)
|
||||
f.allowRbacResource(system.resourceID...)
|
||||
f.allowRbacResource(automation.resourceID...)
|
||||
rr, err := pof(
|
||||
system.decodeRbac(ctx, s, f.rbac),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return append(rr, mm...).MarshalEnvoy()
|
||||
}
|
||||
|
||||
func (ux *userIndex) add(ctx context.Context, uu ...uint64) error {
|
||||
// List of filtered users
|
||||
filtered := make([]uint64, 0, len(uu))
|
||||
|
||||
for _, u := range uu {
|
||||
// not defined, we don't need
|
||||
if u == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// we have, no need
|
||||
if ux.users[u] != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
filtered = append(filtered, u)
|
||||
}
|
||||
|
||||
if len(filtered) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
users, _, err := store.SearchUsers(ctx, ux.s, types.UserFilter{
|
||||
UserID: id.Strings(filtered...),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, u := range users {
|
||||
ux.users[u.ID] = u
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
229
server/pkg/envoy/store/encoder.go
Normal file
229
server/pkg/envoy/store/encoder.go
Normal file
@@ -0,0 +1,229 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/auth"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/pkg/rbac"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
)
|
||||
|
||||
type (
|
||||
storeEncoder struct {
|
||||
s store.Storer
|
||||
dal dalService
|
||||
cfg *EncoderConfig
|
||||
|
||||
// Each resource should define its own state that is used when encoding the resource.
|
||||
// Such approach removes the need for a janky generic global state.
|
||||
// This also simplifies any slight deviations between resource complexities.
|
||||
state map[resource.Interface]resourceState
|
||||
}
|
||||
|
||||
// EncoderConfig allows us to configure the resource encoding process
|
||||
EncoderConfig struct {
|
||||
// OnExisting defines what to do if the resource exists
|
||||
OnExisting resource.MergeAlg
|
||||
// Skip if defines a pkg/expr expression when to skip the resource
|
||||
SkipIf string
|
||||
// Defer is called after the resource is encoded, regardles of the result
|
||||
Defer func()
|
||||
// DeferOk is called after the resource is encoded, only when successful
|
||||
DeferOk func()
|
||||
// DeferNok is called after the resource is encoded, only when failed
|
||||
// If you return an error, the encoding will terminate.
|
||||
// If you return nil (ignore the error), the encoding will continue.
|
||||
DeferNok func(error) error
|
||||
}
|
||||
|
||||
accessControlRBACServicer interface {
|
||||
Can([]uint64, string, rbac.Resource) bool
|
||||
}
|
||||
|
||||
composeAccessController interface {
|
||||
composeRecordValueAccessController
|
||||
composeRecordAccessController
|
||||
}
|
||||
composeRecordValueAccessController interface {
|
||||
CanReadRecordValueOnModuleField(context.Context, *types.ModuleField) bool
|
||||
CanUpdateRecordValueOnModuleField(context.Context, *types.ModuleField) bool
|
||||
}
|
||||
|
||||
composeRecordAccessController interface {
|
||||
CanCreateRecordOnModule(context.Context, *types.Module) bool
|
||||
CanUpdateRecord(context.Context, *types.Record) bool
|
||||
CanDeleteRecord(context.Context, *types.Record) bool
|
||||
}
|
||||
|
||||
payload struct {
|
||||
s store.Storer
|
||||
dal dalService
|
||||
state *envoy.ResourceState
|
||||
|
||||
invokerID uint64
|
||||
}
|
||||
|
||||
// resourceState allows each conforming struct to be initialized and encoded
|
||||
// by the store encoder
|
||||
resourceState interface {
|
||||
Prepare(context.Context, *payload) (err error)
|
||||
Encode(context.Context, *payload) (err error)
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnknownResource = fmt.Errorf("unknown resource")
|
||||
ErrResourceStateUndefined = fmt.Errorf("undefined resource state")
|
||||
)
|
||||
|
||||
// NewStoreEncoder initializes a fresh store encoder
|
||||
//
|
||||
// If no config is provided, it uses Skip as the default merge alg.
|
||||
func NewStoreEncoder(s store.Storer, dal dalService, cfg *EncoderConfig) envoy.PrepareEncoder {
|
||||
if cfg == nil {
|
||||
cfg = &EncoderConfig{
|
||||
OnExisting: resource.Skip,
|
||||
}
|
||||
}
|
||||
|
||||
return &storeEncoder{
|
||||
s: s,
|
||||
dal: dal,
|
||||
cfg: cfg,
|
||||
|
||||
state: make(map[resource.Interface]resourceState),
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare prepares the encoder for the given set of resources
|
||||
//
|
||||
// It initializes and prepares the resource state for each provided resource
|
||||
func (se *storeEncoder) Prepare(ctx context.Context, ee ...*envoy.ResourceState) (err error) {
|
||||
f := func(rs resourceState, ers *envoy.ResourceState) error {
|
||||
if rs == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = rs.Prepare(ctx, se.makePayload(ctx, se.s, se.dal, ers))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
se.state[ers.Res] = rs
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, ers := range ee {
|
||||
// Skip placeholders
|
||||
if ers.Res.Placeholder() {
|
||||
continue
|
||||
}
|
||||
|
||||
switch res := ers.Res.(type) {
|
||||
// Compose resources
|
||||
case *resource.ComposeNamespace:
|
||||
err = f(newComposeNamespaceFromResource(res, se.cfg), ers)
|
||||
case *resource.ComposeModule:
|
||||
err = f(NewComposeModuleFromResource(res, se.cfg), ers)
|
||||
case *resource.ComposeRecord:
|
||||
err = f(NewComposeRecordFromResource(res, se.cfg), ers)
|
||||
case *resource.ComposeChart:
|
||||
err = f(newComposeChartFromResource(res, se.cfg), ers)
|
||||
case *resource.ComposePage:
|
||||
err = f(newComposePageFromResource(res, se.cfg), ers)
|
||||
|
||||
// System resources
|
||||
case *resource.User:
|
||||
err = f(NewUserFromResource(res, se.cfg), ers)
|
||||
case *resource.Template:
|
||||
err = f(NewTemplateFromResource(res, se.cfg), ers)
|
||||
case *resource.Role:
|
||||
err = f(NewRoleFromResource(res, se.cfg), ers)
|
||||
case *resource.Application:
|
||||
err = f(NewApplicationFromResource(res, se.cfg), ers)
|
||||
case *resource.APIGateway:
|
||||
err = f(newAPIGatewayFromResource(res, se.cfg), ers)
|
||||
case *resource.Report:
|
||||
err = f(newReportFromResource(res, se.cfg), ers)
|
||||
case *resource.Setting:
|
||||
err = f(NewSettingFromResource(res, se.cfg), ers)
|
||||
case *resource.RbacRule:
|
||||
err = f(newRbacRuleFromResource(res, se.cfg), ers)
|
||||
case *resource.ResourceTranslation:
|
||||
err = f(newResourceTranslationFromResource(res, se.cfg), ers)
|
||||
|
||||
// Automation resources
|
||||
case *resource.AutomationWorkflow:
|
||||
err = f(newAutomationWorkflowFromResource(res, se.cfg), ers)
|
||||
|
||||
default:
|
||||
err = ErrUnknownResource
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return se.WrapError("prepare", ers.Res, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Encode encodes available resource states using the given store encoder
|
||||
func (se *storeEncoder) Encode(ctx context.Context, p envoy.Provider) error {
|
||||
var ers *envoy.ResourceState
|
||||
return store.Tx(ctx, se.s, func(ctx context.Context, s store.Storer) (err error) {
|
||||
for {
|
||||
ers, err = p.NextInverted(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ers == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Skip placeholders
|
||||
if ers.Res.Placeholder() {
|
||||
continue
|
||||
}
|
||||
|
||||
state := se.state[ers.Res]
|
||||
if state == nil {
|
||||
err = ErrResourceStateUndefined
|
||||
} else {
|
||||
if state != nil {
|
||||
err = state.Encode(ctx, se.makePayload(ctx, s, se.dal, ers))
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return se.WrapError("encode", ers.Res, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (se *storeEncoder) makePayload(ctx context.Context, s store.Storer, dal dalService, ers *envoy.ResourceState) *payload {
|
||||
return &payload{
|
||||
s: s,
|
||||
dal: dal,
|
||||
state: ers,
|
||||
invokerID: auth.GetIdentityFromContext(ctx).Identity(),
|
||||
}
|
||||
}
|
||||
|
||||
func (se *storeEncoder) WrapError(act string, res resource.Interface, err error) error {
|
||||
if ww, ok := res.(resource.ErrorWrapper); ok {
|
||||
err = ww.WrapError(act, err)
|
||||
} else {
|
||||
err = fmt.Errorf("%s %s %v: %s", act, res.ResourceType(), res.Identifiers().StringSlice(), err)
|
||||
}
|
||||
|
||||
return fmt.Errorf("store encoder %s", err)
|
||||
}
|
||||
|
||||
func resourceErrIdentifierNotUnique(i string) error {
|
||||
return fmt.Errorf("bad resource identifier %v: not unique", i)
|
||||
}
|
||||
30
server/pkg/envoy/store/rbac_rule.go
Normal file
30
server/pkg/envoy/store/rbac_rule.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/pkg/rbac"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
rbacRule struct {
|
||||
cfg *EncoderConfig
|
||||
|
||||
rule *rbac.Rule
|
||||
|
||||
// point to the rbac rule
|
||||
refRbacResource string
|
||||
refRbacRes *resource.Ref
|
||||
|
||||
refPathRes []*resource.Ref
|
||||
|
||||
refRole *resource.Ref
|
||||
role *types.Role
|
||||
}
|
||||
)
|
||||
|
||||
func rbacResourceErrUnidentifiable(ii resource.Identifiers) error {
|
||||
return fmt.Errorf("rbac resource unidentifiable %v", ii.StringSlice())
|
||||
}
|
||||
397
server/pkg/envoy/store/rbac_rule_marshal.go
Normal file
397
server/pkg/envoy/store/rbac_rule_marshal.go
Normal file
@@ -0,0 +1,397 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
automationTypes "github.com/cortezaproject/corteza/server/automation/types"
|
||||
composeTypes "github.com/cortezaproject/corteza/server/compose/types"
|
||||
federationTypes "github.com/cortezaproject/corteza/server/federation/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/pkg/rbac"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
systemTypes "github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
var (
|
||||
gRbacRules map[string]bool
|
||||
)
|
||||
|
||||
func newRbacRuleFromResource(res *resource.RbacRule, cfg *EncoderConfig) resourceState {
|
||||
return &rbacRule{
|
||||
cfg: mergeConfig(cfg, res.Config()),
|
||||
|
||||
rule: res.Res,
|
||||
|
||||
refRbacResource: res.RefResource,
|
||||
refRbacRes: res.RefRes,
|
||||
|
||||
refPathRes: res.RefPath,
|
||||
|
||||
refRole: res.RefRole,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *rbacRule) Prepare(ctx context.Context, pl *payload) (err error) {
|
||||
// Init global state
|
||||
if gRbacRules == nil {
|
||||
err = n.initGlobalIndex(ctx, pl.s)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Related role
|
||||
n.role, err = findRole(ctx, pl.s, pl.state.ParentResources, n.refRole.Identifiers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n.role == nil {
|
||||
return resource.RoleErrUnresolved(n.refRole.Identifiers)
|
||||
}
|
||||
|
||||
refRes := n.refRbacRes
|
||||
if refRes == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// For now we will only allow resource specific RBAC rules if that resource is
|
||||
// also present.
|
||||
// Here we check if we can find it in case we're handling a resource specific rule.
|
||||
found := false
|
||||
switch refRes.ResourceType {
|
||||
case composeTypes.RecordResourceType:
|
||||
if found = n.handleComposeRecord(pl.state.ParentResources); !found {
|
||||
return resource.RbacResourceErrNotFound(refRes.Identifiers)
|
||||
}
|
||||
|
||||
case composeTypes.ModuleFieldResourceType:
|
||||
if found = n.handleComposeModuleField(pl.state.ParentResources); !found {
|
||||
return resource.RbacResourceErrNotFound(refRes.Identifiers)
|
||||
}
|
||||
|
||||
default:
|
||||
for _, r := range pl.state.ParentResources {
|
||||
if refRes.ResourceType == r.ResourceType() && r.Identifiers().HasAny(refRes.Identifiers) {
|
||||
return
|
||||
}
|
||||
}
|
||||
// We couldn't find it...
|
||||
return resource.RbacResourceErrNotFound(refRes.Identifiers)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (n *rbacRule) Encode(ctx context.Context, pl *payload) (err error) {
|
||||
// Assure correct resource
|
||||
n.rule.Resource, err = n.makeRBACResource(pl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Assure correct role
|
||||
n.rule.RoleID = n.role.ID
|
||||
if n.rule.RoleID == 0 {
|
||||
rl := resource.FindRole(pl.state.ParentResources, n.refRole.Identifiers)
|
||||
n.rule.RoleID = rl.ID
|
||||
}
|
||||
if n.rule.RoleID == 0 {
|
||||
return resource.RoleErrUnresolved(n.refRole.Identifiers)
|
||||
}
|
||||
|
||||
// Upsert
|
||||
if _, exists := gRbacRules[n.rbacRuleIndex((n.rule))]; !exists {
|
||||
return store.CreateRbacRule(ctx, pl.s, n.rule)
|
||||
}
|
||||
|
||||
// On existing rbac rule, replace/merge right basically overwrites the existing rule;
|
||||
// otherwise, the new rule is ignored.
|
||||
switch n.cfg.OnExisting {
|
||||
case resource.Replace,
|
||||
resource.MergeRight:
|
||||
return store.UpdateRbacRule(ctx, pl.s, n.rule)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *rbacRule) rbacRuleIndex(r *rbac.Rule) string {
|
||||
return fmt.Sprintf("%d_%s_%s", r.RoleID, r.Resource, r.Operation)
|
||||
}
|
||||
|
||||
func (n *rbacRule) initGlobalIndex(ctx context.Context, s store.Storer) error {
|
||||
gRbacRules = make(map[string]bool)
|
||||
|
||||
rr, _, err := store.SearchRbacRules(ctx, s, rbac.RuleFilter{})
|
||||
if err != store.ErrNotFound && err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, r := range rr {
|
||||
gRbacRules[n.rbacRuleIndex(r)] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *rbacRule) makeRBACResource(pl *payload) (string, error) {
|
||||
p0ID := uint64(0)
|
||||
p1ID := uint64(0)
|
||||
p2ID := uint64(0)
|
||||
|
||||
rt := strings.Split(n.refRbacResource, "/")[0]
|
||||
|
||||
// Component level stuff
|
||||
switch rt {
|
||||
case composeTypes.ComponentResourceType:
|
||||
return composeTypes.ComponentRbacResource(), nil
|
||||
case systemTypes.ComponentResourceType:
|
||||
return systemTypes.ComponentRbacResource(), nil
|
||||
case automationTypes.ComponentResourceType:
|
||||
return automationTypes.ComponentRbacResource(), nil
|
||||
case federationTypes.ComponentResourceType:
|
||||
return federationTypes.ComponentRbacResource(), nil
|
||||
}
|
||||
|
||||
// Component resource stuff
|
||||
switch rt {
|
||||
case automationTypes.WorkflowResourceType:
|
||||
if n.refRbacRes != nil {
|
||||
p1 := resource.FindAutomationWorkflow(pl.state.ParentResources, n.refRbacRes.Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.AutomationWorkflowErrUnresolved(n.refRbacRes.Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
}
|
||||
return automationTypes.WorkflowRbacResource(p1ID), nil
|
||||
|
||||
case composeTypes.NamespaceResourceType:
|
||||
if n.refRbacRes != nil {
|
||||
p1 := resource.FindComposeNamespace(pl.state.ParentResources, n.refRbacRes.Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.ComposeNamespaceErrUnresolved(n.refRbacRes.Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
}
|
||||
return composeTypes.NamespaceRbacResource(p1ID), nil
|
||||
|
||||
case composeTypes.ModuleResourceType:
|
||||
if len(n.refPathRes) > 0 {
|
||||
p0 := resource.FindComposeNamespace(pl.state.ParentResources, n.refPathRes[0].Identifiers)
|
||||
if p0 == nil {
|
||||
return "", resource.ComposeNamespaceErrUnresolved(n.refPathRes[0].Identifiers)
|
||||
}
|
||||
p0ID = p0.ID
|
||||
}
|
||||
|
||||
if n.refRbacRes != nil {
|
||||
p1 := resource.FindComposeModule(pl.state.ParentResources, n.refRbacRes.Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.ComposeModuleErrUnresolved(n.refRbacRes.Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
}
|
||||
return composeTypes.ModuleRbacResource(p0ID, p1ID), nil
|
||||
|
||||
case composeTypes.ChartResourceType:
|
||||
if len(n.refPathRes) > 0 {
|
||||
p0 := resource.FindComposeNamespace(pl.state.ParentResources, n.refPathRes[0].Identifiers)
|
||||
if p0 == nil {
|
||||
return "", resource.ComposeNamespaceErrUnresolved(n.refPathRes[0].Identifiers)
|
||||
}
|
||||
p0ID = p0.ID
|
||||
}
|
||||
|
||||
if n.refRbacRes != nil {
|
||||
p1 := resource.FindComposeChart(pl.state.ParentResources, n.refRbacRes.Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.ComposeChartErrUnresolved(n.refRbacRes.Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
}
|
||||
return composeTypes.ChartRbacResource(p0ID, p1ID), nil
|
||||
|
||||
case composeTypes.PageResourceType:
|
||||
if len(n.refPathRes) > 0 {
|
||||
p0 := resource.FindComposeNamespace(pl.state.ParentResources, n.refPathRes[0].Identifiers)
|
||||
if p0 == nil {
|
||||
return "", resource.ComposeNamespaceErrUnresolved(n.refPathRes[0].Identifiers)
|
||||
}
|
||||
p0ID = p0.ID
|
||||
}
|
||||
|
||||
if n.refRbacRes != nil {
|
||||
p1 := resource.FindComposePage(pl.state.ParentResources, n.refRbacRes.Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.ComposePageErrUnresolved(n.refRbacRes.Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
}
|
||||
return composeTypes.PageRbacResource(p0ID, p1ID), nil
|
||||
|
||||
case composeTypes.RecordResourceType:
|
||||
if len(n.refPathRes) > 0 {
|
||||
p0 := resource.FindComposeNamespace(pl.state.ParentResources, n.refPathRes[0].Identifiers)
|
||||
if p0 == nil {
|
||||
return "", resource.ComposeNamespaceErrUnresolved(n.refPathRes[0].Identifiers)
|
||||
}
|
||||
p0ID = p0.ID
|
||||
}
|
||||
if len(n.refPathRes) > 1 {
|
||||
p1 := resource.FindComposeModule(pl.state.ParentResources, n.refPathRes[1].Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.ComposeModuleErrUnresolved(n.refPathRes[1].Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
}
|
||||
if n.refRbacRes != nil {
|
||||
ref := n.refPathRes[1]
|
||||
|
||||
p2 := resource.FindComposeRecordResource(pl.state.ParentResources, ref.Identifiers)
|
||||
if p2 == nil {
|
||||
return "", resource.ComposeRecordErrUnresolved(n.refRbacRes.Identifiers)
|
||||
}
|
||||
|
||||
for _, i := range n.refRbacRes.Identifiers {
|
||||
if p2ID = p2.IDMap[i]; p2ID > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if p2ID == 0 {
|
||||
return "", resource.ComposeRecordErrUnresolved(n.refRbacRes.Identifiers)
|
||||
}
|
||||
}
|
||||
return composeTypes.RecordRbacResource(p0ID, p1ID, p2ID), nil
|
||||
|
||||
case composeTypes.ModuleFieldResourceType:
|
||||
if len(n.refPathRes) > 0 {
|
||||
p0 := resource.FindComposeNamespace(pl.state.ParentResources, n.refPathRes[0].Identifiers)
|
||||
if p0 == nil {
|
||||
return "", resource.ComposeNamespaceErrUnresolved(n.refPathRes[0].Identifiers)
|
||||
}
|
||||
p0ID = p0.ID
|
||||
}
|
||||
if len(n.refPathRes) > 1 {
|
||||
p1 := resource.FindComposeModule(pl.state.ParentResources, n.refPathRes[1].Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.ComposeModuleErrUnresolved(n.refPathRes[1].Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
}
|
||||
|
||||
if n.refRbacRes != nil {
|
||||
modRef := n.refPathRes[1]
|
||||
p2 := resource.FindComposeModuleField(pl.state.ParentResources, modRef.Identifiers, n.refRbacRes.Identifiers)
|
||||
if p2 == nil {
|
||||
return "", resource.ComposeModuleFieldErrUnresolved(n.refRbacRes.Identifiers)
|
||||
}
|
||||
|
||||
p2ID = p2.ID
|
||||
}
|
||||
|
||||
return composeTypes.ModuleFieldRbacResource(p0ID, p1ID, p2ID), nil
|
||||
|
||||
case systemTypes.UserResourceType:
|
||||
if n.refRbacRes != nil {
|
||||
p1 := resource.FindUser(pl.state.ParentResources, n.refRbacRes.Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.UserErrUnresolved(n.refRbacRes.Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
}
|
||||
return systemTypes.UserRbacResource(p1ID), nil
|
||||
|
||||
case systemTypes.RoleResourceType:
|
||||
if n.refRbacRes != nil {
|
||||
p1 := resource.FindRole(pl.state.ParentResources, n.refRbacRes.Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.RoleErrUnresolved(n.refRbacRes.Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
}
|
||||
return systemTypes.RoleRbacResource(p1ID), nil
|
||||
|
||||
case systemTypes.ApplicationResourceType:
|
||||
if n.refRbacRes != nil {
|
||||
p1 := resource.FindApplication(pl.state.ParentResources, n.refRbacRes.Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.ApplicationErrUnresolved(n.refRbacRes.Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
}
|
||||
return systemTypes.ApplicationRbacResource(p1ID), nil
|
||||
|
||||
case systemTypes.ApigwRouteResourceType:
|
||||
if n.refRbacRes != nil {
|
||||
p1 := resource.FindAPIGateway(pl.state.ParentResources, n.refRbacRes.Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.APIGatewayErrUnresolved(n.refRbacRes.Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
}
|
||||
return systemTypes.ApigwRouteRbacResource(p1ID), nil
|
||||
|
||||
case systemTypes.AuthClientResourceType:
|
||||
// @todo add support for importing rbac rules for specific client
|
||||
return systemTypes.AuthClientRbacResource(p1ID), nil
|
||||
|
||||
case systemTypes.TemplateResourceType:
|
||||
if n.refRbacRes != nil {
|
||||
p1 := resource.FindTemplate(pl.state.ParentResources, n.refRbacRes.Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.TemplateErrUnresolved(n.refRbacRes.Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
}
|
||||
return systemTypes.TemplateRbacResource(p1ID), nil
|
||||
|
||||
case systemTypes.ReportResourceType:
|
||||
if n.refRbacRes != nil {
|
||||
p1 := resource.FindReport(pl.state.ParentResources, n.refRbacRes.Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.ReportErrUnresolved(n.refRbacRes.Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
}
|
||||
return systemTypes.ReportRbacResource(p1ID), nil
|
||||
|
||||
case systemTypes.QueueResourceType:
|
||||
// @todo add support for importing rbac rules for specific queue
|
||||
return systemTypes.QueueRbacResource(p1ID), nil
|
||||
|
||||
case systemTypes.DataPrivacyRequestResourceType:
|
||||
return systemTypes.DataPrivacyRequestRbacResource(p1ID), nil
|
||||
|
||||
case federationTypes.NodeResourceType:
|
||||
return federationTypes.NodeRbacResource(p1ID), nil
|
||||
case federationTypes.SharedModuleResourceType:
|
||||
return federationTypes.SharedModuleRbacResource(p0ID, p1ID), nil
|
||||
case federationTypes.ExposedModuleResourceType:
|
||||
return federationTypes.ExposedModuleRbacResource(p0ID, p1ID), nil
|
||||
}
|
||||
|
||||
// @todo if we wish to support rbac for external stuff, this needs to pass through.
|
||||
// this also requires some tweaks in the path ID thing.
|
||||
return "", fmt.Errorf("unsupported resource type '%s' for RBAC store encode", rt)
|
||||
}
|
||||
|
||||
func (n *rbacRule) handleComposeRecord(pp []resource.Interface) bool {
|
||||
for _, p := range pp {
|
||||
if p.ResourceType() == composeTypes.RecordResourceType && p.Identifiers().HasAny(n.refPathRes[1].Identifiers) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (n *rbacRule) handleComposeModuleField(pp []resource.Interface) bool {
|
||||
for _, p := range pp {
|
||||
if p.ResourceType() == composeTypes.ModuleResourceType && p.Identifiers().HasAny(n.refPathRes[1].Identifiers) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
32
server/pkg/envoy/store/rbac_rule_unmarshal.go
Normal file
32
server/pkg/envoy/store/rbac_rule_unmarshal.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/pkg/rbac"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
func newRbacRule(rl *rbac.Rule) (*rbacRule, error) {
|
||||
res := rl.Resource
|
||||
_, ref, pp, err := resource.ParseRule(res)
|
||||
|
||||
return &rbacRule{
|
||||
rule: rl,
|
||||
|
||||
refRbacResource: res,
|
||||
refRbacRes: ref,
|
||||
|
||||
refPathRes: pp,
|
||||
|
||||
refRole: resource.MakeRef(types.RoleResourceType, resource.MakeIdentifiers(strconv.FormatUint(rl.RoleID, 10))),
|
||||
}, err
|
||||
}
|
||||
|
||||
func (rl *rbacRule) MarshalEnvoy() ([]resource.Interface, error) {
|
||||
return envoy.CollectNodes(
|
||||
resource.NewRbacRule(rl.rule, rl.refRole, rl.refRbacRes, rl.refRbacResource, rl.refPathRes...),
|
||||
)
|
||||
}
|
||||
115
server/pkg/envoy/store/report.go
Normal file
115
server/pkg/envoy/store/report.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
report struct {
|
||||
cfg *EncoderConfig
|
||||
|
||||
res *resource.Report
|
||||
rp *types.Report
|
||||
ss types.ReportDataSourceSet
|
||||
pp types.ReportBlockSet
|
||||
|
||||
ux *userIndex
|
||||
}
|
||||
reportSet []*report
|
||||
|
||||
reportSource struct {
|
||||
cfg *EncoderConfig
|
||||
|
||||
res *resource.ReportSource
|
||||
tr *types.ReportDataSource
|
||||
}
|
||||
reportSourceSet []*reportSource
|
||||
)
|
||||
|
||||
// mergeReports merges b into a, prioritising a
|
||||
func mergeReports(a, b *types.Report) *types.Report {
|
||||
c := a
|
||||
|
||||
if c.Handle == "" {
|
||||
c.Handle = b.Handle
|
||||
}
|
||||
if c.Meta == nil {
|
||||
c.Meta = b.Meta
|
||||
}
|
||||
if c.Sources == nil {
|
||||
c.Sources = b.Sources
|
||||
}
|
||||
if c.Blocks == nil {
|
||||
c.Blocks = b.Blocks
|
||||
}
|
||||
|
||||
if c.OwnedBy == 0 {
|
||||
c.OwnedBy = b.OwnedBy
|
||||
}
|
||||
if c.CreatedBy == 0 {
|
||||
c.CreatedBy = b.CreatedBy
|
||||
}
|
||||
if c.UpdatedBy == 0 {
|
||||
c.UpdatedBy = b.UpdatedBy
|
||||
}
|
||||
if c.DeletedBy == 0 {
|
||||
c.DeletedBy = b.DeletedBy
|
||||
}
|
||||
|
||||
if c.CreatedAt.IsZero() {
|
||||
c.CreatedAt = b.CreatedAt
|
||||
}
|
||||
|
||||
if c.UpdatedAt == nil {
|
||||
c.UpdatedAt = b.UpdatedAt
|
||||
}
|
||||
|
||||
if c.DeletedAt == nil {
|
||||
c.DeletedAt = b.DeletedAt
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// findReport looks for the report in the resources & the store
|
||||
//
|
||||
// Provided resources are prioritized.
|
||||
func findReport(ctx context.Context, s store.Storer, rr resource.InterfaceSet, ii resource.Identifiers) (wf *types.Report, err error) {
|
||||
wf = resource.FindReport(rr, ii)
|
||||
if wf != nil {
|
||||
return wf, nil
|
||||
}
|
||||
|
||||
return findReportStore(ctx, s, makeGenericFilter(ii))
|
||||
}
|
||||
|
||||
// findReportStore looks for the report in the store
|
||||
func findReportStore(ctx context.Context, s store.Storer, gf genericFilter) (wf *types.Report, err error) {
|
||||
if gf.id > 0 {
|
||||
wf, err = store.LookupReportByID(ctx, s, gf.id)
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if wf != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, i := range gf.identifiers {
|
||||
wf, err = store.LookupReportByHandle(ctx, s, i)
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if wf != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
126
server/pkg/envoy/store/report_marshal.go
Normal file
126
server/pkg/envoy/store/report_marshal.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
func newReportFromResource(res *resource.Report, cfg *EncoderConfig) resourceState {
|
||||
return &report{
|
||||
cfg: mergeConfig(cfg, res.Config()),
|
||||
|
||||
res: res,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *report) Prepare(ctx context.Context, pl *payload) (err error) {
|
||||
// Reset old identifiers
|
||||
n.res.Res.ID = 0
|
||||
|
||||
// Try to get the original report
|
||||
n.rp, err = findReportStore(ctx, pl.s, makeGenericFilter(n.res.Identifiers()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if n.rp != nil {
|
||||
n.res.Res.ID = n.rp.ID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *report) Encode(ctx context.Context, pl *payload) (err error) {
|
||||
res := n.res.Res
|
||||
exists := n.rp != nil && n.rp.ID > 0
|
||||
|
||||
// Determine the ID
|
||||
if res.ID <= 0 && exists {
|
||||
res.ID = n.rp.ID
|
||||
}
|
||||
if res.ID <= 0 {
|
||||
res.ID = NextID()
|
||||
}
|
||||
|
||||
// Sys users
|
||||
us, err := resolveUserstamps(ctx, pl.s, pl.state.ParentResources, n.res.Userstamps())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ts := n.res.Timestamps()
|
||||
if ts != nil {
|
||||
if ts.CreatedAt != nil {
|
||||
res.CreatedAt = *ts.CreatedAt.T
|
||||
} else {
|
||||
res.CreatedAt = *now()
|
||||
}
|
||||
if ts.UpdatedAt != nil {
|
||||
res.UpdatedAt = ts.UpdatedAt.T
|
||||
}
|
||||
if ts.DeletedAt != nil {
|
||||
res.DeletedAt = ts.DeletedAt.T
|
||||
}
|
||||
}
|
||||
|
||||
res.CreatedBy = pl.invokerID
|
||||
if us != nil {
|
||||
if us.OwnedBy != nil {
|
||||
res.OwnedBy = us.OwnedBy.UserID
|
||||
}
|
||||
if us.CreatedBy != nil {
|
||||
res.CreatedBy = us.CreatedBy.UserID
|
||||
}
|
||||
if us.UpdatedBy != nil {
|
||||
res.UpdatedBy = us.UpdatedBy.UserID
|
||||
}
|
||||
if us.DeletedBy != nil {
|
||||
res.DeletedBy = us.DeletedBy.UserID
|
||||
}
|
||||
}
|
||||
|
||||
res.Sources = make(types.ReportDataSourceSet, 0, 10)
|
||||
for _, rp := range n.res.Sources {
|
||||
res.Sources = append(res.Sources, rp.Res)
|
||||
}
|
||||
|
||||
res.Blocks = make(types.ReportBlockSet, 0, 10)
|
||||
for _, rp := range n.res.Blocks {
|
||||
res.Blocks = append(res.Blocks, rp.Res)
|
||||
}
|
||||
|
||||
// Evaluate the resource skip expression
|
||||
// @todo expand available parameters; similar implementation to automation/types/record@Dict
|
||||
if skip, err := basicSkipEval(ctx, n.cfg, !exists); err != nil {
|
||||
return err
|
||||
} else if skip {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a fresh report
|
||||
if !exists {
|
||||
return store.CreateReport(ctx, pl.s, res)
|
||||
}
|
||||
|
||||
// Update existing report
|
||||
switch n.cfg.OnExisting {
|
||||
case resource.Skip:
|
||||
return nil
|
||||
|
||||
case resource.MergeLeft:
|
||||
res = mergeReports(n.rp, res)
|
||||
|
||||
case resource.MergeRight:
|
||||
res = mergeReports(res, n.rp)
|
||||
}
|
||||
|
||||
err = store.UpdateReport(ctx, pl.s, res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.res.Res = res
|
||||
return nil
|
||||
}
|
||||
34
server/pkg/envoy/store/report_unmarshal.go
Normal file
34
server/pkg/envoy/store/report_unmarshal.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
func newReport(wf *types.Report, ux *userIndex) *report {
|
||||
return &report{
|
||||
rp: wf,
|
||||
ss: wf.Sources,
|
||||
pp: wf.Blocks,
|
||||
|
||||
ux: ux,
|
||||
}
|
||||
}
|
||||
|
||||
func (awf *report) MarshalEnvoy() ([]resource.Interface, error) {
|
||||
rs := resource.NewReport(awf.rp)
|
||||
syncUserStamps(rs.Userstamps(), awf.ux)
|
||||
|
||||
for _, s := range awf.ss {
|
||||
rs.AddReportSource(s)
|
||||
}
|
||||
|
||||
for _, p := range awf.pp {
|
||||
rs.AddReportBlock(p)
|
||||
}
|
||||
|
||||
return envoy.CollectNodes(
|
||||
rs,
|
||||
)
|
||||
}
|
||||
26
server/pkg/envoy/store/resource_translation.go
Normal file
26
server/pkg/envoy/store/resource_translation.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
resourceTranslation struct {
|
||||
cfg *EncoderConfig
|
||||
|
||||
locales types.ResourceTranslationSet
|
||||
|
||||
// point to the resource translation
|
||||
refResourceTranslation string
|
||||
refLocaleRes *resource.Ref
|
||||
|
||||
refPathRes []*resource.Ref
|
||||
}
|
||||
)
|
||||
|
||||
func resourceTranslationErrUnidentifiable(ii resource.Identifiers) error {
|
||||
return fmt.Errorf("resource translation unidentifiable %v", ii.StringSlice())
|
||||
}
|
||||
250
server/pkg/envoy/store/resource_translation_marshal.go
Normal file
250
server/pkg/envoy/store/resource_translation_marshal.go
Normal file
@@ -0,0 +1,250 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
composeTypes "github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
var (
|
||||
gResourceTranslations map[language.Tag]map[string]*types.ResourceTranslation
|
||||
|
||||
resourceTranslationIndex = func(l *types.ResourceTranslation) string {
|
||||
return fmt.Sprintf("%s/%s", l.Resource, l.K)
|
||||
}
|
||||
)
|
||||
|
||||
func newResourceTranslationFromResource(res *resource.ResourceTranslation, cfg *EncoderConfig) resourceState {
|
||||
return &resourceTranslation{
|
||||
cfg: mergeConfig(cfg, res.Config()),
|
||||
|
||||
locales: res.Res,
|
||||
|
||||
refResourceTranslation: res.Res[0].Resource,
|
||||
refLocaleRes: res.RefRes,
|
||||
|
||||
refPathRes: res.RefPath,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *resourceTranslation) Prepare(ctx context.Context, pl *payload) (err error) {
|
||||
// Init global state
|
||||
if gResourceTranslations == nil {
|
||||
err = n.initGlobalIndex(ctx, pl.s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// For now we will only allow resource specific locale resources if that resource is
|
||||
// also present.
|
||||
// Here we check if we can find it in case we're handling a resource specific rule.
|
||||
refRes := n.refLocaleRes
|
||||
if refRes.ResourceType == composeTypes.ModuleFieldResourceType {
|
||||
for _, r := range pl.state.ParentResources {
|
||||
if r.ResourceType() == composeTypes.ModuleResourceType && r.Identifiers().HasAny(n.refPathRes[1].Identifiers) {
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, r := range pl.state.ParentResources {
|
||||
if refRes.ResourceType == r.ResourceType() && r.Identifiers().HasAny(refRes.Identifiers) {
|
||||
return
|
||||
}
|
||||
}
|
||||
// We couldn't find it...
|
||||
return resource.ResourceTranslationErrNotFound(refRes.Identifiers)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (n *resourceTranslation) Encode(ctx context.Context, pl *payload) (err error) {
|
||||
|
||||
// @todo move out of the encoding logic
|
||||
if n == nil {
|
||||
return
|
||||
}
|
||||
|
||||
localeRes, err := n.makeResourceTranslation(pl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Assure correct resources
|
||||
for _, l := range n.locales {
|
||||
l.Resource = localeRes
|
||||
}
|
||||
|
||||
ll := make(types.ResourceTranslationSet, 0, len(n.locales))
|
||||
for _, l := range n.locales {
|
||||
if l.Message == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
old, ok := gResourceTranslations[l.Lang.Tag][resourceTranslationIndex(l)]
|
||||
|
||||
// not exist
|
||||
if !ok {
|
||||
l.ID = NextID()
|
||||
l.CreatedAt = *now()
|
||||
l.CreatedBy = pl.invokerID
|
||||
|
||||
ll = append(ll, l)
|
||||
continue
|
||||
}
|
||||
|
||||
// exists
|
||||
switch n.cfg.OnExisting {
|
||||
case resource.Skip,
|
||||
resource.MergeLeft:
|
||||
continue
|
||||
|
||||
case resource.MergeRight,
|
||||
resource.Default:
|
||||
old.Message = l.Message
|
||||
|
||||
ll = append(ll, old)
|
||||
}
|
||||
}
|
||||
|
||||
// Update global index for following runs
|
||||
n.updateGlobalIndex(ll)
|
||||
|
||||
return store.UpsertResourceTranslation(ctx, pl.s, ll...)
|
||||
}
|
||||
|
||||
func (n *resourceTranslation) makeResourceTranslation(pl *payload) (string, error) {
|
||||
p0ID := uint64(0)
|
||||
p1ID := uint64(0)
|
||||
p2ID := uint64(0)
|
||||
|
||||
localeRes := ""
|
||||
_ = localeRes
|
||||
|
||||
switch n.refLocaleRes.ResourceType {
|
||||
// case automationTypes.WorkflowResourceType:
|
||||
// p1 := resource.FindAutomationWorkflow(pl.state.ParentResources, n.refLocaleRes.Identifiers)
|
||||
// if p1 == nil {
|
||||
// return "", resource.AutomationWorkflowErrUnresolved(n.refLocaleRes.Identifiers)
|
||||
// }
|
||||
// p1ID = p1.ID
|
||||
|
||||
// return automationTypes.WorkflowResourceTranslation(p1ID), nil
|
||||
|
||||
case composeTypes.NamespaceResourceType:
|
||||
p1 := resource.FindComposeNamespace(pl.state.ParentResources, n.refLocaleRes.Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.ComposeNamespaceErrUnresolved(n.refLocaleRes.Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
|
||||
return composeTypes.NamespaceResourceTranslation(p1ID), nil
|
||||
|
||||
case composeTypes.ModuleResourceType:
|
||||
p0 := resource.FindComposeNamespace(pl.state.ParentResources, n.refPathRes[0].Identifiers)
|
||||
if p0 == nil {
|
||||
return "", resource.ComposeNamespaceErrUnresolved(n.refPathRes[0].Identifiers)
|
||||
}
|
||||
p0ID = p0.ID
|
||||
|
||||
p1 := resource.FindComposeModule(pl.state.ParentResources, n.refLocaleRes.Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.ComposeModuleErrUnresolved(n.refLocaleRes.Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
|
||||
return composeTypes.ModuleResourceTranslation(p0ID, p1ID), nil
|
||||
|
||||
case composeTypes.PageResourceType:
|
||||
p0 := resource.FindComposeNamespace(pl.state.ParentResources, n.refPathRes[0].Identifiers)
|
||||
if p0 == nil {
|
||||
return "", resource.ComposeNamespaceErrUnresolved(n.refPathRes[0].Identifiers)
|
||||
}
|
||||
p0ID = p0.ID
|
||||
|
||||
p1 := resource.FindComposePage(pl.state.ParentResources, n.refLocaleRes.Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.ComposePageErrUnresolved(n.refLocaleRes.Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
|
||||
return composeTypes.PageResourceTranslation(p0ID, p1ID), nil
|
||||
|
||||
case composeTypes.ChartResourceType:
|
||||
p0 := resource.FindComposeNamespace(pl.state.ParentResources, n.refPathRes[0].Identifiers)
|
||||
if p0 == nil {
|
||||
return "", resource.ComposeNamespaceErrUnresolved(n.refPathRes[0].Identifiers)
|
||||
}
|
||||
p0ID = p0.ID
|
||||
|
||||
p1 := resource.FindComposeChart(pl.state.ParentResources, n.refLocaleRes.Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.ComposeChartErrUnresolved(n.refLocaleRes.Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
|
||||
return composeTypes.ChartResourceTranslation(p0ID, p1ID), nil
|
||||
|
||||
case composeTypes.ModuleFieldResourceType:
|
||||
p0 := resource.FindComposeNamespace(pl.state.ParentResources, n.refPathRes[0].Identifiers)
|
||||
if p0 == nil {
|
||||
return "", resource.ComposeNamespaceErrUnresolved(n.refPathRes[0].Identifiers)
|
||||
}
|
||||
p0ID = p0.ID
|
||||
|
||||
p1 := resource.FindComposeModule(pl.state.ParentResources, n.refPathRes[1].Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.ComposeModuleErrUnresolved(n.refPathRes[1].Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
|
||||
// field
|
||||
f := resource.FindComposeModuleField(pl.state.ParentResources, n.refPathRes[1].Identifiers, n.refLocaleRes.Identifiers)
|
||||
if f == nil {
|
||||
return "", resource.ComposeModuleFieldErrUnresolved(n.refLocaleRes.Identifiers)
|
||||
}
|
||||
p2ID = f.ID
|
||||
|
||||
return composeTypes.ModuleFieldResourceTranslation(p0ID, p1ID, p2ID), nil
|
||||
|
||||
// case types.ReportResourceType:
|
||||
// p1 := resource.FindReport(pl.state.ParentResources, n.refLocaleRes.Identifiers)
|
||||
// if p1 == nil {
|
||||
// return "", resource.ReportErrUnresolved(n.refLocaleRes.Identifiers)
|
||||
// }
|
||||
// p1ID = p1.ID
|
||||
|
||||
// return types.ReportResourceTranslation(p1ID), nil
|
||||
|
||||
default:
|
||||
// @todo if we wish to support res. trans. for external stuff, this needs to pass through.
|
||||
// this also requires some tweaks in the path ID thing.
|
||||
return "", fmt.Errorf("unsupported resource type '%s' for resource translation store encode", n.refLocaleRes.ResourceType)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *resourceTranslation) initGlobalIndex(ctx context.Context, s store.Storer) (err error) {
|
||||
gResourceTranslations = make(map[language.Tag]map[string]*types.ResourceTranslation)
|
||||
ll, _, err := store.SearchResourceTranslations(ctx, s, types.ResourceTranslationFilter{})
|
||||
if err != store.ErrNotFound && err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.updateGlobalIndex(ll)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *resourceTranslation) updateGlobalIndex(ll types.ResourceTranslationSet) {
|
||||
for _, l := range ll {
|
||||
if _, ok := gResourceTranslations[l.Lang.Tag]; !ok {
|
||||
gResourceTranslations[l.Lang.Tag] = make(map[string]*types.ResourceTranslation)
|
||||
}
|
||||
gResourceTranslations[l.Lang.Tag][resourceTranslationIndex(l)] = l
|
||||
}
|
||||
}
|
||||
27
server/pkg/envoy/store/resource_translation_unmarshal.go
Normal file
27
server/pkg/envoy/store/resource_translation_unmarshal.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
func newResourceTranslation(l types.ResourceTranslationSet) (*resourceTranslation, error) {
|
||||
res := l[0].Resource
|
||||
_, ref, pp, err := resource.ParseResourceTranslation(res)
|
||||
|
||||
return &resourceTranslation{
|
||||
locales: l,
|
||||
|
||||
refResourceTranslation: res,
|
||||
refLocaleRes: ref,
|
||||
|
||||
refPathRes: pp,
|
||||
}, err
|
||||
}
|
||||
|
||||
func (lr *resourceTranslation) MarshalEnvoy() ([]resource.Interface, error) {
|
||||
return envoy.CollectNodes(
|
||||
resource.NewResourceTranslation(lr.locales, lr.refResourceTranslation, lr.refLocaleRes, lr.refPathRes...),
|
||||
)
|
||||
}
|
||||
101
server/pkg/envoy/store/role.go
Normal file
101
server/pkg/envoy/store/role.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
role struct {
|
||||
cfg *EncoderConfig
|
||||
|
||||
res *resource.Role
|
||||
rl *types.Role
|
||||
}
|
||||
)
|
||||
|
||||
// mergeRoles merges b into a, prioritising a
|
||||
func mergeRoles(a, b *types.Role) *types.Role {
|
||||
c := *a
|
||||
|
||||
if c.Name == "" {
|
||||
c.Name = b.Name
|
||||
}
|
||||
if c.Handle == "" {
|
||||
c.Handle = b.Handle
|
||||
}
|
||||
|
||||
if c.CreatedAt.IsZero() {
|
||||
c.CreatedAt = b.CreatedAt
|
||||
}
|
||||
if c.UpdatedAt == nil {
|
||||
c.UpdatedAt = b.UpdatedAt
|
||||
}
|
||||
if c.ArchivedAt == nil {
|
||||
c.ArchivedAt = b.ArchivedAt
|
||||
}
|
||||
if c.DeletedAt == nil {
|
||||
c.DeletedAt = b.DeletedAt
|
||||
}
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
// findRole looks for the role in the resources & the store
|
||||
//
|
||||
// Provided resources are prioritized.
|
||||
func findRole(ctx context.Context, s store.Storer, rr resource.InterfaceSet, ii resource.Identifiers) (rl *types.Role, err error) {
|
||||
rl = resource.FindRole(rr, ii)
|
||||
if rl != nil {
|
||||
return rl, nil
|
||||
}
|
||||
|
||||
return findRoleStore(ctx, s, makeGenericFilter(ii))
|
||||
}
|
||||
|
||||
// findRoleStore looks for the role in the store
|
||||
func findRoleStore(ctx context.Context, s store.Storer, gf genericFilter) (rl *types.Role, err error) {
|
||||
if gf.id > 0 {
|
||||
rl, err = store.LookupRoleByID(ctx, s, gf.id)
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if rl != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, i := range gf.identifiers {
|
||||
rl, err = store.LookupRoleByHandle(ctx, s, i)
|
||||
if err == store.ErrNotFound {
|
||||
var rr types.RoleSet
|
||||
rr, _, err = store.SearchRoles(ctx, s, types.RoleFilter{
|
||||
Name: i,
|
||||
Paging: filter.Paging{
|
||||
Limit: 2,
|
||||
},
|
||||
})
|
||||
if len(rr) > 1 {
|
||||
return nil, resourceErrIdentifierNotUnique(i)
|
||||
}
|
||||
if len(rr) == 1 {
|
||||
rl = rr[0]
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if rl != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
95
server/pkg/envoy/store/role_marshal.go
Normal file
95
server/pkg/envoy/store/role_marshal.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
)
|
||||
|
||||
func NewRoleFromResource(res *resource.Role, cfg *EncoderConfig) resourceState {
|
||||
return &role{
|
||||
cfg: mergeConfig(cfg, res.Config()),
|
||||
|
||||
res: res,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *role) Prepare(ctx context.Context, pl *payload) (err error) {
|
||||
// Reset old identifiers
|
||||
n.res.Res.ID = 0
|
||||
|
||||
n.rl, err = findRoleStore(ctx, pl.s, makeGenericFilter(n.res.Identifiers()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if n.rl != nil {
|
||||
n.res.Res.ID = n.rl.ID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *role) Encode(ctx context.Context, pl *payload) (err error) {
|
||||
res := n.res.Res
|
||||
exists := n.rl != nil && n.rl.ID > 0
|
||||
|
||||
// Determine the ID
|
||||
if res.ID <= 0 && exists {
|
||||
res.ID = n.rl.ID
|
||||
}
|
||||
if res.ID <= 0 {
|
||||
res.ID = NextID()
|
||||
}
|
||||
|
||||
ts := n.res.Timestamps()
|
||||
if ts != nil {
|
||||
if ts.CreatedAt != nil {
|
||||
res.CreatedAt = *ts.CreatedAt.T
|
||||
} else {
|
||||
res.CreatedAt = *now()
|
||||
}
|
||||
if ts.UpdatedAt != nil {
|
||||
res.UpdatedAt = ts.UpdatedAt.T
|
||||
}
|
||||
if ts.ArchivedAt != nil {
|
||||
res.ArchivedAt = ts.ArchivedAt.T
|
||||
}
|
||||
if ts.DeletedAt != nil {
|
||||
res.DeletedAt = ts.DeletedAt.T
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate the resource skip expression
|
||||
// @todo expand available parameters; similar implementation to compose/types/record@Dict
|
||||
if skip, err := basicSkipEval(ctx, n.cfg, !exists); err != nil {
|
||||
return err
|
||||
} else if skip {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a fresh role
|
||||
if !exists {
|
||||
return store.CreateRole(ctx, pl.s, res)
|
||||
}
|
||||
|
||||
// Update existing roles
|
||||
switch n.cfg.OnExisting {
|
||||
case resource.Skip:
|
||||
return nil
|
||||
|
||||
case resource.MergeLeft:
|
||||
res = mergeRoles(n.rl, res)
|
||||
|
||||
case resource.MergeRight:
|
||||
res = mergeRoles(res, n.rl)
|
||||
}
|
||||
|
||||
err = store.UpdateRole(ctx, pl.s, res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.res.Res = res
|
||||
return nil
|
||||
}
|
||||
46
server/pkg/envoy/store/role_test.go
Normal file
46
server/pkg/envoy/store/role_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRole_Merger(t *testing.T) {
|
||||
req := require.New(t)
|
||||
|
||||
now := time.Time{}
|
||||
nowP := &time.Time{}
|
||||
|
||||
empty := &types.Role{}
|
||||
full := &types.Role{
|
||||
Name: "name",
|
||||
Handle: "handle",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: nowP,
|
||||
ArchivedAt: nowP,
|
||||
DeletedAt: nowP,
|
||||
}
|
||||
|
||||
t.Run("merge on empty", func(t *testing.T) {
|
||||
c := mergeRoles(empty, full)
|
||||
req.Equal("name", c.Name)
|
||||
req.Equal("handle", c.Handle)
|
||||
req.Equal(now, c.CreatedAt)
|
||||
req.Equal(nowP, c.UpdatedAt)
|
||||
req.Equal(nowP, c.ArchivedAt)
|
||||
req.Equal(nowP, c.DeletedAt)
|
||||
})
|
||||
|
||||
t.Run("merge with empty", func(t *testing.T) {
|
||||
c := mergeRoles(full, empty)
|
||||
req.Equal("name", c.Name)
|
||||
req.Equal("handle", c.Handle)
|
||||
req.Equal(now, c.CreatedAt)
|
||||
req.Equal(nowP, c.UpdatedAt)
|
||||
req.Equal(nowP, c.ArchivedAt)
|
||||
req.Equal(nowP, c.DeletedAt)
|
||||
})
|
||||
}
|
||||
19
server/pkg/envoy/store/role_unmarshal.go
Normal file
19
server/pkg/envoy/store/role_unmarshal.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
func newRole(rl *types.Role) *role {
|
||||
return &role{
|
||||
rl: rl,
|
||||
}
|
||||
}
|
||||
|
||||
func (rl *role) MarshalEnvoy() ([]resource.Interface, error) {
|
||||
return envoy.CollectNodes(
|
||||
resource.NewRole(rl.rl),
|
||||
)
|
||||
}
|
||||
17
server/pkg/envoy/store/setting.go
Normal file
17
server/pkg/envoy/store/setting.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
setting struct {
|
||||
cfg *EncoderConfig
|
||||
|
||||
res *resource.Setting
|
||||
st *types.SettingValue
|
||||
|
||||
ux *userIndex
|
||||
}
|
||||
)
|
||||
81
server/pkg/envoy/store/setting_marshal.go
Normal file
81
server/pkg/envoy/store/setting_marshal.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
var (
|
||||
gSettings map[string]bool
|
||||
)
|
||||
|
||||
func NewSettingFromResource(res *resource.Setting, cfg *EncoderConfig) resourceState {
|
||||
return &setting{
|
||||
cfg: mergeConfig(cfg, res.Config()),
|
||||
|
||||
res: res,
|
||||
st: &res.Res,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *setting) Prepare(ctx context.Context, pl *payload) (err error) {
|
||||
// Init global state
|
||||
if gSettings == nil {
|
||||
gSettings = make(map[string]bool)
|
||||
ss, _, err := store.SearchSettingValues(ctx, pl.s, types.SettingsFilter{})
|
||||
if err != store.ErrNotFound && err != nil {
|
||||
return err
|
||||
}
|
||||
for _, s := range ss {
|
||||
gSettings[s.Name] = true
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *setting) Encode(ctx context.Context, pl *payload) (err error) {
|
||||
res := n.st
|
||||
|
||||
us, err := resolveUserstamps(ctx, pl.s, pl.state.ParentResources, n.res.Userstamps())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ts := n.res.Timestamps()
|
||||
if ts != nil {
|
||||
if ts.UpdatedAt != nil {
|
||||
res.UpdatedAt = *ts.UpdatedAt.T
|
||||
} else {
|
||||
res.UpdatedAt = *now()
|
||||
}
|
||||
} else {
|
||||
res.UpdatedAt = *now()
|
||||
}
|
||||
|
||||
if us != nil {
|
||||
if us.OwnedBy != nil {
|
||||
res.OwnedBy = us.OwnedBy.UserID
|
||||
}
|
||||
if us.UpdatedBy != nil {
|
||||
res.UpdatedBy = us.UpdatedBy.UserID
|
||||
}
|
||||
}
|
||||
|
||||
if _, exists := gSettings[res.Name]; !exists {
|
||||
return store.CreateSettingValue(ctx, pl.s, res)
|
||||
}
|
||||
|
||||
// On existing setting, replace/merge right basically overwrites the existing value;
|
||||
// otherwise, the new value is ignored.
|
||||
switch n.cfg.OnExisting {
|
||||
case resource.Replace,
|
||||
resource.MergeRight:
|
||||
return store.UpdateSettingValue(ctx, pl.s, res)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
24
server/pkg/envoy/store/setting_unmarshal.go
Normal file
24
server/pkg/envoy/store/setting_unmarshal.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
func newSetting(res *types.SettingValue, ux *userIndex) *setting {
|
||||
return &setting{
|
||||
st: res,
|
||||
|
||||
ux: ux,
|
||||
}
|
||||
}
|
||||
|
||||
func (st *setting) MarshalEnvoy() ([]resource.Interface, error) {
|
||||
rs := resource.NewSetting(st.st)
|
||||
syncUserStamps(rs.Userstamps(), st.ux)
|
||||
|
||||
return envoy.CollectNodes(
|
||||
rs,
|
||||
)
|
||||
}
|
||||
764
server/pkg/envoy/store/system.go
Normal file
764
server/pkg/envoy/store/system.go
Normal file
@@ -0,0 +1,764 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
composeTypes "github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
pkgid "github.com/cortezaproject/corteza/server/pkg/id"
|
||||
"github.com/cortezaproject/corteza/server/pkg/rbac"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
type (
|
||||
roleFilter types.RoleFilter
|
||||
userFilter types.UserFilter
|
||||
templateFilter types.TemplateFilter
|
||||
applicationFilter types.ApplicationFilter
|
||||
apiGwRouteFilter types.ApigwRouteFilter
|
||||
reportFilter types.ReportFilter
|
||||
settingFilter types.SettingsFilter
|
||||
rbacFilter struct {
|
||||
rbac.RuleFilter
|
||||
// This will help us determine what rules for what resources we are able to export
|
||||
resourceID map[uint64]bool
|
||||
strict bool
|
||||
}
|
||||
resourceTranslationFilter types.ResourceTranslationFilter
|
||||
|
||||
systemDecoder struct {
|
||||
resourceID []uint64
|
||||
ux *userIndex
|
||||
}
|
||||
)
|
||||
|
||||
func newSystemDecoder(ux *userIndex) *systemDecoder {
|
||||
return &systemDecoder{
|
||||
resourceID: make([]uint64, 0, 200),
|
||||
ux: ux,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *systemDecoder) decodeRoles(ctx context.Context, s store.Storer, ff []*roleFilter) *auxRsp {
|
||||
mm := make([]envoy.Marshaller, 0, 100)
|
||||
if ff == nil {
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
var nn types.RoleSet
|
||||
var fn types.RoleFilter
|
||||
var err error
|
||||
|
||||
for _, f := range ff {
|
||||
aux := *f
|
||||
|
||||
if aux.Limit == 0 {
|
||||
aux.Limit = 1000
|
||||
}
|
||||
|
||||
for {
|
||||
nn, fn, err = s.SearchRoles(ctx, types.RoleFilter(aux))
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
for _, n := range nn {
|
||||
mm = append(mm, newRole(n))
|
||||
d.resourceID = append(d.resourceID, n.ID)
|
||||
}
|
||||
|
||||
if fn.NextPage != nil {
|
||||
aux.PageCursor = fn.NextPage
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *systemDecoder) decodeUsers(ctx context.Context, s store.Storer, ff []*userFilter) *auxRsp {
|
||||
mm := make([]envoy.Marshaller, 0, 100)
|
||||
if ff == nil {
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
var nn types.UserSet
|
||||
var fn types.UserFilter
|
||||
var err error
|
||||
|
||||
for _, f := range ff {
|
||||
aux := *f
|
||||
|
||||
if aux.Limit == 0 {
|
||||
aux.Limit = 1000
|
||||
}
|
||||
|
||||
for {
|
||||
nn, fn, err = s.SearchUsers(ctx, types.UserFilter(aux))
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
for _, n := range nn {
|
||||
mm = append(mm, newUser(n))
|
||||
d.resourceID = append(d.resourceID, n.ID)
|
||||
}
|
||||
|
||||
if fn.NextPage != nil {
|
||||
aux.PageCursor = fn.NextPage
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *systemDecoder) decodeTemplates(ctx context.Context, s store.Storer, ff []*templateFilter) *auxRsp {
|
||||
mm := make([]envoy.Marshaller, 0, 100)
|
||||
if ff == nil {
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
var nn types.TemplateSet
|
||||
var fn types.TemplateFilter
|
||||
var err error
|
||||
|
||||
for _, f := range ff {
|
||||
aux := *f
|
||||
|
||||
if aux.Limit == 0 {
|
||||
aux.Limit = 1000
|
||||
}
|
||||
|
||||
for {
|
||||
nn, fn, err = s.SearchTemplates(ctx, types.TemplateFilter(aux))
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
for _, n := range nn {
|
||||
mm = append(mm, newTemplate(n))
|
||||
d.resourceID = append(d.resourceID, n.ID)
|
||||
}
|
||||
|
||||
if fn.NextPage != nil {
|
||||
aux.PageCursor = fn.NextPage
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *systemDecoder) decodeAPIGWRoutes(ctx context.Context, s store.Storer, ff []*apiGwRouteFilter) *auxRsp {
|
||||
mm := make([]envoy.Marshaller, 0, 100)
|
||||
if ff == nil {
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
var nn types.ApigwRouteSet
|
||||
var fn types.ApigwRouteFilter
|
||||
var err error
|
||||
|
||||
for _, f := range ff {
|
||||
aux := *f
|
||||
|
||||
if aux.Limit == 0 {
|
||||
aux.Limit = 1000
|
||||
}
|
||||
|
||||
for {
|
||||
nn, fn, err = s.SearchApigwRoutes(ctx, types.ApigwRouteFilter(aux))
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// filters
|
||||
for _, n := range nn {
|
||||
gwf, _, err := s.SearchApigwFilters(ctx, types.ApigwFilterFilter{RouteID: n.ID})
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
mm = append(mm, newAPIGateway(n, gwf, d.ux))
|
||||
d.resourceID = append(d.resourceID, n.ID)
|
||||
}
|
||||
|
||||
if fn.NextPage != nil {
|
||||
aux.PageCursor = fn.NextPage
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *systemDecoder) decodeReports(ctx context.Context, s store.Storer, ff []*reportFilter) *auxRsp {
|
||||
mm := make([]envoy.Marshaller, 0, 100)
|
||||
if ff == nil {
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
var nn types.ReportSet
|
||||
var fn types.ReportFilter
|
||||
var err error
|
||||
|
||||
for _, f := range ff {
|
||||
aux := *f
|
||||
|
||||
if aux.Limit == 0 {
|
||||
aux.Limit = 1000
|
||||
}
|
||||
|
||||
for {
|
||||
nn, fn, err = s.SearchReports(ctx, types.ReportFilter(aux))
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
for _, n := range nn {
|
||||
mm = append(mm, newReport(n, d.ux))
|
||||
d.resourceID = append(d.resourceID, n.ID)
|
||||
}
|
||||
|
||||
if fn.NextPage != nil {
|
||||
aux.PageCursor = fn.NextPage
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *systemDecoder) decodeApplications(ctx context.Context, s store.Storer, ff []*applicationFilter) *auxRsp {
|
||||
mm := make([]envoy.Marshaller, 0, 100)
|
||||
if ff == nil {
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
var nn types.ApplicationSet
|
||||
var fn types.ApplicationFilter
|
||||
var err error
|
||||
|
||||
for _, f := range ff {
|
||||
aux := *f
|
||||
|
||||
if aux.Limit == 0 {
|
||||
aux.Limit = 1000
|
||||
}
|
||||
|
||||
for {
|
||||
nn, fn, err = s.SearchApplications(ctx, types.ApplicationFilter(aux))
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
for _, n := range nn {
|
||||
// Index users
|
||||
err = d.ux.add(
|
||||
ctx,
|
||||
n.OwnerID,
|
||||
)
|
||||
|
||||
mm = append(mm, newApplication(n, d.ux))
|
||||
d.resourceID = append(d.resourceID, n.ID)
|
||||
}
|
||||
|
||||
if fn.NextPage != nil {
|
||||
aux.PageCursor = fn.NextPage
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
func (d *systemDecoder) decodeSettings(ctx context.Context, s store.Storer, ff []*settingFilter) *auxRsp {
|
||||
mm := make([]envoy.Marshaller, 0, 100)
|
||||
if ff == nil {
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
var nn types.SettingValueSet
|
||||
var err error
|
||||
|
||||
for _, f := range ff {
|
||||
aux := *f
|
||||
|
||||
for {
|
||||
nn, _, err = store.SearchSettingValues(ctx, s, types.SettingsFilter(aux))
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
for _, n := range nn {
|
||||
// Index users
|
||||
err = d.ux.add(
|
||||
ctx,
|
||||
n.OwnedBy,
|
||||
n.UpdatedBy,
|
||||
)
|
||||
|
||||
mm = append(mm, newSetting(n, d.ux))
|
||||
}
|
||||
// mm = append(mm, NewSettings(nn))
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *systemDecoder) decodeRbac(ctx context.Context, s store.Storer, ff []*rbacFilter) *auxRsp {
|
||||
mm := make([]envoy.Marshaller, 0, 100)
|
||||
if ff == nil {
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
var nn rbac.RuleSet
|
||||
var err error
|
||||
|
||||
c := func(r *resource.Ref, f *rbacFilter, path ...*resource.Ref) (bool, error) {
|
||||
if r == nil {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// the first identifier is the most specific .. the ID
|
||||
id, err := cast.ToUint64E(r.Identifiers.First())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if r.ResourceType == composeTypes.ModuleFieldResourceType {
|
||||
id, err = cast.ToUint64E(path[1].Identifiers.First())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
return f.resourceID[id], nil
|
||||
}
|
||||
|
||||
for _, f := range ff {
|
||||
aux := *f
|
||||
|
||||
for {
|
||||
nn, _, err = s.SearchRbacRules(ctx, rbac.RuleFilter(aux.RuleFilter))
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
for _, n := range nn {
|
||||
r, err := newRbacRule(n)
|
||||
rt := strings.Split(r.refRbacResource, "/")[0]
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// somesort of a generic rule; no specifc resource
|
||||
// @todo check for pp inclusion!!
|
||||
if r.refRbacRes == nil && len(r.refPathRes) == 0 {
|
||||
mm = append(mm, r)
|
||||
} else {
|
||||
// Check the resource ref and the path refs for validity.
|
||||
// ComposeRecords are fetched in chunks so this check is not valid here.
|
||||
if rt != composeTypes.RecordResourceType {
|
||||
if ok, err := c(r.refRbacRes, f, r.refPathRes...); err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
} else if !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
for _, p := range r.refPathRes {
|
||||
if ok, err := c(p, f); err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
} else if !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
mm = append(mm, r)
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *systemDecoder) decodeResourceTranslation(ctx context.Context, s store.Storer, ff []*resourceTranslationFilter) *auxRsp {
|
||||
mm := make([]envoy.Marshaller, 0, 100)
|
||||
if ff == nil {
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
resMap = make(map[string]*resourceTranslation)
|
||||
|
||||
nn types.ResourceTranslationSet
|
||||
fn types.ResourceTranslationFilter
|
||||
err error
|
||||
)
|
||||
|
||||
for _, f := range ff {
|
||||
aux := *f
|
||||
|
||||
if aux.Limit == 0 {
|
||||
aux.Limit = 1000
|
||||
}
|
||||
|
||||
for {
|
||||
nn, fn, err = s.SearchResourceTranslations(ctx, types.ResourceTranslationFilter(aux))
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
for _, n := range nn {
|
||||
if _, ok := resMap[n.Resource]; !ok {
|
||||
resMap[n.Resource], err = newResourceTranslation(types.ResourceTranslationSet{n})
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// parse the resource wo we can define relations/check if we can unmarshal it
|
||||
_, ref, pp, err := resource.ParseResourceTranslation(n.Resource)
|
||||
resMap[n.Resource].refLocaleRes = ref
|
||||
resMap[n.Resource].refPathRes = pp
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
resMap[n.Resource].locales = append(resMap[n.Resource].locales, n)
|
||||
}
|
||||
|
||||
if fn.NextPage != nil {
|
||||
aux.PageCursor = fn.NextPage
|
||||
} else {
|
||||
break
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for _, lr := range resMap {
|
||||
mm = append(mm, lr)
|
||||
}
|
||||
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
func (df *DecodeFilter) systemFromResource(rr ...string) *DecodeFilter {
|
||||
for _, r := range rr {
|
||||
if !strings.HasPrefix(r, "system") {
|
||||
continue
|
||||
}
|
||||
|
||||
id := ""
|
||||
if strings.Count(r, ":") == 2 && !strings.HasSuffix(r, "*") {
|
||||
// There is an identifier
|
||||
aux := strings.Split(r, ":")
|
||||
|
||||
id = aux[len(aux)-1]
|
||||
r = strings.Join(aux[:len(aux)-1], ":")
|
||||
}
|
||||
|
||||
switch strings.ToLower(r) {
|
||||
case "system:role":
|
||||
df = df.Roles(&types.RoleFilter{
|
||||
Query: id,
|
||||
})
|
||||
case "system:user":
|
||||
df = df.Users(&types.UserFilter{
|
||||
Query: id,
|
||||
AllKinds: true,
|
||||
})
|
||||
case "system:template":
|
||||
df = df.Templates(&types.TemplateFilter{
|
||||
Handle: id,
|
||||
})
|
||||
templateID, err := cast.ToUint64E(id)
|
||||
if err == nil && templateID > 0 {
|
||||
df = df.Templates(&types.TemplateFilter{
|
||||
TemplateID: pkgid.Strings(templateID),
|
||||
})
|
||||
}
|
||||
case "system:apigw-route":
|
||||
df = df.APIGWRoutes(&types.ApigwRouteFilter{
|
||||
Route: id,
|
||||
})
|
||||
case "system:report":
|
||||
df = df.Reports(&types.ReportFilter{
|
||||
Handle: id,
|
||||
})
|
||||
reportID, err := cast.ToUint64E(id)
|
||||
if err == nil && reportID > 0 {
|
||||
df = df.Reports(&types.ReportFilter{
|
||||
ReportID: pkgid.Strings(reportID),
|
||||
})
|
||||
}
|
||||
|
||||
case "system:application":
|
||||
df = df.Applications(&types.ApplicationFilter{
|
||||
Query: id,
|
||||
})
|
||||
case "system:setting":
|
||||
df = df.Settings(&types.SettingsFilter{})
|
||||
case "system:rbac":
|
||||
df = df.Rbac(&rbac.RuleFilter{})
|
||||
case "system:resource-translation":
|
||||
df = df.Rbac(&rbac.RuleFilter{})
|
||||
}
|
||||
}
|
||||
|
||||
return df
|
||||
}
|
||||
|
||||
func (df *DecodeFilter) systemFromRef(rr ...*resource.Ref) *DecodeFilter {
|
||||
for _, r := range rr {
|
||||
if strings.Index(r.ResourceType, "system") < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
switch r.ResourceType {
|
||||
case types.RoleResourceType:
|
||||
for _, i := range r.Identifiers.StringSlice() {
|
||||
df = df.Roles(&types.RoleFilter{
|
||||
Query: i,
|
||||
})
|
||||
}
|
||||
case types.UserResourceType:
|
||||
for _, i := range r.Identifiers.StringSlice() {
|
||||
df = df.Users(&types.UserFilter{
|
||||
Query: i,
|
||||
AllKinds: true,
|
||||
})
|
||||
}
|
||||
case types.TemplateResourceType:
|
||||
for _, i := range r.Identifiers.StringSlice() {
|
||||
df = df.Templates(&types.TemplateFilter{
|
||||
Handle: i,
|
||||
})
|
||||
templateID, err := cast.ToUint64E(i)
|
||||
if err == nil && templateID > 0 {
|
||||
df = df.Templates(&types.TemplateFilter{
|
||||
TemplateID: pkgid.Strings(templateID),
|
||||
})
|
||||
}
|
||||
}
|
||||
case types.ApplicationResourceType:
|
||||
for _, i := range r.Identifiers.StringSlice() {
|
||||
df = df.Applications(&types.ApplicationFilter{
|
||||
Query: i,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return df
|
||||
}
|
||||
|
||||
// Roles adds a new RoleFilter
|
||||
func (df *DecodeFilter) Roles(f *types.RoleFilter) *DecodeFilter {
|
||||
if df.roles == nil {
|
||||
df.roles = make([]*roleFilter, 0, 1)
|
||||
}
|
||||
df.roles = append(df.roles, (*roleFilter)(f))
|
||||
return df
|
||||
}
|
||||
|
||||
// Users adds a new UserFilter
|
||||
func (df *DecodeFilter) Users(f *types.UserFilter) *DecodeFilter {
|
||||
if df.users == nil {
|
||||
df.users = make([]*userFilter, 0, 1)
|
||||
}
|
||||
df.users = append(df.users, (*userFilter)(f))
|
||||
return df
|
||||
}
|
||||
|
||||
// Templates adds a new TemplateFilter
|
||||
func (df *DecodeFilter) Templates(f *types.TemplateFilter) *DecodeFilter {
|
||||
if df.templates == nil {
|
||||
df.templates = make([]*templateFilter, 0, 1)
|
||||
}
|
||||
df.templates = append(df.templates, (*templateFilter)(f))
|
||||
return df
|
||||
}
|
||||
|
||||
func (df *DecodeFilter) APIGWRoutes(f *types.ApigwRouteFilter) *DecodeFilter {
|
||||
if df.apiGwRoutes == nil {
|
||||
df.apiGwRoutes = make([]*apiGwRouteFilter, 0, 1)
|
||||
}
|
||||
df.apiGwRoutes = append(df.apiGwRoutes, (*apiGwRouteFilter)(f))
|
||||
return df
|
||||
}
|
||||
|
||||
func (df *DecodeFilter) Reports(f *types.ReportFilter) *DecodeFilter {
|
||||
if df.reports == nil {
|
||||
df.reports = make([]*reportFilter, 0, 1)
|
||||
}
|
||||
df.reports = append(df.reports, (*reportFilter)(f))
|
||||
return df
|
||||
}
|
||||
|
||||
// Applications adds a new ApplicationFilter
|
||||
func (df *DecodeFilter) Applications(f *types.ApplicationFilter) *DecodeFilter {
|
||||
if df.applications == nil {
|
||||
df.applications = make([]*applicationFilter, 0, 1)
|
||||
}
|
||||
df.applications = append(df.applications, (*applicationFilter)(f))
|
||||
return df
|
||||
}
|
||||
|
||||
// Settings adds a new SettingsFilter
|
||||
func (df *DecodeFilter) Settings(f *types.SettingsFilter) *DecodeFilter {
|
||||
if df.settings == nil {
|
||||
df.settings = make([]*settingFilter, 0, 1)
|
||||
}
|
||||
df.settings = append(df.settings, (*settingFilter)(f))
|
||||
return df
|
||||
}
|
||||
|
||||
// Rbac adds a new RuleFilter
|
||||
func (df *DecodeFilter) Rbac(f *rbac.RuleFilter) *DecodeFilter {
|
||||
if df.rbac == nil {
|
||||
df.rbac = make([]*rbacFilter, 0, 1)
|
||||
} else {
|
||||
// There can only be a single rbac filter
|
||||
// since it makes no sense to have multiple of
|
||||
return df
|
||||
}
|
||||
|
||||
df.rbac = append(df.rbac, &rbacFilter{RuleFilter: *f})
|
||||
return df
|
||||
}
|
||||
|
||||
// ResourceTranslation adds a new ResourceTranslationFilter
|
||||
func (df *DecodeFilter) ResourceTranslation(f *types.ResourceTranslationFilter) *DecodeFilter {
|
||||
if df.resourceTranslations == nil {
|
||||
df.resourceTranslations = make([]*resourceTranslationFilter, 0, 1)
|
||||
} else {
|
||||
// There can only be a single resourceTranslations filter
|
||||
// since it makes no sense to have multiple of
|
||||
return df
|
||||
}
|
||||
|
||||
df.resourceTranslations = append(df.resourceTranslations, (*resourceTranslationFilter)(f))
|
||||
return df
|
||||
}
|
||||
|
||||
func (df *DecodeFilter) RbacStrict(f *rbac.RuleFilter) *DecodeFilter {
|
||||
if df.rbac == nil {
|
||||
df.rbac = make([]*rbacFilter, 0, 1)
|
||||
} else {
|
||||
// There can only be a single rbac filter
|
||||
// since it makes no sense to have multiple of
|
||||
return df
|
||||
}
|
||||
|
||||
df.rbac = append(df.rbac, &rbacFilter{RuleFilter: *f})
|
||||
return df
|
||||
}
|
||||
|
||||
// allowRbacResource adds a new resource identifier to supported resource rules
|
||||
func (df *DecodeFilter) allowRbacResource(id ...uint64) {
|
||||
if df.rbac == nil || len(df.rbac) == 0 {
|
||||
return
|
||||
}
|
||||
rf := df.rbac[0]
|
||||
|
||||
if rf.resourceID == nil {
|
||||
rf.resourceID = make(map[uint64]bool)
|
||||
}
|
||||
for _, i := range id {
|
||||
rf.resourceID[i] = true
|
||||
}
|
||||
}
|
||||
98
server/pkg/envoy/store/template.go
Normal file
98
server/pkg/envoy/store/template.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
template struct {
|
||||
cfg *EncoderConfig
|
||||
|
||||
res *resource.Template
|
||||
t *types.Template
|
||||
}
|
||||
)
|
||||
|
||||
// mergeTemplates merges b into a, prioritising a
|
||||
func mergeTemplates(a, b *types.Template) *types.Template {
|
||||
c := *a
|
||||
|
||||
if c.Handle == "" {
|
||||
c.Handle = b.Handle
|
||||
}
|
||||
if c.Language == "" {
|
||||
c.Language = b.Language
|
||||
}
|
||||
if c.Type == "" {
|
||||
c.Type = b.Type
|
||||
}
|
||||
if c.Meta.Short == "" {
|
||||
c.Meta.Short = b.Meta.Short
|
||||
}
|
||||
if c.Meta.Description == "" {
|
||||
c.Meta.Description = b.Meta.Description
|
||||
}
|
||||
if c.Template == "" {
|
||||
c.Template = b.Template
|
||||
}
|
||||
|
||||
if c.OwnerID == 0 {
|
||||
c.OwnerID = b.OwnerID
|
||||
}
|
||||
|
||||
if c.CreatedAt.IsZero() {
|
||||
c.CreatedAt = b.CreatedAt
|
||||
}
|
||||
if c.UpdatedAt == nil {
|
||||
c.UpdatedAt = b.UpdatedAt
|
||||
}
|
||||
if c.DeletedAt == nil {
|
||||
c.DeletedAt = b.DeletedAt
|
||||
}
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
// findTemplate looks for the template in the resources & the store
|
||||
//
|
||||
// Provided resources are prioritized.
|
||||
func findTemplate(ctx context.Context, s store.Storer, rr resource.InterfaceSet, ii resource.Identifiers) (u *types.Template, err error) {
|
||||
u = resource.FindTemplate(rr, ii)
|
||||
if u != nil {
|
||||
return u, nil
|
||||
}
|
||||
|
||||
return findTemplateStore(ctx, s, makeGenericFilter(ii))
|
||||
}
|
||||
|
||||
// findTemplateStore looks for the template in the store
|
||||
func findTemplateStore(ctx context.Context, s store.Storer, gf genericFilter) (t *types.Template, err error) {
|
||||
if gf.id > 0 {
|
||||
t, err = store.LookupTemplateByID(ctx, s, gf.id)
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if t != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, i := range gf.identifiers {
|
||||
// Handle
|
||||
t, err = store.LookupTemplateByHandle(ctx, s, i)
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if t != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
104
server/pkg/envoy/store/template_marshal.go
Normal file
104
server/pkg/envoy/store/template_marshal.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
)
|
||||
|
||||
func NewTemplateFromResource(res *resource.Template, cfg *EncoderConfig) resourceState {
|
||||
return &template{
|
||||
cfg: mergeConfig(cfg, res.Config()),
|
||||
|
||||
res: res,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *template) Prepare(ctx context.Context, pl *payload) (err error) {
|
||||
// Reset old identifiers
|
||||
n.res.Res.ID = 0
|
||||
|
||||
// Try to get the original template
|
||||
n.t, err = findTemplateStore(ctx, pl.s, makeGenericFilter(n.res.Identifiers()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if n.t != nil {
|
||||
n.res.Res.ID = n.t.ID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *template) Encode(ctx context.Context, pl *payload) (err error) {
|
||||
res := n.res.Res
|
||||
exists := n.t != nil && n.t.ID > 0
|
||||
|
||||
// Determine the ID
|
||||
if res.ID <= 0 && exists {
|
||||
res.ID = n.t.ID
|
||||
}
|
||||
if res.ID <= 0 {
|
||||
res.ID = NextID()
|
||||
}
|
||||
|
||||
us, err := resolveUserstamps(ctx, pl.s, pl.state.ParentResources, n.res.Userstamps())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ts := n.res.Timestamps()
|
||||
if ts != nil {
|
||||
if ts.CreatedAt != nil {
|
||||
res.CreatedAt = *ts.CreatedAt.T
|
||||
} else {
|
||||
res.CreatedAt = *now()
|
||||
}
|
||||
if ts.UpdatedAt != nil {
|
||||
res.UpdatedAt = ts.UpdatedAt.T
|
||||
}
|
||||
if ts.DeletedAt != nil {
|
||||
res.DeletedAt = ts.DeletedAt.T
|
||||
}
|
||||
}
|
||||
|
||||
if us != nil {
|
||||
if us.OwnedBy != nil {
|
||||
res.OwnerID = us.OwnedBy.UserID
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate the resource skip expression
|
||||
// @todo expand available parameters; similar implementation to compose/types/record@Dict
|
||||
if skip, err := basicSkipEval(ctx, n.cfg, !exists); err != nil {
|
||||
return err
|
||||
} else if skip {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a fresh template
|
||||
if !exists {
|
||||
return store.CreateTemplate(ctx, pl.s, res)
|
||||
}
|
||||
|
||||
// Update existing template
|
||||
switch n.cfg.OnExisting {
|
||||
case resource.Skip:
|
||||
return nil
|
||||
|
||||
case resource.MergeLeft:
|
||||
res = mergeTemplates(n.t, res)
|
||||
|
||||
case resource.MergeRight:
|
||||
res = mergeTemplates(res, n.t)
|
||||
}
|
||||
|
||||
err = store.UpdateTemplate(ctx, pl.s, res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.res.Res = res
|
||||
return nil
|
||||
}
|
||||
19
server/pkg/envoy/store/template_unmarshal.go
Normal file
19
server/pkg/envoy/store/template_unmarshal.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
func newTemplate(t *types.Template) *template {
|
||||
return &template{
|
||||
t: t,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *template) MarshalEnvoy() ([]resource.Interface, error) {
|
||||
return envoy.CollectNodes(
|
||||
resource.NewTemplate(u.t),
|
||||
)
|
||||
}
|
||||
111
server/pkg/envoy/store/user.go
Normal file
111
server/pkg/envoy/store/user.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/mail"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
user struct {
|
||||
cfg *EncoderConfig
|
||||
|
||||
res *resource.User
|
||||
u *types.User
|
||||
}
|
||||
)
|
||||
|
||||
// mergeUsers merges b into a, prioritising a
|
||||
func mergeUsers(a, b *types.User) *types.User {
|
||||
c := *a
|
||||
|
||||
if c.Username == "" {
|
||||
c.Username = b.Username
|
||||
}
|
||||
if c.Email == "" {
|
||||
c.Email = b.Email
|
||||
}
|
||||
if c.Name == "" {
|
||||
c.Name = b.Name
|
||||
}
|
||||
if c.Handle == "" {
|
||||
c.Handle = b.Handle
|
||||
}
|
||||
if c.Kind == "" {
|
||||
c.Kind = b.Kind
|
||||
}
|
||||
if c.Meta == nil {
|
||||
c.Meta = b.Meta
|
||||
}
|
||||
|
||||
if c.CreatedAt.IsZero() {
|
||||
c.CreatedAt = b.CreatedAt
|
||||
}
|
||||
if c.UpdatedAt == nil {
|
||||
c.UpdatedAt = b.UpdatedAt
|
||||
}
|
||||
if c.SuspendedAt == nil {
|
||||
c.SuspendedAt = b.SuspendedAt
|
||||
}
|
||||
if c.DeletedAt == nil {
|
||||
c.DeletedAt = b.DeletedAt
|
||||
}
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
// findUser looks for the user in the resources & the store
|
||||
//
|
||||
// Provided resources are prioritized.
|
||||
func findUser(ctx context.Context, s store.Storer, rr resource.InterfaceSet, ii resource.Identifiers) (u *types.User, err error) {
|
||||
u = resource.FindUser(rr, ii)
|
||||
if u != nil {
|
||||
return u, nil
|
||||
}
|
||||
|
||||
return findUserStore(ctx, s, makeGenericFilter(ii))
|
||||
}
|
||||
|
||||
// findUserStore looks for the user in the store
|
||||
func findUserStore(ctx context.Context, s store.Storer, gf genericFilter) (u *types.User, err error) {
|
||||
if gf.id > 0 {
|
||||
u, err = store.LookupUserByID(ctx, s, gf.id)
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if u != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, i := range gf.identifiers {
|
||||
// email
|
||||
if _, err = mail.ParseAddress(i); err == nil {
|
||||
u, err = store.LookupUserByEmail(ctx, s, i)
|
||||
if err == store.ErrNotFound {
|
||||
return nil, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Handle & username
|
||||
u, err = store.LookupUserByHandle(ctx, s, i)
|
||||
if err == store.ErrNotFound {
|
||||
u, err = store.LookupUserByUsername(ctx, s, i)
|
||||
}
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if u != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
137
server/pkg/envoy/store/user_marshal.go
Normal file
137
server/pkg/envoy/store/user_marshal.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
func NewUserFromResource(res *resource.User, cfg *EncoderConfig) resourceState {
|
||||
return &user{
|
||||
cfg: mergeConfig(cfg, res.Config()),
|
||||
|
||||
res: res,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *user) Prepare(ctx context.Context, pl *payload) (err error) {
|
||||
// Reset old identifiers
|
||||
n.res.Res.ID = 0
|
||||
|
||||
// Try to get the original user
|
||||
n.u, err = findUserStore(ctx, pl.s, makeGenericFilter(n.res.Identifiers()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if n.u != nil {
|
||||
n.res.Res.ID = n.u.ID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *user) Encode(ctx context.Context, pl *payload) (err error) {
|
||||
res := n.res.Res
|
||||
exists := n.u != nil && n.u.ID > 0
|
||||
|
||||
// Determine the ID
|
||||
if res.ID <= 0 && exists {
|
||||
res.ID = n.u.ID
|
||||
}
|
||||
if res.ID <= 0 {
|
||||
res.ID = NextID()
|
||||
}
|
||||
|
||||
ts := n.res.Timestamps()
|
||||
if ts != nil {
|
||||
if ts.CreatedAt != nil {
|
||||
res.CreatedAt = *ts.CreatedAt.T
|
||||
} else {
|
||||
res.CreatedAt = *now()
|
||||
}
|
||||
if ts.UpdatedAt != nil {
|
||||
res.UpdatedAt = ts.UpdatedAt.T
|
||||
}
|
||||
if ts.SuspendedAt != nil {
|
||||
res.SuspendedAt = ts.SuspendedAt.T
|
||||
}
|
||||
if ts.DeletedAt != nil {
|
||||
res.DeletedAt = ts.DeletedAt.T
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate the resource skip expression
|
||||
// @todo expand available parameters; similar implementation to compose/types/record@Dict
|
||||
if skip, err := basicSkipEval(ctx, n.cfg, !exists); err != nil {
|
||||
return err
|
||||
} else if skip {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a fresh user
|
||||
if !exists {
|
||||
if err = store.CreateUser(ctx, pl.s, res); err != nil {
|
||||
return
|
||||
}
|
||||
return n.membership(ctx, pl.s, res, pl.state.ParentResources, exists)
|
||||
}
|
||||
|
||||
// Update existing user
|
||||
switch n.cfg.OnExisting {
|
||||
case resource.Skip:
|
||||
return nil
|
||||
|
||||
case resource.MergeLeft:
|
||||
res = mergeUsers(n.u, res)
|
||||
|
||||
case resource.MergeRight:
|
||||
res = mergeUsers(res, n.u)
|
||||
}
|
||||
|
||||
if err = store.UpdateUser(ctx, pl.s, res); err != nil {
|
||||
return err
|
||||
}
|
||||
n.res.Res = res
|
||||
|
||||
return n.membership(ctx, pl.s, res, pl.state.ParentResources, exists)
|
||||
}
|
||||
|
||||
func (n *user) membership(ctx context.Context, s store.Storer, res *types.User, pp resource.InterfaceSet, exists bool) (err error) {
|
||||
// find all roles
|
||||
roles := make([]uint64, 0, 10)
|
||||
var r *types.Role
|
||||
for _, m := range n.res.RoleMembership {
|
||||
r, err = findRole(ctx, s, pp, m)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
roles = append(roles, r.ID)
|
||||
}
|
||||
|
||||
// update
|
||||
// @todo some smarter diff calculations; should be fine for now but could be improved.
|
||||
var mm types.RoleMemberSet
|
||||
if exists {
|
||||
mm, _, err = s.SearchRoleMembers(ctx, types.RoleMemberFilter{Resource: fmt.Sprintf("corteza::system:user/%d", n.u.ID)})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, m := range mm {
|
||||
if err = store.DeleteRoleMember(ctx, s, m); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
for _, r := range roles {
|
||||
if err = store.CreateRoleMember(ctx, s, &types.RoleMember{Resource: fmt.Sprintf("corteza::system:user/%d", res.ID), RoleID: r}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
58
server/pkg/envoy/store/user_test.go
Normal file
58
server/pkg/envoy/store/user_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUser_Merger(t *testing.T) {
|
||||
req := require.New(t)
|
||||
|
||||
now := time.Time{}
|
||||
nowP := &time.Time{}
|
||||
|
||||
empty := &types.User{}
|
||||
full := &types.User{
|
||||
Username: "username",
|
||||
Email: "email",
|
||||
Name: "name",
|
||||
Handle: "handle",
|
||||
Kind: types.SystemUser,
|
||||
Meta: &types.UserMeta{},
|
||||
CreatedAt: now,
|
||||
UpdatedAt: nowP,
|
||||
SuspendedAt: nowP,
|
||||
DeletedAt: nowP,
|
||||
}
|
||||
|
||||
t.Run("merge on empty", func(t *testing.T) {
|
||||
c := mergeUsers(empty, full)
|
||||
req.Equal("username", c.Username)
|
||||
req.Equal("email", c.Email)
|
||||
req.Equal("name", c.Name)
|
||||
req.Equal("handle", c.Handle)
|
||||
req.Equal(types.SystemUser, c.Kind)
|
||||
req.Equal(&types.UserMeta{}, c.Meta)
|
||||
req.Equal(now, c.CreatedAt)
|
||||
req.Equal(nowP, c.UpdatedAt)
|
||||
req.Equal(nowP, c.SuspendedAt)
|
||||
req.Equal(nowP, c.DeletedAt)
|
||||
})
|
||||
|
||||
t.Run("merge with empty", func(t *testing.T) {
|
||||
c := mergeUsers(full, empty)
|
||||
req.Equal("username", c.Username)
|
||||
req.Equal("email", c.Email)
|
||||
req.Equal("name", c.Name)
|
||||
req.Equal("handle", c.Handle)
|
||||
req.Equal(types.SystemUser, c.Kind)
|
||||
req.Equal(&types.UserMeta{}, c.Meta)
|
||||
req.Equal(now, c.CreatedAt)
|
||||
req.Equal(nowP, c.UpdatedAt)
|
||||
req.Equal(nowP, c.SuspendedAt)
|
||||
req.Equal(nowP, c.DeletedAt)
|
||||
})
|
||||
}
|
||||
19
server/pkg/envoy/store/user_unmarshal.go
Normal file
19
server/pkg/envoy/store/user_unmarshal.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
func newUser(u *types.User) *user {
|
||||
return &user{
|
||||
u: u,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *user) MarshalEnvoy() ([]resource.Interface, error) {
|
||||
return envoy.CollectNodes(
|
||||
resource.NewUser(u.u),
|
||||
)
|
||||
}
|
||||
184
server/pkg/envoy/store/util.go
Normal file
184
server/pkg/envoy/store/util.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/pkg/expr"
|
||||
"github.com/cortezaproject/corteza/server/pkg/id"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
)
|
||||
|
||||
type (
|
||||
// genericFilter defines resource identifiers in a generic manner
|
||||
genericFilter struct {
|
||||
id uint64
|
||||
identifiers []string
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
// simple uint check.
|
||||
// we'll use the pkg/handle to check for handles.
|
||||
refy = regexp.MustCompile(`^[1-9](\d*)$`)
|
||||
|
||||
// wrapper around NextID that will aid service testing
|
||||
NextID = func() uint64 {
|
||||
return id.Next()
|
||||
}
|
||||
|
||||
// wrapper around time.Now() that will aid testing
|
||||
now = func() *time.Time {
|
||||
c := time.Now().Round(time.Second)
|
||||
return &c
|
||||
}
|
||||
|
||||
// Global expression parser
|
||||
exprP = expr.Parser()
|
||||
)
|
||||
|
||||
func makeGenericFilter(ii resource.Identifiers) (f genericFilter) {
|
||||
for _, i := range ii {
|
||||
if i == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if refy.MatchString(i) && f.id <= 0 {
|
||||
id, err := strconv.ParseUint(i, 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
f.id = id
|
||||
} else {
|
||||
f.identifiers = append(f.identifiers, i)
|
||||
}
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
// resolveUserstamps tries to resolve any missing userstamp using the store
|
||||
//
|
||||
// @todo rework based on the bellow userIndex
|
||||
func resolveUserstamps(ctx context.Context, s store.Storer, rr []resource.Interface, us *resource.Userstamps) (*resource.Userstamps, error) {
|
||||
if us == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
fetch := func(us *resource.Userstamp) (*resource.Userstamp, error) {
|
||||
if us == nil || (us.UserID == 0 && us.Ref == "") {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// This one can be considered as valid
|
||||
if us.Ref != "" && us.UserID > 0 && us.U != nil {
|
||||
return us, nil
|
||||
}
|
||||
|
||||
ii := resource.MakeIdentifiers()
|
||||
|
||||
if us.UserID > 0 {
|
||||
ii = ii.Add(strconv.FormatUint(us.UserID, 10))
|
||||
}
|
||||
if us.Ref != "" {
|
||||
ii = ii.Add(us.Ref)
|
||||
}
|
||||
|
||||
u, err := findUser(ctx, s, rr, ii)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if u == nil {
|
||||
return nil, resource.UserErrUnresolved(ii)
|
||||
}
|
||||
|
||||
return resource.MakeUserstamp(u), nil
|
||||
}
|
||||
|
||||
var err error
|
||||
us.CreatedBy, err = fetch(us.CreatedBy)
|
||||
us.UpdatedBy, err = fetch(us.UpdatedBy)
|
||||
us.DeletedBy, err = fetch(us.DeletedBy)
|
||||
us.OwnedBy, err = fetch(us.OwnedBy)
|
||||
us.RunAs, err = fetch(us.RunAs)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return us, nil
|
||||
}
|
||||
|
||||
// syncUserStamps tries to resolve any missing userstamp using the userIndex
|
||||
func syncUserStamps(us *resource.Userstamps, ux *userIndex) {
|
||||
if us.CreatedBy != nil && us.CreatedBy.UserID > 0 {
|
||||
us.CreatedBy.U = ux.users[us.CreatedBy.UserID]
|
||||
}
|
||||
if us.UpdatedBy != nil && us.UpdatedBy.UserID > 0 {
|
||||
us.UpdatedBy.U = ux.users[us.UpdatedBy.UserID]
|
||||
}
|
||||
if us.DeletedBy != nil && us.DeletedBy.UserID > 0 {
|
||||
us.DeletedBy.U = ux.users[us.DeletedBy.UserID]
|
||||
}
|
||||
if us.OwnedBy != nil && us.OwnedBy.UserID > 0 {
|
||||
us.OwnedBy.U = ux.users[us.OwnedBy.UserID]
|
||||
}
|
||||
if us.RunAs != nil && us.RunAs.UserID > 0 {
|
||||
us.RunAs.U = ux.users[us.RunAs.UserID]
|
||||
}
|
||||
}
|
||||
|
||||
func mergeConfig(ec *EncoderConfig, rs *resource.EnvoyConfig) *EncoderConfig {
|
||||
// Nothing we can do
|
||||
if rs == nil {
|
||||
return ec
|
||||
}
|
||||
|
||||
// Take resource config as base
|
||||
rr := &EncoderConfig{
|
||||
OnExisting: rs.OnExisting,
|
||||
SkipIf: rs.SkipIf,
|
||||
}
|
||||
|
||||
// Default to store config
|
||||
rr.Defer = ec.Defer
|
||||
rr.DeferNok = ec.DeferNok
|
||||
rr.DeferOk = ec.DeferOk
|
||||
if rr.OnExisting == resource.Default {
|
||||
rr.OnExisting = ec.OnExisting
|
||||
}
|
||||
if rr.SkipIf == "" {
|
||||
rr.SkipIf = ec.SkipIf
|
||||
}
|
||||
|
||||
return rr
|
||||
}
|
||||
|
||||
func basicSkipEval(ctx context.Context, cfg *EncoderConfig, missing bool) (bool, error) {
|
||||
if cfg == nil {
|
||||
cfg = &EncoderConfig{}
|
||||
}
|
||||
|
||||
if cfg.SkipIf != "" {
|
||||
evl, err := exprP.NewEvaluable(cfg.SkipIf)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// @todo expand this
|
||||
skip, err := evl.EvalBool(ctx, map[string]interface{}{
|
||||
"missing": missing,
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return skip, nil
|
||||
}
|
||||
|
||||
// Don't skip by default
|
||||
return false, nil
|
||||
}
|
||||
Reference in New Issue
Block a user