Update Envoy for new/extended resources

* Reports
* API GW
* Module field; user role filter
* Comment page block
This commit is contained in:
Tomaž Jerman
2021-09-22 11:38:35 +02:00
parent 46a7d94d53
commit 104bfb23de
36 changed files with 2348 additions and 16 deletions
+85
View File
@@ -0,0 +1,85 @@
package resource
import (
"fmt"
"github.com/cortezaproject/corteza-server/system/types"
)
type (
APIGateway struct {
*base
Res *types.ApigwRoute
Filters []*APIGatewayFilter
}
APIGatewayFilter struct {
*base
Res *types.ApigwFilter
}
)
func NewAPIGateway(res *types.ApigwRoute) *APIGateway {
r := &APIGateway{
base: &base{},
}
r.SetResourceType(types.ApigwRouteResourceType)
r.Res = res
r.AddIdentifier(identifiers(res.ID)...)
// Initial stamps
r.SetTimestamps(MakeTimestampsCUDA(&res.CreatedAt, res.UpdatedAt, res.DeletedAt, nil))
us := MakeUserstampsCUDO(res.CreatedBy, res.UpdatedBy, res.DeletedBy, 0)
r.SetUserstamps(us)
return r
}
func (r *APIGateway) AddGatewayFilter(res *types.ApigwFilter) *APIGatewayFilter {
f := &APIGatewayFilter{
base: &base{},
}
f.Res = res
// Initial stamps
f.SetTimestamps(MakeTimestampsCUDA(&res.CreatedAt, res.UpdatedAt, res.DeletedAt, nil))
f.SetUserstamps(MakeUserstampsCUDO(res.CreatedBy, res.UpdatedBy, res.DeletedBy, 0))
r.Filters = append(r.Filters, f)
return f
}
func (r *APIGateway) SysID() uint64 {
return r.Res.ID
}
// FindAPIGateway looks for the ApigwRoute in the resource set
func FindAPIGateway(rr InterfaceSet, ii Identifiers) (ns *types.ApigwRoute) {
var wfRes *APIGateway
rr.Walk(func(r Interface) error {
wr, ok := r.(*APIGateway)
if !ok {
return nil
}
if wr.Identifiers().HasAny(ii) {
wfRes = wr
}
return nil
})
// Found it
if wfRes != nil {
return wfRes.Res
}
return nil
}
func APIGatewayErrUnresolved(ii Identifiers) error {
return fmt.Errorf("automation apu gateway unresolved %v", ii.StringSlice())
}
+81 -2
View File
@@ -5,6 +5,7 @@ import (
"strconv"
"github.com/cortezaproject/corteza-server/compose/types"
"github.com/cortezaproject/corteza-server/pkg/minions"
systemTypes "github.com/cortezaproject/corteza-server/system/types"
"github.com/spf13/cast"
)
@@ -24,8 +25,9 @@ type (
ResFields []*ComposeModuleField
// Might keep track of related NS
RefNs *Ref
RefMods RefSet
RefNs *Ref
RefMods RefSet
RefRoles RefSet
}
)
@@ -52,6 +54,19 @@ func NewComposeModule(res *types.Module, nsRef string) *ComposeModule {
if refMod != "" && refMod != "0" {
r.RefMods = append(r.RefMods, r.AddRef(types.ModuleResourceType, refMod).Constraint(r.RefNs))
}
case "User":
refRoles := ComposeModuleFieldExtractUserFieldRoles(f.Options["roles"])
if len(refRoles) == 0 {
refRoles = ComposeModuleFieldExtractUserFieldRoles(f.Options["role"])
}
if len(refRoles) == 0 {
refRoles = ComposeModuleFieldExtractUserFieldRoles(f.Options["roleID"])
}
for _, refRole := range refRoles {
r.RefRoles = append(r.RefRoles, r.AddRef(systemTypes.RoleResourceType, refRole))
}
}
}
@@ -204,3 +219,67 @@ func NewComposeModuleField(res *types.ModuleField, nsRef, modRef string) *Compos
return r
}
// ComposeModuleFieldExtractUserFieldRoles is a helper to extract roles
// from the given filer options.
func ComposeModuleFieldExtractUserFieldRoles(i interface{}) []string {
if minions.IsNil(i) {
return nil
}
out := make([]string, 0, 1)
isOk := func(v string) bool {
return v != "" && v != "0"
}
switch v := i.(type) {
case uint64:
aux := strconv.FormatUint(v, 10)
if !isOk(aux) {
return nil
}
return []string{aux}
case []uint64:
for _, i := range v {
aux := strconv.FormatUint(i, 10)
if !isOk(aux) {
continue
}
out = append(out, aux)
}
return out
case string:
if !isOk(v) {
return nil
}
return []string{v}
case []string:
for _, aux := range v {
if !isOk(aux) {
continue
}
out = append(out, aux)
}
return out
case []interface{}:
for _, i := range v {
aux := cast.ToString(i)
if !isOk(aux) {
continue
}
out = append(out, aux)
}
return out
case interface{}:
aux := cast.ToString(v)
if !isOk(aux) {
return nil
}
return []string{aux}
}
return nil
}
+8
View File
@@ -129,6 +129,14 @@ func NewComposePage(pg *types.Page, nsRef, modRef, parentRef string) *ComposePag
r.ModRefs = append(r.ModRefs, ref)
}
}
case "Comment":
id := ss(b.Options, "module", "moduleID")
if id != "" {
ref := r.AddRef(types.ModuleResourceType, id).Constraint(r.RefNs)
r.BlockRefs[i] = add(r.BlockRefs[i], ref)
r.ModRefs = append(r.ModRefs, ref)
}
}
}
+113
View 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
}
+219
View File
@@ -0,0 +1,219 @@
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) {
if n.cfg.IgnoreStore {
n.res.Res.ID = 0
return nil
}
// 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
}
if n.cfg.IgnoreStore {
for _, t := range n.ff {
t.ID = 0
}
return nil
}
// 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
View 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,
)
}
+7 -2
View File
@@ -17,8 +17,9 @@ type (
res *resource.ComposeModule
mod *types.Module
relNS *types.Namespace
recFields map[string]uint64
relNS *types.Namespace
recFields map[string]uint64
userFields map[string]uint64
}
)
@@ -203,3 +204,7 @@ func findComposeModuleFieldsStore(ctx context.Context, s store.Storer, mod *type
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())
}
+51 -1
View File
@@ -7,6 +7,7 @@ import (
"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 {
@@ -15,7 +16,8 @@ func NewComposeModuleFromResource(res *resource.ComposeModule, cfg *EncoderConfi
res: res,
recFields: make(map[string]uint64),
recFields: make(map[string]uint64),
userFields: make(map[string]uint64),
}
}
@@ -57,6 +59,25 @@ func (n *composeModule) Prepare(ctx context.Context, pl *payload) (err error) {
}
}
// 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.cfg.IgnoreStore || n.relNS.ID <= 0 {
return nil
@@ -164,6 +185,35 @@ func (n *composeModule) Encode(ctx context.Context, pl *payload) (err error) {
f.Options["moduleID"] = strconv.FormatUint(modID, 10)
delete(f.Options, "module")
}
if 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
+12
View File
@@ -329,6 +329,18 @@ func (n *composePage) Encode(ctx context.Context, pl *payload) (err error) {
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")
}
}
+9 -4
View File
@@ -24,10 +24,13 @@ type (
composeChart []*composeChartFilter
// System stuff
roles []*roleFilter
users []*userFilter
templates []*templateFilter
applications []*applicationFilter
roles []*roleFilter
users []*userFilter
templates []*templateFilter
applications []*applicationFilter
apiGwRoutes []*apiGwRouteFilter
reports []*reportFilter
settings []*settingFilter
rbac []*rbacFilter
resourceTranslations []*resourceTranslationFilter
@@ -146,6 +149,8 @@ func (d *decoder) Decode(ctx context.Context, s store.Storer, f *DecodeFilter) (
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),
+4
View File
@@ -146,6 +146,10 @@ func (se *storeEncoder) Prepare(ctx context.Context, ee ...*envoy.ResourceState)
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:
+115
View 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.ReportProjectionSet
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.Projections == nil {
c.Projections = b.Projections
}
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
}
+128
View File
@@ -0,0 +1,128 @@
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) {
if n.cfg.IgnoreStore {
n.res.Res.ID = 0
return nil
}
// 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.Projections = make(types.ReportProjectionSet, 0, 10)
for _, rp := range n.res.Projections {
res.Projections = append(res.Projections, 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
View 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.Projections,
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.AddReportProjection(p)
}
return envoy.CollectNodes(
rs,
)
}
@@ -209,6 +209,15 @@ func (n *resourceTranslation) makeResourceTranslation(pl *payload) (string, erro
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.
+131
View File
@@ -18,6 +18,8 @@ type (
userFilter types.UserFilter
templateFilter types.TemplateFilter
applicationFilter types.ApplicationFilter
apiGwRouteFilter types.ApigwRouteFilter
reportFilter types.ReportFilter
settingFilter types.SettingsFilter
rbacFilter struct {
rbac.RuleFilter
@@ -175,6 +177,104 @@ func (d *systemDecoder) decodeTemplates(ctx context.Context, s store.Storer, ff
}
}
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 {
@@ -467,6 +567,21 @@ func (df *DecodeFilter) systemFromResource(rr ...string) *DecodeFilter {
TemplateID: []uint64{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: []uint64{reportID},
})
}
case "system:application":
df = df.Applications(&types.ApplicationFilter{
Query: id,
@@ -554,6 +669,22 @@ func (df *DecodeFilter) Templates(f *types.TemplateFilter) *DecodeFilter {
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 {
+39
View File
@@ -0,0 +1,39 @@
package yaml
import (
"github.com/cortezaproject/corteza-server/pkg/envoy/resource"
"github.com/cortezaproject/corteza-server/system/types"
)
type (
apiGateway struct {
res *types.ApigwRoute
filters apiGwFilterSet
ts *resource.Timestamps
us *resource.Userstamps
envoyConfig *resource.EnvoyConfig
encoderConfig *EncoderConfig
rbac rbacRuleSet
}
apiGatewaySet []*apiGateway
apiGwFilter struct {
res *types.ApigwFilter
ts *resource.Timestamps
us *resource.Userstamps
envoyConfig *resource.EnvoyConfig
encoderConfig *EncoderConfig
}
apiGwFilterSet []*apiGwFilter
)
func (nn apiGatewaySet) configureEncoder(cfg *EncoderConfig) {
for _, n := range nn {
n.encoderConfig = cfg
}
}
+115
View File
@@ -0,0 +1,115 @@
package yaml
import (
"context"
"github.com/cortezaproject/corteza-server/pkg/envoy"
"github.com/cortezaproject/corteza-server/pkg/envoy/resource"
systemTypes "github.com/cortezaproject/corteza-server/system/types"
)
func apiGatewayFromResource(r *resource.APIGateway, cfg *EncoderConfig) *apiGateway {
ff := make(apiGwFilterSet, len(r.Filters))
for i, t := range r.Filters {
ff[i] = &apiGwFilter{
res: t.Res,
encoderConfig: cfg,
}
}
return &apiGateway{
res: r.Res,
filters: ff,
encoderConfig: cfg,
}
}
func (n *apiGateway) Prepare(ctx context.Context, state *envoy.ResourceState) (err error) {
wf, ok := state.Res.(*resource.APIGateway)
if !ok {
return encoderErrInvalidResource(systemTypes.ApigwRouteResourceType, state.Res.ResourceType())
}
n.res = wf.Res
n.us = wf.Userstamps()
return nil
}
func (n *apiGateway) Encode(ctx context.Context, doc *Document, state *envoy.ResourceState) (err error) {
if n.res.ID <= 0 {
n.res.ID = nextID()
}
n.ts, err = resource.MakeTimestampsCUDA(&n.res.CreatedAt, n.res.UpdatedAt, n.res.DeletedAt, nil).
Model(n.encoderConfig.TimeLayout, n.encoderConfig.Timezone)
if err != nil {
return err
}
n.us, err = resolveUserstamps(state.ParentResources, n.us)
if err != nil {
return err
}
// @todo skip eval?
doc.addApiGateway(n)
return err
}
func (g *apiGateway) MarshalYAML() (interface{}, error) {
var err error
nn, err := makeMap(
"endpoint", g.res.Endpoint,
"method", g.res.Method,
"enabled", g.res.Enabled,
"group", g.res.Group,
"meta", g.res.Meta,
"filters", g.filters,
)
if err != nil {
return nil, err
}
nn, err = encodeTimestamps(nn, g.ts)
if err != nil {
return nil, err
}
nn, err = encodeUserstamps(nn, g.us)
if err != nil {
return nil, err
}
return nn, nil
}
func (f *apiGwFilter) MarshalYAML() (interface{}, error) {
var err error
nn, err := makeMap(
"weight", f.res.Weight,
"ref", f.res.Ref,
"kind", f.res.Kind,
"params", f.res.Params,
)
if err != nil {
return nil, err
}
nn, err = encodeTimestamps(nn, f.ts)
if err != nil {
return nil, err
}
nn, err = encodeUserstamps(nn, f.us)
if err != nil {
return nil, err
}
return nn, nil
}
+153
View File
@@ -0,0 +1,153 @@
package yaml
import (
"strings"
"github.com/cortezaproject/corteza-server/pkg/envoy"
"github.com/cortezaproject/corteza-server/pkg/envoy/resource"
"github.com/cortezaproject/corteza-server/pkg/y7s"
"github.com/cortezaproject/corteza-server/system/types"
"gopkg.in/yaml.v3"
)
func (wset *apiGatewaySet) UnmarshalYAML(n *yaml.Node) error {
return y7s.Each(n, func(k, v *yaml.Node) (err error) {
var (
wrap = &apiGateway{}
)
if v == nil {
return y7s.NodeErr(n, "malformed api gateway definition")
}
if err = v.Decode(&wrap); err != nil {
return
}
*wset = append(*wset, wrap)
return
})
}
func (wrap *apiGateway) UnmarshalYAML(n *yaml.Node) (err error) {
if wrap.res == nil {
wrap.rbac = make(rbacRuleSet, 0, 10)
wrap.res = &types.ApigwRoute{}
}
if wrap.rbac, err = decodeRbac(n); err != nil {
return
}
if wrap.envoyConfig, err = decodeEnvoyConfig(n); err != nil {
return
}
if wrap.ts, err = decodeTimestamps(n); err != nil {
return
}
if wrap.us, err = decodeUserstamps(n); err != nil {
return
}
return y7s.EachMap(n, func(k, v *yaml.Node) (err error) {
switch strings.ToLower(k.Value) {
case "endpoint":
return y7s.DecodeScalar(v, "api gw endpoint", &wrap.res.Endpoint)
case "method":
return y7s.DecodeScalar(v, "api gw method", &wrap.res.Method)
case "enabled":
return y7s.DecodeScalar(v, "api gw enabled", &wrap.res.Enabled)
case "group":
return y7s.DecodeScalar(v, "api gw group", &wrap.res.Group)
case "meta":
aux := &types.ApigwRouteMeta{}
err = v.Decode(&aux)
if err != nil {
return err
}
wrap.res.Meta = *aux
return nil
case "filters":
wrap.filters = make(apiGwFilterSet, 0, 10)
err = v.Decode(&wrap.filters)
if err != nil {
return err
}
}
return nil
})
}
func (wrap *apiGwFilter) UnmarshalYAML(n *yaml.Node) (err error) {
if wrap.res == nil {
wrap.res = &types.ApigwFilter{}
}
if wrap.envoyConfig, err = decodeEnvoyConfig(n); err != nil {
return
}
if wrap.ts, err = decodeTimestamps(n); err != nil {
return
}
if wrap.us, err = decodeUserstamps(n); err != nil {
return
}
return y7s.EachMap(n, func(k, v *yaml.Node) (err error) {
switch strings.ToLower(k.Value) {
case "weight":
return y7s.DecodeScalar(v, "route filter weight", &wrap.res.Weight)
case "ref":
return y7s.DecodeScalar(v, "route filter ref", &wrap.res.Ref)
case "kind":
return y7s.DecodeScalar(v, "route filter kind", &wrap.res.Kind)
case "params":
return v.Decode(&wrap.res.Params)
}
return nil
})
}
func (wset apiGatewaySet) MarshalEnvoy() ([]resource.Interface, error) {
nn := make([]resource.Interface, 0, len(wset)*2)
for _, res := range wset {
if tmp, err := res.MarshalEnvoy(); err != nil {
return nil, err
} else {
nn = append(nn, tmp...)
}
}
return nn, nil
}
func (wrap apiGateway) MarshalEnvoy() ([]resource.Interface, error) {
rs := resource.NewAPIGateway(wrap.res)
rs.SetTimestamps(wrap.ts)
rs.SetUserstamps(wrap.us)
rs.SetConfig(wrap.envoyConfig)
for _, f := range wrap.filters {
trs := rs.AddGatewayFilter(f.res)
trs.SetTimestamps(f.ts)
trs.SetUserstamps(f.us)
}
return envoy.CollectNodes(
rs,
wrap.rbac.bindResource(rs),
)
}
+3 -1
View File
@@ -3,6 +3,7 @@ package yaml
import (
"github.com/cortezaproject/corteza-server/compose/types"
"github.com/cortezaproject/corteza-server/pkg/envoy/resource"
systemTypes "github.com/cortezaproject/corteza-server/system/types"
)
type (
@@ -42,7 +43,8 @@ type (
cfg *EncoderConfig
expr composeModuleFieldExpr
relMod *types.Module
relMod *types.Module
relRoles systemTypes.RoleSet
rbac rbacRuleSet
locale resourceTranslationSet
+32 -2
View File
@@ -3,6 +3,7 @@ package yaml
import (
"context"
"encoding/json"
"strconv"
"github.com/cortezaproject/corteza-server/compose/types"
"github.com/cortezaproject/corteza-server/pkg/envoy"
@@ -38,7 +39,8 @@ func (n *composeModule) Prepare(ctx context.Context, state *envoy.ResourceState)
expr: composeModuleFieldExpr(f.Expressions),
}
if f.Kind == "Record" {
switch f.Kind {
case "Record":
refMod := f.Options.String("module")
if refMod == "" {
refMod = f.Options.String("moduleID")
@@ -47,6 +49,23 @@ func (n *composeModule) Prepare(ctx context.Context, state *envoy.ResourceState)
if cmf.relMod == nil {
return resource.ComposeModuleErrUnresolved(resource.MakeIdentifiers(refMod))
}
case "User":
refRoles := resource.ComposeModuleFieldExtractUserFieldRoles(f.Options["roles"])
if len(refRoles) == 0 {
refRoles = resource.ComposeModuleFieldExtractUserFieldRoles(f.Options["role"])
}
if len(refRoles) == 0 {
refRoles = resource.ComposeModuleFieldExtractUserFieldRoles(f.Options["roleID"])
}
for _, ref := range refRoles {
aux := resource.FindRole(state.ParentResources, resource.MakeIdentifiers(ref))
if aux == nil {
return resource.RoleErrUnresolved(resource.MakeIdentifiers(ref))
}
cmf.relRoles = append(cmf.relRoles, aux)
}
}
n.fields = append(n.fields, cmf)
@@ -136,7 +155,8 @@ func (c *composeModule) MarshalYAML() (interface{}, error) {
func (c *composeModuleField) MarshalYAML() (interface{}, error) {
auxOpt := c.res.Options
if c.res.Kind == "Record" {
switch c.res.Kind {
case "Record":
ref := c.relMod.Handle
if ref == "" {
ref = c.relMod.Name
@@ -144,6 +164,16 @@ func (c *composeModuleField) MarshalYAML() (interface{}, error) {
auxOpt["module"] = ref
delete(auxOpt, "moduleID")
case "User":
aux := make([]string, 0, len(c.relRoles))
for _, r := range c.relRoles {
aux = append(aux, firstOkString(r.Handle, strconv.FormatUint(r.ID, 10)))
}
auxOpt["roles"] = aux
delete(auxOpt, "role")
delete(auxOpt, "roleID")
}
if _, has := auxOpt["multiDelimiter"]; has {
+5
View File
@@ -228,6 +228,11 @@ func (c *composePageBlock) MarshalYAML() (interface{}, error) {
}
break
case "Comment":
opt["moduleID"] = c.refMod[0]
delete(opt, "module")
break
}
return makeMap(
+49 -3
View File
@@ -19,9 +19,13 @@ type (
users userSet
templates templateSet
applications applicationSet
settings settingSet
rbac rbacRuleSet
locale resourceTranslationSet
apiGateway apiGatewaySet
reports reportSet
settings settingSet
rbac rbacRuleSet
locale resourceTranslationSet
cfg *EncoderConfig
}
@@ -55,6 +59,12 @@ func (doc *Document) UnmarshalYAML(n *yaml.Node) (err error) {
case "templates":
return v.Decode(&doc.templates)
case "apigateway", "apigw":
return v.Decode(&doc.apiGateway)
case "reports":
return v.Decode(&doc.reports)
case "applications":
return v.Decode(&doc.applications)
@@ -120,6 +130,28 @@ func (doc *Document) MarshalYAML() (interface{}, error) {
}
}
if doc.reports != nil && len(doc.reports) > 0 {
doc.reports.configureEncoder(doc.cfg)
dn, err = encodeResource(dn, "reports", doc.reports, doc.cfg.MappedOutput, "handle")
if err != nil {
return nil, err
}
}
if doc.apiGateway != nil && len(doc.apiGateway) > 0 {
doc.apiGateway.configureEncoder(doc.cfg)
// API GW don't support map representation
// @todo use path+proto?
dn, err = addMap(dn,
"apigateway", doc.apiGateway,
)
if err != nil {
return nil, err
}
}
if doc.applications != nil && len(doc.applications) > 0 {
doc.applications.configureEncoder(doc.cfg)
@@ -186,6 +218,12 @@ func (doc *Document) Decode(ctx context.Context) ([]resource.Interface, error) {
if doc.applications != nil {
mm = append(mm, doc.applications)
}
if doc.reports != nil {
mm = append(mm, doc.reports)
}
if doc.apiGateway != nil {
mm = append(mm, doc.apiGateway)
}
if doc.settings != nil {
for _, s := range doc.settings {
mm = append(mm, s)
@@ -275,6 +313,14 @@ func (doc *Document) addAutomationWorkflow(m *automationWorkflow) {
doc.automation.Workflows = append(doc.automation.Workflows, m)
}
func (doc *Document) addApiGateway(a *apiGateway) {
doc.apiGateway = append(doc.apiGateway, a)
}
func (doc *Document) addReport(a *report) {
doc.reports = append(doc.reports, a)
}
func (doc *Document) addRole(r *role) {
if doc.roles == nil {
doc.roles = make(roleSet, 0, 20)
+8
View File
@@ -98,6 +98,10 @@ func NewYamlEncoder(cfg *EncoderConfig) envoy.PrepareEncodeStreamer {
// It initializes and prepares the resource state for each provided resource
func (ye *yamlEncoder) Prepare(ctx context.Context, ee ...*envoy.ResourceState) (err error) {
f := func(rs resourceState, es *envoy.ResourceState) error {
if rs == nil {
return nil
}
err = rs.Prepare(ctx, es)
if err != nil {
return err
@@ -135,6 +139,10 @@ func (ye *yamlEncoder) Prepare(ctx context.Context, ee ...*envoy.ResourceState)
err = f(templateFromResource(res, ye.cfg), e)
case *resource.Application:
err = f(applicationFromResource(res, ye.cfg), e)
case *resource.APIGateway:
err = f(apiGatewayFromResource(res, ye.cfg), e)
case *resource.Report:
err = f(reportFromResource(res, ye.cfg), e)
case *resource.Setting:
err = f(settingFromResource(res, ye.cfg), e)
case *resource.RbacRule:
+48
View File
@@ -0,0 +1,48 @@
package yaml
import (
"github.com/cortezaproject/corteza-server/pkg/envoy/resource"
"github.com/cortezaproject/corteza-server/system/types"
)
type (
report struct {
res *types.Report
sources reportSourceSet
projections reportProjectionSet
ts *resource.Timestamps
us *resource.Userstamps
envoyConfig *resource.EnvoyConfig
encoderConfig *EncoderConfig
rbac rbacRuleSet
locale resourceTranslationSet
}
reportSet []*report
reportSource struct {
res *types.ReportDataSource
envoyConfig *resource.EnvoyConfig
encoderConfig *EncoderConfig
}
reportSourceSet []*reportSource
reportProjection struct {
res *types.ReportProjection
envoyConfig *resource.EnvoyConfig
encoderConfig *EncoderConfig
locale resourceTranslationSet
}
reportProjectionSet []*reportProjection
)
func (nn reportSet) configureEncoder(cfg *EncoderConfig) {
for _, n := range nn {
n.encoderConfig = cfg
}
}
+131
View File
@@ -0,0 +1,131 @@
package yaml
import (
"context"
automationTypes "github.com/cortezaproject/corteza-server/automation/types"
"github.com/cortezaproject/corteza-server/pkg/envoy"
"github.com/cortezaproject/corteza-server/pkg/envoy/resource"
)
func reportFromResource(r *resource.Report, cfg *EncoderConfig) *report {
ss := make(reportSourceSet, len(r.Sources))
for i, s := range r.Sources {
ss[i] = &reportSource{
res: s.Res,
encoderConfig: cfg,
}
}
pp := make(reportProjectionSet, len(r.Projections))
for i, p := range r.Projections {
pp[i] = &reportProjection{
res: p.Res,
encoderConfig: cfg,
}
}
return &report{
res: r.Res,
sources: ss,
projections: pp,
encoderConfig: cfg,
}
}
func (n *report) Prepare(ctx context.Context, state *envoy.ResourceState) (err error) {
wf, ok := state.Res.(*resource.Report)
if !ok {
return encoderErrInvalidResource(automationTypes.WorkflowResourceType, state.Res.ResourceType())
}
n.res = wf.Res
n.us = wf.Userstamps()
return nil
}
func (n *report) Encode(ctx context.Context, doc *Document, state *envoy.ResourceState) (err error) {
if n.res.ID <= 0 {
n.res.ID = nextID()
}
n.ts, err = resource.MakeTimestampsCUDA(&n.res.CreatedAt, n.res.UpdatedAt, n.res.DeletedAt, nil).
Model(n.encoderConfig.TimeLayout, n.encoderConfig.Timezone)
if err != nil {
return err
}
n.us, err = resolveUserstamps(state.ParentResources, n.us)
if err != nil {
return err
}
// @todo skip eval?
doc.addReport(n)
return err
}
func (wf *report) MarshalYAML() (interface{}, error) {
var err error
nn, err := makeMap(
"handle", wf.res.Handle,
"meta", wf.res.Meta,
"sources", wf.sources,
"projections", wf.projections,
)
if err != nil {
return nil, err
}
nn, err = encodeTimestamps(nn, wf.ts)
if err != nil {
return nil, err
}
nn, err = encodeUserstamps(nn, wf.us)
if err != nil {
return nil, err
}
return nn, nil
}
func (t *reportSource) MarshalYAML() (interface{}, error) {
var err error
nn, err := makeMap(
"meta", t.res.Meta,
"step", t.res.Step,
)
if err != nil {
return nil, err
}
return nn, nil
}
func (t *reportProjection) MarshalYAML() (interface{}, error) {
var err error
nn, err := makeMap(
"title", t.res.Title,
"description", t.res.Description,
"key", t.res.Key,
"kind", t.res.Kind,
"options", t.res.Options,
"elements", t.res.Elements,
"sources", t.res.Sources,
"xywh", t.res.XYWH,
"layout", t.res.Layout,
)
if err != nil {
return nil, err
}
return nn, nil
}
+204
View File
@@ -0,0 +1,204 @@
package yaml
import (
"strings"
"github.com/cortezaproject/corteza-server/pkg/envoy"
"github.com/cortezaproject/corteza-server/pkg/envoy/resource"
"github.com/cortezaproject/corteza-server/pkg/y7s"
"github.com/cortezaproject/corteza-server/system/types"
"gopkg.in/yaml.v3"
)
func (wset *reportSet) UnmarshalYAML(n *yaml.Node) error {
return y7s.Each(n, func(k, v *yaml.Node) (err error) {
var (
wrap = &report{}
)
if v == nil {
return y7s.NodeErr(n, "malformed report definition")
}
if err = v.Decode(&wrap); err != nil {
return
}
if err = decodeRef(k, "report handle", &wrap.res.Handle); err != nil {
return y7s.NodeErr(n, "Report reference must be a valid handle")
}
if wrap.res.Meta == nil {
wrap.res.Meta = &types.ReportMeta{}
}
if wrap.res.Meta.Name == "" {
// if name is not set, use handle
wrap.res.Meta.Name = wrap.res.Handle
}
*wset = append(*wset, wrap)
return
})
}
func (wrap *report) UnmarshalYAML(n *yaml.Node) (err error) {
if wrap.res == nil {
wrap.rbac = make(rbacRuleSet, 0, 10)
wrap.res = &types.Report{}
}
if wrap.rbac, err = decodeRbac(n); err != nil {
return
}
if wrap.locale, err = decodeLocale(n); err != nil {
return
}
if wrap.envoyConfig, err = decodeEnvoyConfig(n); err != nil {
return
}
if wrap.ts, err = decodeTimestamps(n); err != nil {
return
}
if wrap.us, err = decodeUserstamps(n); err != nil {
return
}
return y7s.EachMap(n, func(k, v *yaml.Node) (err error) {
switch strings.ToLower(k.Value) {
case "handle":
return y7s.DecodeScalar(v, "report handle", &wrap.res.Handle)
case "meta":
return v.Decode(&wrap.res.Meta)
case "sources":
wrap.sources = make(reportSourceSet, 0, 10)
err = v.Decode(&wrap.sources)
if err != nil {
return err
}
case "projections":
wrap.projections = make(reportProjectionSet, 0, 10)
err = v.Decode(&wrap.projections)
if err != nil {
return err
}
}
return nil
})
}
func (wrap *reportSource) UnmarshalYAML(n *yaml.Node) (err error) {
if wrap.res == nil {
wrap.res = &types.ReportDataSource{}
}
if wrap.envoyConfig, err = decodeEnvoyConfig(n); err != nil {
return
}
return y7s.EachMap(n, func(k, v *yaml.Node) (err error) {
switch k.Value {
case "meta":
return v.Decode(&wrap.res.Meta)
case "step":
return v.Decode(&wrap.res.Step)
}
return nil
})
}
func (wrap *reportProjection) UnmarshalYAML(n *yaml.Node) (err error) {
if wrap.res == nil {
wrap.res = &types.ReportProjection{}
}
if wrap.envoyConfig, err = decodeEnvoyConfig(n); err != nil {
return
}
if wrap.locale, err = decodeLocale(n); err != nil {
return
}
return y7s.EachMap(n, func(k, v *yaml.Node) (err error) {
switch strings.ToLower(k.Value) {
case "title":
return y7s.DecodeScalar(v, "title", &wrap.res.Title)
case "description":
return y7s.DecodeScalar(v, "description", &wrap.res.Description)
case "key":
return y7s.DecodeScalar(v, "key", &wrap.res.Key)
case "kind":
return y7s.DecodeScalar(v, "kind", &wrap.res.Kind)
case "options":
return v.Decode(&wrap.res.Options)
case "elements":
return v.Decode(&wrap.res.Elements)
case "sources":
return v.Decode(&wrap.res.Sources)
case "xywh":
return v.Decode(&wrap.res.XYWH)
case "layout":
return y7s.DecodeScalar(v, "layout", &wrap.res.Layout)
}
return nil
})
}
func (wset reportSet) MarshalEnvoy() ([]resource.Interface, error) {
nn := make([]resource.Interface, 0, len(wset)*2)
for _, res := range wset {
if tmp, err := res.MarshalEnvoy(); err != nil {
return nil, err
} else {
nn = append(nn, tmp...)
}
}
return nn, nil
}
func (wrap report) MarshalEnvoy() ([]resource.Interface, error) {
rs := resource.NewReport(wrap.res)
rs.SetTimestamps(wrap.ts)
rs.SetUserstamps(wrap.us)
rs.SetConfig(wrap.envoyConfig)
// default report translations
// includes translations for nested resources also
var defaultReportTranslations []resource.Interface
dft, err := rs.EncodeTranslations()
if err != nil {
return nil, err
}
for _, d := range dft {
d.MarkDefault()
defaultReportTranslations = append(defaultReportTranslations, d)
}
for _, s := range wrap.sources {
rs.AddReportSource(s.res)
}
for _, p := range wrap.projections {
rs.AddReportProjection(p.res)
}
return envoy.CollectNodes(
rs,
defaultReportTranslations,
wrap.rbac.bindResource(rs),
)
}
-1
View File
@@ -46,7 +46,6 @@ func resourceTranslationFromResource(r *resource.ResourceTranslation, cfg *Encod
if len(r.Res) == 0 {
return nil
}
return &resourceTranslation{
locales: r.Res,
@@ -10,6 +10,7 @@ import (
composeTypes "github.com/cortezaproject/corteza-server/compose/types"
"github.com/cortezaproject/corteza-server/pkg/envoy"
"github.com/cortezaproject/corteza-server/pkg/envoy/resource"
systemTypes "github.com/cortezaproject/corteza-server/system/types"
"golang.org/x/text/language"
)
@@ -192,6 +193,17 @@ func (r *resourceTranslation) makeResourceTranslationResource(state *envoy.Resou
// return fmt.Sprintf(automationTypes.WorkflowResourceTranslationTpl(), automationTypes.WorkflowResourceTranslationType, p0ID), nil
case systemTypes.ReportResourceType:
if res.RefRes != nil {
p0 := resource.FindReport(state.ParentResources, res.RefRes.Identifiers)
if p0 == nil {
return "", resource.ReportErrUnresolved(res.RefRes.Identifiers)
}
p0ID = p0.Handle
}
return fmt.Sprintf(systemTypes.ReportResourceTranslationTpl(), systemTypes.ReportResourceTranslationType, p0ID), nil
default:
return "", fmt.Errorf("unsupported resource type '%s' for locale resource YAML encode", r.refLocaleRes.ResourceType)
}
+18
View File
@@ -0,0 +1,18 @@
package minions
import "reflect"
// IsNil checks if the given interface is truly nil
//
// Due to how interfaces are handled under-the-hood, a simple i == nil may
// not always be ok.
func IsNil(i interface{}) bool {
if i == nil {
return true
}
switch reflect.TypeOf(i).Kind() {
case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice:
return reflect.ValueOf(i).IsNil()
}
return false
}
+3
View File
@@ -106,6 +106,9 @@ func truncateStore(ctx context.Context, s store.Storer, t *testing.T) {
s.TruncateUsers(ctx),
s.TruncateTemplates(ctx),
s.TruncateApplications(ctx),
s.TruncateApigwRoutes(ctx),
s.TruncateApigwFilters(ctx),
s.TruncateReports(ctx),
s.TruncateSettings(ctx),
s.TruncateRbacRules(ctx),
+141
View File
@@ -0,0 +1,141 @@
package envoy
import (
"context"
"testing"
"github.com/cortezaproject/corteza-server/pkg/auth"
"github.com/cortezaproject/corteza-server/pkg/envoy"
"github.com/cortezaproject/corteza-server/pkg/envoy/resource"
su "github.com/cortezaproject/corteza-server/pkg/envoy/store"
"github.com/cortezaproject/corteza-server/pkg/envoy/yaml"
"github.com/cortezaproject/corteza-server/store"
"github.com/cortezaproject/corteza-server/system/types"
"github.com/stretchr/testify/require"
)
func TestStoreYaml_APIGateway(t *testing.T) {
type (
tc struct {
name string
// Before the data gets processed
pre func(ctx context.Context, s store.Storer) (error, *su.DecodeFilter)
// After the data gets processed
postStoreDecode func(req *require.Assertions, err error)
postYamlEncode func(req *require.Assertions, err error)
postStoreEncode func(req *require.Assertions, err error)
// Data assertions
check func(ctx context.Context, s store.Storer, req *require.Assertions)
}
)
ctx := context.Background()
s := initServices(ctx, t)
ctx = auth.SetIdentityToContext(ctx, auth.ServiceUser())
ni := uint64(10)
su.NextID = func() uint64 {
ni++
return ni
}
cases := []*tc{
{
name: "base",
pre: func(ctx context.Context, s store.Storer) (error, *su.DecodeFilter) {
gwr := sTestAPIGatewayRoute(ctx, t, s, "test")
_ = sTestAPIGatewayFilter(ctx, t, s, gwr.ID, "test")
df := su.NewDecodeFilter().
APIGWRoutes(&types.ApigwRouteFilter{Enabled: true})
return nil, df
},
check: func(ctx context.Context, s store.Storer, req *require.Assertions) {
rr, _, err := store.SearchApigwRoutes(ctx, s, types.ApigwRouteFilter{})
req.NoError(err)
req.Len(rr, 1)
r := rr[0]
req.Equal("/testing/test", r.Endpoint)
req.Equal("POST", r.Method)
req.True(r.Enabled)
req.True(r.Meta.Debug)
req.True(r.Meta.Async)
ff, _, err := store.SearchApigwFilters(ctx, s, types.ApigwFilterFilter{RouteID: rr[0].ID})
req.NoError(err)
req.Len(ff, 1)
f := ff[0]
req.Equal(r.ID, f.Route)
req.Equal("test_ref", f.Ref)
req.Equal("test_kind", f.Kind)
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
req := require.New(t)
truncateStore(ctx, s, t)
err, df := c.pre(ctx, s)
if err != nil {
t.Fatal(err.Error())
}
// Decode from store
sd := su.Decoder()
nn, err := sd.Decode(ctx, s, df)
if c.postStoreDecode != nil {
c.postStoreDecode(req, err)
} else {
req.NoError(err)
}
// Encode into YAML
ye := yaml.NewYamlEncoder(&yaml.EncoderConfig{})
bld := envoy.NewBuilder(ye)
g, err := bld.Build(ctx, nn...)
req.NoError(err)
err = envoy.Encode(ctx, g, ye)
ss := ye.Stream()
if c.postYamlEncode != nil {
c.postYamlEncode(req, err)
} else {
req.NoError(err)
}
// Cleanup the store
truncateStore(ctx, s, t)
// Encode back into store
se := su.NewStoreEncoder(s, &su.EncoderConfig{})
yd := yaml.Decoder()
nn = make([]resource.Interface, 0, len(nn))
for _, s := range ss {
mm, err := yd.Decode(ctx, s.Source, nil)
req.NoError(err)
nn = append(nn, mm...)
}
bld = envoy.NewBuilder(se)
g, err = bld.Build(ctx, nn...)
req.NoError(err)
err = envoy.Encode(ctx, g, se)
if c.postStoreEncode != nil {
c.postStoreEncode(req, err)
} else {
req.NoError(err)
}
// Assert
c.check(ctx, s, req)
// Cleanup the store
truncateStore(ctx, s, t)
})
ni = 0
truncateStore(ctx, s, t)
}
}
+70
View File
@@ -12,6 +12,7 @@ import (
su "github.com/cortezaproject/corteza-server/pkg/envoy/store"
"github.com/cortezaproject/corteza-server/pkg/envoy/yaml"
"github.com/cortezaproject/corteza-server/store"
systemTypes "github.com/cortezaproject/corteza-server/system/types"
"github.com/stretchr/testify/require"
)
@@ -41,6 +42,75 @@ func TestStoreYaml_moduleFieldRefs(t *testing.T) {
}
cases := []*tc{
{
name: "user field; role filter",
pre: func(ctx context.Context, s store.Storer) (error, *su.DecodeFilter) {
ns := sTestComposeNamespace(ctx, t, s, "base")
rl := sTestRole(ctx, t, s, "base")
modID := su.NextID()
mod := &types.Module{
ID: modID,
Name: "usr_rl",
Handle: "usr_rl",
NamespaceID: ns.ID,
Fields: types.ModuleFieldSet{
&types.ModuleField{
ID: su.NextID(),
ModuleID: modID,
Kind: "User",
Place: 0,
Name: "usr_rl",
Options: types.ModuleFieldOptions{
"roles": []string{strconv.FormatUint(rl.ID, 10)},
},
},
},
}
err := store.CreateComposeModule(ctx, s, mod)
if err != nil {
t.Fatal(err)
}
err = store.CreateComposeModuleField(ctx, s, mod.Fields...)
if err != nil {
t.Fatal(err)
}
df := su.NewDecodeFilter().
ComposeNamespace(&types.NamespaceFilter{
Slug: "base_namespace",
}).
Roles(&systemTypes.RoleFilter{}).
ComposeModule(&types.ModuleFilter{
NamespaceID: ns.ID,
})
return nil, df
},
check: func(ctx context.Context, s store.Storer, req *require.Assertions) {
n, err := store.LookupComposeNamespaceBySlug(ctx, s, "base_namespace")
req.NoError(err)
mod, err := store.LookupComposeModuleByNamespaceIDHandle(ctx, s, n.ID, "usr_rl")
req.NoError(err)
role, err := store.LookupRoleByHandle(ctx, s, "base_role")
req.NoError(err)
req.NotNil(role)
mff, _, err := store.SearchComposeModuleFields(ctx, s, types.ModuleFieldFilter{
ModuleID: []uint64{mod.ID},
})
req.NoError(err)
// Check module relations for Options.module variant
f := mff.FindByName("usr_rl")
req.NotNil(f)
rr := f.Options["roles"].([]interface{})
req.Len(rr, 1)
req.Equal(strconv.FormatUint(role.ID, 10), rr[0])
},
},
{
name: "external module ref",
pre: func(ctx context.Context, s store.Storer) (error, *su.DecodeFilter) {
+66
View File
@@ -112,6 +112,72 @@ func TestStoreYaml_pageRefs(t *testing.T) {
},
},
{
name: "pageblock comment",
pre: func(ctx context.Context, s store.Storer) (error, *su.DecodeFilter) {
ns := sTestComposeNamespace(ctx, t, s, "base")
mod := sTestComposeModule(ctx, t, s, ns.ID, "base")
pg := &types.Page{
ID: su.NextID(),
NamespaceID: ns.ID,
Handle: "page",
Title: "page",
Blocks: types.PageBlocks{
{
Title: "comment_1",
Kind: "Comment",
Options: map[string]interface{}{
"module": strconv.FormatUint(mod.ID, 10),
},
},
{
Title: "comment_2",
Kind: "Comment",
Options: map[string]interface{}{
"moduleID": strconv.FormatUint(mod.ID, 10),
},
},
},
}
err := store.CreateComposePage(ctx, s, pg)
if err != nil {
t.Fatal(err)
}
df := su.NewDecodeFilter().
ComposeNamespace(&types.NamespaceFilter{
Slug: "base_namespace",
}).
ComposeModule(&types.ModuleFilter{
NamespaceID: ns.ID,
}).
ComposePage(&types.PageFilter{
NamespaceID: ns.ID,
})
return nil, df
},
check: func(ctx context.Context, s store.Storer, req *require.Assertions) {
n, err := store.LookupComposeNamespaceBySlug(ctx, s, "base_namespace")
req.NoError(err)
mod, err := store.LookupComposeModuleByNamespaceIDHandle(ctx, s, n.ID, "base_module")
req.NoError(err)
pg, err := store.LookupComposePageByNamespaceIDHandle(ctx, s, n.ID, "page")
req.NoError(err)
req.Len(pg.Blocks, 2)
// provided as module
b := pg.Blocks[0]
req.Equal(strconv.FormatUint(mod.ID, 10), b.Options["moduleID"])
// provided as moduleID
b = pg.Blocks[1]
req.Equal(strconv.FormatUint(mod.ID, 10), b.Options["moduleID"])
},
},
{
name: "pageblock; automation",
pre: func(ctx context.Context, s store.Storer) (error, *su.DecodeFilter) {
+129
View File
@@ -0,0 +1,129 @@
package envoy
import (
"context"
"testing"
"github.com/cortezaproject/corteza-server/pkg/auth"
"github.com/cortezaproject/corteza-server/pkg/envoy"
"github.com/cortezaproject/corteza-server/pkg/envoy/resource"
su "github.com/cortezaproject/corteza-server/pkg/envoy/store"
"github.com/cortezaproject/corteza-server/pkg/envoy/yaml"
"github.com/cortezaproject/corteza-server/store"
"github.com/cortezaproject/corteza-server/system/types"
"github.com/stretchr/testify/require"
)
func TestStoreYaml_reports(t *testing.T) {
type (
tc struct {
name string
// Before the data gets processed
pre func(ctx context.Context, s store.Storer) (error, *su.DecodeFilter)
// After the data gets processed
postStoreDecode func(req *require.Assertions, err error)
postYamlEncode func(req *require.Assertions, err error)
postStoreEncode func(req *require.Assertions, err error)
// Data assertions
check func(ctx context.Context, s store.Storer, req *require.Assertions)
}
)
ctx := context.Background()
s := initServices(ctx, t)
ctx = auth.SetIdentityToContext(ctx, auth.ServiceUser())
ni := uint64(10)
su.NextID = func() uint64 {
ni++
return ni
}
cases := []*tc{
{
name: "base",
pre: func(ctx context.Context, s store.Storer) (error, *su.DecodeFilter) {
sTestReport(ctx, t, s, "test")
df := su.NewDecodeFilter().
Reports(&types.ReportFilter{})
return nil, df
},
check: func(ctx context.Context, s store.Storer, req *require.Assertions) {
rr, _, err := store.SearchReports(ctx, s, types.ReportFilter{})
req.NoError(err)
req.Len(rr, 1)
r := rr[0]
req.Equal("test_report", r.Handle)
req.Equal("test report", r.Meta.Name)
req.Equal("testing", r.Meta.Description)
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
req := require.New(t)
truncateStore(ctx, s, t)
err, df := c.pre(ctx, s)
if err != nil {
t.Fatal(err.Error())
}
// Decode from store
sd := su.Decoder()
nn, err := sd.Decode(ctx, s, df)
if c.postStoreDecode != nil {
c.postStoreDecode(req, err)
} else {
req.NoError(err)
}
// Encode into YAML
ye := yaml.NewYamlEncoder(&yaml.EncoderConfig{})
bld := envoy.NewBuilder(ye)
g, err := bld.Build(ctx, nn...)
req.NoError(err)
err = envoy.Encode(ctx, g, ye)
ss := ye.Stream()
if c.postYamlEncode != nil {
c.postYamlEncode(req, err)
} else {
req.NoError(err)
}
// Cleanup the store
truncateStore(ctx, s, t)
// Encode back into store
se := su.NewStoreEncoder(s, &su.EncoderConfig{})
yd := yaml.Decoder()
nn = make([]resource.Interface, 0, len(nn))
for _, s := range ss {
mm, err := yd.Decode(ctx, s.Source, nil)
req.NoError(err)
nn = append(nn, mm...)
}
bld = envoy.NewBuilder(se)
g, err = bld.Build(ctx, nn...)
req.NoError(err)
err = envoy.Encode(ctx, g, se)
if c.postStoreEncode != nil {
c.postStoreEncode(req, err)
} else {
req.NoError(err)
}
// Assert
c.check(ctx, s, req)
// Cleanup the store
truncateStore(ctx, s, t)
})
ni = 0
truncateStore(ctx, s, t)
}
}
+86
View File
@@ -6,6 +6,7 @@ import (
su "github.com/cortezaproject/corteza-server/pkg/envoy/store"
"github.com/cortezaproject/corteza-server/pkg/rbac"
"github.com/cortezaproject/corteza-server/pkg/report"
"github.com/cortezaproject/corteza-server/store"
"github.com/cortezaproject/corteza-server/system/types"
)
@@ -161,3 +162,88 @@ func sTestRbac(ctx context.Context, t *testing.T, s store.Storer, roleID uint64)
return rr
}
func sTestReport(ctx context.Context, t *testing.T, s store.Storer, pfx string) *types.Report {
r := &types.Report{
ID: su.NextID(),
Handle: pfx + "_report",
Meta: &types.ReportMeta{Name: pfx + " report", Description: "testing"},
Sources: types.ReportDataSourceSet{{
Meta: map[string]interface{}{"key1": "value1"},
Step: &report.StepDefinition{
Kind: "Load",
Load: &report.LoadStepDefinition{
Name: "Test",
Source: "test",
Definition: map[string]interface{}{
"k1": "v1",
},
Columns: report.FrameColumnSet{{Name: "col1", Label: "col1 label"}},
},
},
}},
Projections: types.ReportProjectionSet{{
Title: "title",
Description: "description",
Key: "key",
Kind: "kind",
Options: map[string]interface{}{
"k1": "v1",
},
Elements: []interface{}{
map[string]interface{}{"k1": "v1"},
},
XYWH: [4]int{1, 2, 3, 4},
Layout: "layout",
}},
}
err := store.CreateReport(ctx, s, r)
if err != nil {
t.Fatal(err)
}
return r
}
func sTestAPIGatewayRoute(ctx context.Context, t *testing.T, s store.Storer, r string) *types.ApigwRoute {
gwr := &types.ApigwRoute{
ID: su.NextID(),
Endpoint: "/testing/" + r,
Method: "POST",
Enabled: true,
Group: 0,
Meta: types.ApigwRouteMeta{
Debug: true,
Async: true,
},
}
err := store.CreateApigwRoute(ctx, s, gwr)
if err != nil {
t.Fatal(err)
}
return gwr
}
func sTestAPIGatewayFilter(ctx context.Context, t *testing.T, s store.Storer, routeID uint64, pfx string) *types.ApigwFilter {
gwf := &types.ApigwFilter{
ID: su.NextID(),
Route: routeID,
Weight: 0,
Ref: pfx + "_ref",
Kind: pfx + "_kind",
Params: map[string]interface{}{
"param1": "value1",
},
}
err := store.CreateApigwFilter(ctx, s, gwf)
if err != nil {
t.Fatal(err)
}
return gwf
}