diff --git a/pkg/envoy/resource/api_gateway.go b/pkg/envoy/resource/api_gateway.go new file mode 100644 index 000000000..4fe5a6a5a --- /dev/null +++ b/pkg/envoy/resource/api_gateway.go @@ -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()) +} diff --git a/pkg/envoy/resource/compose_module.go b/pkg/envoy/resource/compose_module.go index 3da7c7c76..d1917a076 100644 --- a/pkg/envoy/resource/compose_module.go +++ b/pkg/envoy/resource/compose_module.go @@ -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 +} diff --git a/pkg/envoy/resource/compose_page.go b/pkg/envoy/resource/compose_page.go index a803f7d08..102dcce0a 100644 --- a/pkg/envoy/resource/compose_page.go +++ b/pkg/envoy/resource/compose_page.go @@ -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) + } } } diff --git a/pkg/envoy/store/api_gateway.go b/pkg/envoy/store/api_gateway.go new file mode 100644 index 000000000..ae7de497a --- /dev/null +++ b/pkg/envoy/store/api_gateway.go @@ -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 +} diff --git a/pkg/envoy/store/api_gateway_marshal.go b/pkg/envoy/store/api_gateway_marshal.go new file mode 100644 index 000000000..112ad68f2 --- /dev/null +++ b/pkg/envoy/store/api_gateway_marshal.go @@ -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...) +} diff --git a/pkg/envoy/store/api_gateway_unmarshal.go b/pkg/envoy/store/api_gateway_unmarshal.go new file mode 100644 index 000000000..fd756c1e1 --- /dev/null +++ b/pkg/envoy/store/api_gateway_unmarshal.go @@ -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, + ) +} diff --git a/pkg/envoy/store/compose_module.go b/pkg/envoy/store/compose_module.go index 0fcd9475e..72467da55 100644 --- a/pkg/envoy/store/compose_module.go +++ b/pkg/envoy/store/compose_module.go @@ -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()) +} diff --git a/pkg/envoy/store/compose_module_marshal.go b/pkg/envoy/store/compose_module_marshal.go index 69ebaf0d5..52907c04c 100644 --- a/pkg/envoy/store/compose_module_marshal.go +++ b/pkg/envoy/store/compose_module_marshal.go @@ -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 diff --git a/pkg/envoy/store/compose_page_marshal.go b/pkg/envoy/store/compose_page_marshal.go index b9ed1ea54..f2bfa0b80 100644 --- a/pkg/envoy/store/compose_page_marshal.go +++ b/pkg/envoy/store/compose_page_marshal.go @@ -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") } } diff --git a/pkg/envoy/store/decoder.go b/pkg/envoy/store/decoder.go index f9a1518d3..db41e2e00 100644 --- a/pkg/envoy/store/decoder.go +++ b/pkg/envoy/store/decoder.go @@ -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), diff --git a/pkg/envoy/store/encoder.go b/pkg/envoy/store/encoder.go index 758bd6d11..a85f22022 100644 --- a/pkg/envoy/store/encoder.go +++ b/pkg/envoy/store/encoder.go @@ -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: diff --git a/pkg/envoy/store/report.go b/pkg/envoy/store/report.go new file mode 100644 index 000000000..b0da4f55f --- /dev/null +++ b/pkg/envoy/store/report.go @@ -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 +} diff --git a/pkg/envoy/store/report_marshal.go b/pkg/envoy/store/report_marshal.go new file mode 100644 index 000000000..f11b40867 --- /dev/null +++ b/pkg/envoy/store/report_marshal.go @@ -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 +} diff --git a/pkg/envoy/store/report_unmarshal.go b/pkg/envoy/store/report_unmarshal.go new file mode 100644 index 000000000..ba004f42c --- /dev/null +++ b/pkg/envoy/store/report_unmarshal.go @@ -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, + ) +} diff --git a/pkg/envoy/store/resource_translation_marshal.go b/pkg/envoy/store/resource_translation_marshal.go index 69088b449..663fc44c4 100644 --- a/pkg/envoy/store/resource_translation_marshal.go +++ b/pkg/envoy/store/resource_translation_marshal.go @@ -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. diff --git a/pkg/envoy/store/system.go b/pkg/envoy/store/system.go index 4f6c93448..49d447a34 100644 --- a/pkg/envoy/store/system.go +++ b/pkg/envoy/store/system.go @@ -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 { diff --git a/pkg/envoy/yaml/api_gateway.go b/pkg/envoy/yaml/api_gateway.go new file mode 100644 index 000000000..45ee57951 --- /dev/null +++ b/pkg/envoy/yaml/api_gateway.go @@ -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 + } +} diff --git a/pkg/envoy/yaml/api_gateway_marshal.go b/pkg/envoy/yaml/api_gateway_marshal.go new file mode 100644 index 000000000..741bee93a --- /dev/null +++ b/pkg/envoy/yaml/api_gateway_marshal.go @@ -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 +} diff --git a/pkg/envoy/yaml/api_gateway_unmarshal.go b/pkg/envoy/yaml/api_gateway_unmarshal.go new file mode 100644 index 000000000..f499058ed --- /dev/null +++ b/pkg/envoy/yaml/api_gateway_unmarshal.go @@ -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), + ) +} diff --git a/pkg/envoy/yaml/compose_module.go b/pkg/envoy/yaml/compose_module.go index 342040d77..5b114a17d 100644 --- a/pkg/envoy/yaml/compose_module.go +++ b/pkg/envoy/yaml/compose_module.go @@ -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 diff --git a/pkg/envoy/yaml/compose_module_marshal.go b/pkg/envoy/yaml/compose_module_marshal.go index 2b650beda..3d0c2d7dc 100644 --- a/pkg/envoy/yaml/compose_module_marshal.go +++ b/pkg/envoy/yaml/compose_module_marshal.go @@ -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 { diff --git a/pkg/envoy/yaml/compose_page_marshal.go b/pkg/envoy/yaml/compose_page_marshal.go index a3b16d21a..c7fcc6acf 100644 --- a/pkg/envoy/yaml/compose_page_marshal.go +++ b/pkg/envoy/yaml/compose_page_marshal.go @@ -228,6 +228,11 @@ func (c *composePageBlock) MarshalYAML() (interface{}, error) { } break + case "Comment": + opt["moduleID"] = c.refMod[0] + delete(opt, "module") + break + } return makeMap( diff --git a/pkg/envoy/yaml/document.go b/pkg/envoy/yaml/document.go index 176bf41e8..8b090fac4 100644 --- a/pkg/envoy/yaml/document.go +++ b/pkg/envoy/yaml/document.go @@ -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) diff --git a/pkg/envoy/yaml/encoder.go b/pkg/envoy/yaml/encoder.go index 412c1291f..552722d2e 100644 --- a/pkg/envoy/yaml/encoder.go +++ b/pkg/envoy/yaml/encoder.go @@ -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: diff --git a/pkg/envoy/yaml/report.go b/pkg/envoy/yaml/report.go new file mode 100644 index 000000000..13dcb41e7 --- /dev/null +++ b/pkg/envoy/yaml/report.go @@ -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 + } +} diff --git a/pkg/envoy/yaml/report_marshal.go b/pkg/envoy/yaml/report_marshal.go new file mode 100644 index 000000000..33fe540d8 --- /dev/null +++ b/pkg/envoy/yaml/report_marshal.go @@ -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 +} diff --git a/pkg/envoy/yaml/report_unmarshal.go b/pkg/envoy/yaml/report_unmarshal.go new file mode 100644 index 000000000..eeb3875cc --- /dev/null +++ b/pkg/envoy/yaml/report_unmarshal.go @@ -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), + ) +} diff --git a/pkg/envoy/yaml/resource_translation.go b/pkg/envoy/yaml/resource_translation.go index 8e835a4a4..7124be6ac 100644 --- a/pkg/envoy/yaml/resource_translation.go +++ b/pkg/envoy/yaml/resource_translation.go @@ -46,7 +46,6 @@ func resourceTranslationFromResource(r *resource.ResourceTranslation, cfg *Encod if len(r.Res) == 0 { return nil } - return &resourceTranslation{ locales: r.Res, diff --git a/pkg/envoy/yaml/resource_translation_marshal.go b/pkg/envoy/yaml/resource_translation_marshal.go index 981c5bc1e..61f9719af 100644 --- a/pkg/envoy/yaml/resource_translation_marshal.go +++ b/pkg/envoy/yaml/resource_translation_marshal.go @@ -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) } diff --git a/pkg/minions/interface.go b/pkg/minions/interface.go new file mode 100644 index 000000000..3945cde90 --- /dev/null +++ b/pkg/minions/interface.go @@ -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 +} diff --git a/tests/envoy/main.go b/tests/envoy/main.go index b8271b88b..5f87b3c36 100644 --- a/tests/envoy/main.go +++ b/tests/envoy/main.go @@ -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), diff --git a/tests/envoy/store_yaml_api_gateway_test.go b/tests/envoy/store_yaml_api_gateway_test.go new file mode 100644 index 000000000..60b1c5ecb --- /dev/null +++ b/tests/envoy/store_yaml_api_gateway_test.go @@ -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) + } +} diff --git a/tests/envoy/store_yaml_modules_test.go b/tests/envoy/store_yaml_modules_test.go index c16daaf37..4103897bd 100644 --- a/tests/envoy/store_yaml_modules_test.go +++ b/tests/envoy/store_yaml_modules_test.go @@ -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) { diff --git a/tests/envoy/store_yaml_pages_test.go b/tests/envoy/store_yaml_pages_test.go index 5737ac498..4687cb1b6 100644 --- a/tests/envoy/store_yaml_pages_test.go +++ b/tests/envoy/store_yaml_pages_test.go @@ -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) { diff --git a/tests/envoy/store_yaml_reports_test.go b/tests/envoy/store_yaml_reports_test.go new file mode 100644 index 000000000..8420197cd --- /dev/null +++ b/tests/envoy/store_yaml_reports_test.go @@ -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) + } +} diff --git a/tests/envoy/system.go b/tests/envoy/system.go index 81c601a4a..4013717c7 100644 --- a/tests/envoy/system.go +++ b/tests/envoy/system.go @@ -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 +}