3
0

Automation script exp/imp, tweaking compose exporters

This commit is contained in:
Denis Arh
2019-09-25 12:48:59 +02:00
parent 1f4e0bb06a
commit 9c37d6d4e2
14 changed files with 772 additions and 342 deletions

View File

@@ -17,7 +17,9 @@ import (
"github.com/cortezaproject/corteza-server/compose/types"
"github.com/cortezaproject/corteza-server/internal/auth"
"github.com/cortezaproject/corteza-server/internal/permissions"
"github.com/cortezaproject/corteza-server/pkg/automation"
"github.com/cortezaproject/corteza-server/pkg/cli"
"github.com/cortezaproject/corteza-server/pkg/deinterfacer"
"github.com/cortezaproject/corteza-server/pkg/handle"
sysTypes "github.com/cortezaproject/corteza-server/system/types"
)
@@ -68,6 +70,16 @@ func Exporter(ctx context.Context, c *cli.Config) *cobra.Command {
charts, _, err := service.DefaultChart.Find(types.ChartFilter{NamespaceID: ns.ID})
cli.HandleError(err)
scripts, _, err := service.DefaultInternalAutomationManager.FindScripts(ctx, automation.ScriptFilter{})
cli.HandleError(err)
triggers, _, err := service.DefaultInternalAutomationManager.FindTriggers(ctx, automation.TriggerFilter{})
cli.HandleError(err)
scripts, _ = scripts.Filter(func(script *automation.Script) (b bool, e error) {
return script.NamespaceID == ns.ID, nil
})
y := yaml.NewEncoder(cmd.OutOrStdout())
nsOut.Name = ns.Name
@@ -85,7 +97,9 @@ func Exporter(ctx context.Context, c *cli.Config) *cobra.Command {
case "chart", "charts":
nsOut.Charts = expCharts(charts, modules)
case "page", "pages":
nsOut.Pages = expPages(0, pages, modules, charts)
nsOut.Pages = expPages(0, pages, modules, charts, scripts)
case "scripts", "triggers", "automation":
nsOut.Scripts = expAutomation(scripts, triggers, modules)
case "allow", "deny", "permission", "permissions":
out.Allow = expServicePermissions(permissions.Allow)
out.Deny = expServicePermissions(permissions.Deny)
@@ -124,6 +138,7 @@ type (
Modules map[string]Module `yaml:",omitempty"`
Pages map[string]Page `yaml:",omitempty"`
Charts map[string]Chart `yaml:",omitempty"`
Scripts map[string]Script `yaml:",omitempty"`
Allow map[string][]string `yaml:",omitempty"`
Deny map[string][]string `yaml:",omitempty"`
@@ -178,17 +193,36 @@ type (
Deny map[string][]string `yaml:",omitempty"`
}
ChartConfig struct {
Reports []*ChartConfigReport
Script struct {
Source string `yaml:"source"`
Async bool `yaml:"async"`
RunInUA bool `yaml:"runInUA"`
Critical bool `yaml:"critical"`
Enabled bool `yaml:"enabled"`
Timeout uint `yaml:"timeout"`
Triggers []map[string]interface{} `yaml:"triggers,omitempty"`
Allow map[string][]string `yaml:",omitempty"`
Deny map[string][]string `yaml:",omitempty"`
}
ChartConfigReport struct {
types.ChartConfigReport
Module string `json:"module"`
ChartConfig struct {
Reports []map[string]interface{}
}
)
var (
// preloaded roles so we can
//
roles sysTypes.RoleSet
// list of used page handles
// we're exporting pages in a tree structure
// so we need this to know if we've used a handle before
//
// non-autogenerated handles should not have this problem
pagesHandles = make(map[string]bool)
)
func expModules(mm types.ModuleSet) (o map[string]Module) {
@@ -310,7 +344,7 @@ func expModuleFieldOptions(f *types.ModuleField, modules types.ModuleSet) types.
return out
}
func expPages(parentID uint64, pages types.PageSet, modules types.ModuleSet, charts types.ChartSet) (o map[string]Page) {
func expPages(parentID uint64, pages types.PageSet, modules types.ModuleSet, charts types.ChartSet, scripts automation.ScriptSet) (o map[string]Page) {
var (
children = pages.FindByParent(parentID)
handle string
@@ -321,8 +355,8 @@ func expPages(parentID uint64, pages types.PageSet, modules types.ModuleSet, cha
page := Page{
Title: child.Title,
Description: child.Description,
Blocks: expPageBlocks(child.Blocks, pages, modules, charts),
Pages: expPages(child.ID, pages, modules, charts),
Blocks: expPageBlocks(child.Blocks, pages, modules, charts, scripts),
Pages: expPages(child.ID, pages, modules, charts, scripts),
Visible: child.Visible,
Allow: expResourcePermissions(permissions.Allow, types.PagePermissionResource),
@@ -335,23 +369,24 @@ func expPages(parentID uint64, pages types.PageSet, modules types.ModuleSet, cha
page.Module = fmt.Sprintf("Error: module with ID %d does not exist", child.ModuleID)
} else {
page.Module = makeHandleFromName(m.Name, m.Handle, "module-%d", child.ModuleID)
if child.Handle == "" {
// Reuse module's handle for page
handle = makeHandleFromName(m.Name, m.Handle, "record-page-%d", child.ModuleID)
}
}
} else {
handle = makeHandleFromName(child.Title, child.Handle, "page-%d", child.ID)
}
handle = makeHandleFromName(child.Title, child.Handle, "page-%d", child.ID)
if handle == "" || pagesHandles[handle] {
// if handle exists, force simple handle with id
handle = makeHandleFromName("", "", "page-%d", child.ID)
}
pagesHandles[handle] = true
o[handle] = page
}
return
}
func expPageBlocks(in types.PageBlocks, pages types.PageSet, modules types.ModuleSet, charts types.ChartSet) types.PageBlocks {
func expPageBlocks(in types.PageBlocks, pages types.PageSet, modules types.ModuleSet, charts types.ChartSet, scripts automation.ScriptSet) types.PageBlocks {
out := types.PageBlocks(in)
// Remove extra options to keep the output tidy
@@ -414,6 +449,31 @@ func expPageBlocks(in types.PageBlocks, pages types.PageSet, modules types.Modul
rmFalse(out[i].Options, "hideSearch")
rmFalse(out[i].Options, "hideSorting")
rmFalse(out[i].Options, "allowExport")
if out[i].Kind == "Automation" {
bb := make([]interface{}, 0)
_ = deinterfacer.Each(out[i].Options["buttons"], func(_ int, _ string, btn interface{}) error {
button := map[string]interface{}{}
_ = deinterfacer.Each(btn, func(_ int, k string, v interface{}) error {
switch k {
case "triggerID", "scriptID":
if s := scripts.FindByID(deinterfacer.ToUint64(v)); s != nil {
button["script"] = makeHandleFromName(s.Name, "", "automation-script-%d", s.ID)
}
default:
button[k] = v
}
return nil
})
bb = append(bb, button)
return nil
})
out[i].Options["buttons"] = bb
}
}
return out
@@ -425,24 +485,30 @@ func expCharts(charts types.ChartSet, modules types.ModuleSet) (o map[string]Cha
for _, c := range charts {
chart := Chart{
Name: c.Name,
Config: ChartConfig{Reports: make([]*ChartConfigReport, len(c.Config.Reports))},
Config: ChartConfig{Reports: make([]map[string]interface{}, len(c.Config.Reports))},
Allow: expResourcePermissions(permissions.Allow, types.ChartPermissionResource),
Deny: expResourcePermissions(permissions.Deny, types.ChartPermissionResource),
}
for i, r := range c.Config.Reports {
chart.Config.Reports[i] = &ChartConfigReport{
ChartConfigReport: *r,
if len(r.Metrics) == 0 {
continue
}
rOut := map[string]interface{}{
"filter": r.Filter,
"metrics": r.Metrics,
"dimensions": r.Dimensions,
"renderer": r.Renderer,
}
if r.ModuleID > 0 {
module := modules.FindByID(r.ModuleID)
chart.Config.Reports[i].ModuleID = 0
chart.Config.Reports[i].Module =
makeHandleFromName(module.Name, module.Handle, "module-%d", module.ID)
rOut["module"] = makeHandleFromName(module.Name, module.Handle, "module-%d", module.ID)
}
chart.Config.Reports[i] = rOut
}
handle := makeHandleFromName(c.Name, c.Handle, "chart-%d", c.ID)
@@ -453,6 +519,78 @@ func expCharts(charts types.ChartSet, modules types.ModuleSet) (o map[string]Cha
return
}
func expAutomation(ss automation.ScriptSet, tt automation.TriggerSet, mm types.ModuleSet) map[string]Script {
var (
script Script
out = map[string]Script{}
)
_ = ss.Walk(func(s *automation.Script) error {
script = Script{
Source: strings.TrimSpace(s.Source),
Async: s.Async,
RunInUA: s.RunInUA,
Critical: s.Critical,
Enabled: s.Enabled,
Timeout: s.Timeout,
// ignoring run-as, we do not have support for user exporting
// this will be solved when a.scripts are migrated to syste,
Triggers: []map[string]interface{}{},
Allow: expResourcePermissions(permissions.Allow, types.AutomationScriptPermissionResource),
Deny: expResourcePermissions(permissions.Deny, types.AutomationScriptPermissionResource),
}
handle := makeHandleFromName(s.Name, "", "automation-script-%d", s.ID)
tt.Walk(func(t *automation.Trigger) error {
if t.ScriptID != s.ID {
return nil
}
trigger := map[string]interface{}{
"resource": t.Resource,
"event": t.Event,
}
switch t.Event {
case "beforeCreate", "beforeUpdate", "beforeDelete",
"afterCreate", "afterUpdate", "afterDelete":
moduleID := t.Uint64Condition()
if moduleID == 0 {
return nil
}
module := mm.FindByID(moduleID)
if module == nil {
return nil
}
trigger["module"] = makeHandleFromName(module.Name, module.Handle, "module-%d", module.ID)
case "interval", "deferred":
trigger["condition"] = t.Condition
}
if !t.Enabled {
trigger["enabled"] = false
}
script.Triggers = append(script.Triggers, trigger)
return nil
})
out[handle] = script
return nil
})
return out
}
func expServicePermissions(access permissions.Access) map[string]map[string][]string {
var (
has bool
@@ -509,9 +647,12 @@ func expResourcePermissions(access permissions.Access, resource permissions.Reso
continue
}
if rule.Access != access {
continue
}
if _, has = sp[r.Handle]; !has {
sp[r.Handle] = make([]string, 0)
}
sp[r.Handle] = append(sp[r.Handle], rule.Operation.String())

View File

@@ -35,14 +35,16 @@ func Importer(ctx context.Context, c *cli.Config) *cobra.Command {
err error
)
if namespaceID, _ := strconv.ParseUint(nsFlag, 10, 64); namespaceID > 0 {
ns, err = service.DefaultNamespace.FindByID(namespaceID)
if err != repository.ErrNamespaceNotFound {
cli.HandleError(err)
}
} else if ns, err = service.DefaultNamespace.FindByHandle(nsFlag); err != nil {
if err != repository.ErrNamespaceNotFound {
cli.HandleError(err)
if nsFlag != "" {
if namespaceID, _ := strconv.ParseUint(nsFlag, 10, 64); namespaceID > 0 {
ns, err = service.DefaultNamespace.FindByID(namespaceID)
if err != repository.ErrNamespaceNotFound {
cli.HandleError(err)
}
} else if ns, err = service.DefaultNamespace.FindByHandle(nsFlag); err != nil {
if err != repository.ErrNamespaceNotFound {
cli.HandleError(err)
}
}
}
@@ -65,19 +67,19 @@ func Importer(ctx context.Context, c *cli.Config) *cobra.Command {
service.DefaultModule.With(ctx),
service.DefaultChart.With(ctx),
service.DefaultPage.With(ctx),
service.DefaultInternalAutomationManager,
permissions.NewImporter(service.DefaultAccessControl.Whitelist()),
)
for i, f := range ff {
cmd.Printf("Importing from %s\n", args[i])
if err = yaml.NewDecoder(f).Decode(&aux); err != nil {
return
}
cli.HandleError(yaml.NewDecoder(f).Decode(&aux))
if ns != nil {
// If we're importing with --namespace switch,
// we're going to import all into one NS
cli.HandleError(imp.GetNamespaceImporter().Cast(ns.Slug, aux))
} else {
// importing one or more namespaces
@@ -92,12 +94,13 @@ func Importer(ctx context.Context, c *cli.Config) *cobra.Command {
service.DefaultModule.With(ctx),
service.DefaultChart.With(ctx),
service.DefaultPage.With(ctx),
service.DefaultInternalAutomationManager,
service.DefaultAccessControl,
))
},
}
cmd.Flags().String("namespace", "crm", "Import into namespace (by ID or string)")
cmd.Flags().String("namespace", "", "Import into namespace (by ID or string)")
return cmd
}

View File

@@ -0,0 +1,248 @@
package importer
import (
"context"
"fmt"
"strconv"
"github.com/pkg/errors"
"github.com/cortezaproject/corteza-server/compose/types"
"github.com/cortezaproject/corteza-server/pkg/automation"
"github.com/cortezaproject/corteza-server/pkg/deinterfacer"
"github.com/cortezaproject/corteza-server/pkg/importer"
)
type (
// AutomationScript provides very basic and immature automation script importer
AutomationScript struct {
imp *Importer
namespace *types.Namespace
set automation.ScriptSet
dirty map[uint64]bool
triggers map[string]automation.TriggerSet
modRefs []automationTriggerModuleRef
}
automationTriggerModuleRef struct {
// automation handle, report index, module handle
as string
ri int
mh string
}
automationFinder interface {
// automation finder does not have find by name...
FindScripts(ctx context.Context, f automation.ScriptFilter) (automation.ScriptSet, automation.ScriptFilter, error)
}
)
func NewAutomationImporter(imp *Importer, ns *types.Namespace) *AutomationScript {
out := &AutomationScript{
imp: imp,
namespace: ns,
set: automation.ScriptSet{},
triggers: make(map[string]automation.TriggerSet),
modRefs: make([]automationTriggerModuleRef, 0),
dirty: make(map[uint64]bool),
}
if imp.automationFinder != nil && ns.ID > 0 {
out.set, _, _ = imp.automationFinder.FindScripts(
context.Background(),
automation.ScriptFilter{
NamespaceID: ns.ID,
},
)
}
return out
}
func (asImp *AutomationScript) getModule(handle string) (*types.Module, error) {
if g, ok := asImp.imp.namespaces.modules[asImp.namespace.Slug]; !ok {
return nil, errors.Errorf("could not get modules %q from non existing namespace %q", handle, asImp.namespace.Slug)
} else {
return g.Get(handle)
}
}
func (asImp *AutomationScript) CastSet(set interface{}) error {
return deinterfacer.Each(set, func(index int, handle string, def interface{}) error {
if index > -1 {
// Automation scripts defined as collection
deinterfacer.KVsetString(&handle, "name", def)
}
return asImp.Cast(handle, def)
})
}
func (asImp *AutomationScript) Cast(handle string, def interface{}) (err error) {
if !deinterfacer.IsMap(def) {
return errors.New("expecting map of values for automation")
}
var script *automation.Script
if !importer.IsValidHandle(handle) {
return errors.New("invalid automation handle")
}
handle = importer.NormalizeHandle(handle)
if script, err = asImp.Get(handle); err != nil {
return err
} else if script == nil {
script = &automation.Script{
NamespaceID: asImp.namespace.ID,
Name: handle,
}
asImp.set = append(asImp.set, script)
} else if script.ID == 0 {
return errors.Errorf("automation handle %q already defined in this import session", script.Name)
}
asImp.dirty[script.ID] = true
return deinterfacer.Each(def, func(_ int, key string, val interface{}) (err error) {
switch key {
case "name":
script.Name = deinterfacer.ToString(val)
case "source":
script.Source = deinterfacer.ToString(val)
case "async":
script.Async = deinterfacer.ToBool(val, false)
case "runInUA":
script.RunInUA = deinterfacer.ToBool(val, false)
case "critical":
script.Critical = deinterfacer.ToBool(val, false)
case "enabled":
script.Enabled = deinterfacer.ToBool(val, true)
case "timeout":
script.Timeout = uint(deinterfacer.ToInt(val))
case "triggers":
asImp.triggers[handle], err = asImp.castTriggers(handle, script, val)
if err != nil {
return err
}
case "allow", "deny":
return asImp.imp.permissions.CastSet(types.AutomationScriptPermissionResource.String()+handle, key, val)
default:
return fmt.Errorf("unexpected key %q for automation %q", key, handle)
}
return
})
}
func (asImp *AutomationScript) castTriggers(handle string, script *automation.Script, def interface{}) (automation.TriggerSet, error) {
var (
t *automation.Trigger
tt = automation.TriggerSet{}
)
return tt, deinterfacer.Each(def, func(n int, _ string, def interface{}) (err error) {
t = &automation.Trigger{}
err = deinterfacer.Each(def, func(_ int, key string, val interface{}) (err error) {
switch key {
case "enabled":
t.Enabled = deinterfacer.ToBool(val, true)
case "event":
t.Condition = deinterfacer.ToString(val)
case "resource":
t.Resource = deinterfacer.ToString(val)
case "condition":
t.Condition = deinterfacer.ToString(val)
case "module":
module := deinterfacer.ToString(val)
if m, err := asImp.getModule(module); err != nil || m == nil {
return fmt.Errorf("unknown module %q referenced from automation script's %q trigger", module, handle)
}
asImp.modRefs = append(asImp.modRefs, automationTriggerModuleRef{handle, len(tt), module})
default:
return fmt.Errorf("unexpected key %q for automation script's %q trigger", key, handle)
}
return
})
if err != nil {
return
}
tt = append(tt, t)
return
})
}
// Get existing automation scripts
func (asImp *AutomationScript) Get(handle string) (*automation.Script, error) {
handle = importer.NormalizeHandle(handle)
if !importer.IsValidHandle(handle) {
return nil, errors.New("invalid automation script handle")
}
fmt.Printf(" => %s (?%v)\n", handle, asImp.set.FindByName(handle, asImp.namespace.ID) != nil)
return asImp.set.FindByName(handle, asImp.namespace.ID), nil
}
func (asImp *AutomationScript) Store(ctx context.Context, k automationScriptKeeper) (err error) {
if err = asImp.resolveRefs(); err != nil {
return
}
return asImp.set.Walk(func(s *automation.Script) (err error) {
var name = s.Name
if s.ID == 0 {
s.NamespaceID = asImp.namespace.ID
err = k.CreateScript(ctx, s)
} else if asImp.dirty[s.ID] {
err = k.UpdateScript(ctx, s)
}
if err != nil {
return errors.Wrapf(err, "could not save script %s (%d)", s.Name, s.ID)
}
asImp.dirty[s.ID] = false
asImp.imp.permissions.UpdateResources(types.AutomationScriptPermissionResource.String(), name, s.ID)
return
})
}
// Resolve all refs for this page (page module, inside block)
func (asImp *AutomationScript) resolveRefs() error {
for _, ref := range asImp.modRefs {
s := asImp.set.FindByName(ref.as, asImp.namespace.ID)
if s == nil {
return errors.Errorf("invalid reference, unknown automation script (%v)", ref)
}
if _, has := asImp.triggers[ref.as]; !has {
return errors.Errorf("invalid reference, triggers not initialized (%v)", ref)
}
if ref.ri > len(asImp.triggers[ref.as]) {
return errors.Errorf("invalid reference, trigger index out of range (%v)", ref)
}
if module, err := asImp.getModule(ref.mh); err != nil {
return errors.Errorf("invalid reference, module loading error: %v", err)
} else if module == nil {
return errors.Errorf("invalid reference, unknown module (%v)", ref)
} else {
asImp.triggers[ref.as][ref.ri].Condition = strconv.FormatUint(module.ID, 10)
}
}
return nil
}

View File

@@ -16,8 +16,8 @@ type (
imp *Importer
namespace *types.Namespace
set types.ChartSet
modRefs []chartModuleRef
dirty map[uint64]bool
modRefs []chartModuleRef
}
chartModuleRef struct {
@@ -27,17 +27,25 @@ type (
mh string
}
// @todo remove finder strategy, directly provide set of items
chartFinder interface {
FindByHandle(uint64, string) (*types.Chart, error)
Find(filter types.ChartFilter) (set types.ChartSet, f types.ChartFilter, err error)
}
)
func NewChartImporter(imp *Importer, ns *types.Namespace) *Chart {
return &Chart{
out := &Chart{
imp: imp,
namespace: ns,
set: types.ChartSet{},
dirty: make(map[uint64]bool),
}
if imp.chartFinder != nil && ns.ID > 0 {
out.set, _, _ = imp.chartFinder.Find(types.ChartFilter{NamespaceID: ns.ID})
}
return out
}
func (pImp *Chart) getModule(handle string) (*types.Module, error) {
@@ -75,18 +83,23 @@ func (cImp *Chart) Cast(handle string, def interface{}) (err error) {
}
handle = importer.NormalizeHandle(handle)
if chart, err = cImp.GetOrMake(handle); err != nil {
if chart, err = cImp.Get(handle); err != nil {
return err
} else if chart == nil {
chart = &types.Chart{
Handle: handle,
Name: handle,
}
cImp.set = append(cImp.set, chart)
} else if chart.ID == 0 {
return errors.Errorf("chart handle %q already defined in this import session", chart.Handle)
} else {
cImp.dirty[chart.ID] = true
}
return deinterfacer.Each(def, func(_ int, key string, val interface{}) (err error) {
switch key {
case "namespace":
// namespace value sanity check
if deinterfacer.ToString(val, cImp.namespace.Slug) != cImp.namespace.Slug {
return fmt.Errorf("explicitly set namespace on chart %q shadows inherited namespace", cImp.namespace.Slug)
}
case "handle":
// handle value sanity check
if deinterfacer.ToString(val, handle) != handle {
@@ -162,51 +175,6 @@ func (cImp *Chart) castConfigReports(chart *types.Chart, def interface{}) ([]*ty
})
}
func (cImp *Chart) Exists(handle string) bool {
handle = importer.NormalizeHandle(handle)
var (
chart *types.Chart
err error
)
chart = cImp.set.FindByHandle(handle)
if chart != nil {
return true
}
if cImp.namespace.ID == 0 {
// Assuming new namespace, nothing exists yet..
return false
}
if cImp.imp.chartFinder != nil {
chart, err = cImp.imp.chartFinder.FindByHandle(cImp.namespace.ID, handle)
if err == nil && chart != nil {
cImp.set = append(cImp.set, chart)
return true
}
}
return false
}
// Get finds or makes a new chart
func (cImp *Chart) GetOrMake(handle string) (chart *types.Chart, err error) {
if chart, err = cImp.Get(handle); err != nil {
return nil, err
} else if chart == nil {
chart = &types.Chart{
Handle: handle,
Name: handle,
}
cImp.set = append(cImp.set, chart)
}
return chart, nil
}
// Get existing charts
func (cImp *Chart) Get(handle string) (*types.Chart, error) {
handle = importer.NormalizeHandle(handle)
@@ -214,11 +182,7 @@ func (cImp *Chart) Get(handle string) (*types.Chart, error) {
return nil, errors.New("invalid chart handle")
}
if cImp.Exists(handle) {
return cImp.set.FindByHandle(handle), nil
} else {
return nil, nil
}
return cImp.set.FindByHandle(handle), nil
}
func (cImp *Chart) Store(ctx context.Context, k chartKeeper) (err error) {
@@ -232,15 +196,15 @@ func (cImp *Chart) Store(ctx context.Context, k chartKeeper) (err error) {
if chart.ID == 0 {
chart.NamespaceID = cImp.namespace.ID
chart, err = k.Create(chart)
} else {
} else if cImp.dirty[chart.ID] {
chart, err = k.Update(chart)
}
if err != nil {
return
}
// @todo update module ref for charts
cImp.dirty[chart.ID] = false
cImp.imp.permissions.UpdateResources(types.ChartPermissionResource.String(), handle, chart.ID)
return

View File

@@ -13,7 +13,8 @@ func TestChartImport_CastSet(t *testing.T) {
impFixTester(t, "chart_full_slice", func(t *testing.T, imp *Importer) {
req := require.New(t)
req.Len(imp.GetChartImporter("test").set, 2)
req.NotNil(imp.GetChartImporter(ns.Slug))
req.Len(imp.GetChartImporter(ns.Slug).set, 2)
})
impFixTester(t,
@@ -21,8 +22,8 @@ func TestChartImport_CastSet(t *testing.T) {
errors.New(`unknown module "un_kno_wn" referenced from chart "chart1" report config`))
// Pre fill with module that imported chart is referring to
// imp.namespaces.modules[ns.Slug] = &Module{set: []*types.Module{{Handle: "foo"}}}
modules.set = []*types.Module{{NamespaceID: ns.ID, Handle: "foo"}}
imp.namespaces.Setup(ns)
imp.GetModuleImporter(ns.Slug).set = types.ModuleSet{{NamespaceID: ns.ID, Handle: "foo"}}
impFixTester(t, "chart_full", func(t *testing.T, chart *Chart) {
req := require.New(t)

View File

@@ -6,20 +6,24 @@ import (
"github.com/pkg/errors"
"github.com/cortezaproject/corteza-server/compose/service"
"github.com/cortezaproject/corteza-server/compose/types"
"github.com/cortezaproject/corteza-server/internal/permissions"
"github.com/cortezaproject/corteza-server/pkg/automation"
"github.com/cortezaproject/corteza-server/pkg/deinterfacer"
"github.com/cortezaproject/corteza-server/pkg/importer"
sysTypes "github.com/cortezaproject/corteza-server/system/types"
)
type (
Importer struct {
namespaces *Namespace
namespaceFinder namespaceFinder
moduleFinder moduleFinder
chartFinder chartFinder
pageFinder pageFinder
namespaceFinder namespaceFinder
moduleFinder moduleFinder
chartFinder chartFinder
pageFinder pageFinder
automationFinder automationFinder
permissions importer.PermissionImporter
}
@@ -38,14 +42,20 @@ type (
Update(*types.Page) (*types.Page, error)
Create(*types.Page) (*types.Page, error)
}
automationScriptKeeper interface {
UpdateScript(context.Context, *automation.Script) error
CreateScript(context.Context, *automation.Script) error
}
)
func NewImporter(nsf namespaceFinder, mf moduleFinder, cf chartFinder, pf pageFinder, p importer.PermissionImporter) *Importer {
func NewImporter(nsf namespaceFinder, mf moduleFinder, cf chartFinder, pf pageFinder, af automationFinder, p importer.PermissionImporter) *Importer {
imp := &Importer{
namespaceFinder: nsf,
moduleFinder: mf,
chartFinder: cf,
pageFinder: pf,
namespaceFinder: nsf,
moduleFinder: mf,
chartFinder: cf,
pageFinder: pf,
automationFinder: af,
permissions: p,
}
@@ -90,12 +100,22 @@ func (imp *Importer) Cast(in interface{}) (err error) {
})
}
func (imp *Importer) Store(ctx context.Context, nsStore namespaceKeeper, mStore moduleKeeper, cStore chartKeeper, pStore pageKeeper, pk permissions.ImportKeeper) (err error) {
err = imp.namespaces.Store(ctx, nsStore, mStore, cStore, pStore)
func (imp *Importer) Store(ctx context.Context, nsStore namespaceKeeper, mStore moduleKeeper, cStore chartKeeper, pStore pageKeeper, asStore automationScriptKeeper, pk permissions.ImportKeeper) (err error) {
err = imp.namespaces.Store(ctx, nsStore, mStore, cStore, pStore, asStore)
if err != nil {
return errors.Wrap(err, "could not import namespaces")
}
// Make sure we properly replace role handles with IDs
if roles, err := service.DefaultSystemRole.Find(ctx); err != nil {
return err
} else {
roles.Walk(func(role *sysTypes.Role) error {
imp.permissions.UpdateRoles(role.Handle, role.ID)
return nil
})
}
err = imp.permissions.Store(ctx, pk)
if err != nil {
return errors.Wrap(err, "could not import permissions")

View File

@@ -8,93 +8,44 @@ import (
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v2"
"github.com/cortezaproject/corteza-server/compose/repository"
"github.com/cortezaproject/corteza-server/compose/service"
"github.com/cortezaproject/corteza-server/compose/types"
"github.com/cortezaproject/corteza-server/internal/permissions"
)
type (
namespaceMock struct{ set types.NamespaceSet }
moduleMock struct{ set types.ModuleSet }
chartMock struct{ set types.ChartSet }
pageMock struct{ set types.PageSet }
)
var (
ns = &types.Namespace{
ID: 1000000,
Name: "Test",
Slug: "test",
Slug: "testing",
Enabled: true,
}
// Add namespace to the stack, make sure importer can find it
namespaces = &namespaceMock{set: types.NamespaceSet{ns}}
modules = &moduleMock{}
charts = &chartMock{}
pages = &pageMock{}
pi *permissions.Importer
// whitelist = nil, anything can be added
pi = permissions.NewImporter(service.AccessControl(nil).Whitelist())
imp = NewImporter(namespaces, modules, charts, pages, pi)
imp *Importer
)
func TestMain(m *testing.M) {
resetMocks()
os.Exit(m.Run())
}
func (mock *namespaceMock) FindByHandle(slug string) (o *types.Namespace, err error) {
oo, err := mock.set.Filter(func(o *types.Namespace) (b bool, e error) {
return o.Slug == slug, nil
})
func resetMocks() {
// whitelist = nil, anything can be added
pi = permissions.NewImporter(service.AccessControl(nil).Whitelist())
if len(oo) > 0 {
return oo[0], nil
} else {
return nil, repository.ErrNamespaceNotFound
}
}
imp = NewImporter(nil, nil, nil, nil, nil, pi)
func (mock *moduleMock) FindByHandle(namespaceID uint64, handle string) (o *types.Module, err error) {
oo, err := mock.set.Filter(func(o *types.Module) (b bool, e error) {
return o.Handle == handle && o.NamespaceID == namespaceID, nil
})
if len(oo) > 0 {
return oo[0], nil
} else {
return nil, repository.ErrModuleNotFound
}
}
func (mock *chartMock) FindByHandle(namespaceID uint64, handle string) (o *types.Chart, err error) {
oo, err := mock.set.Filter(func(o *types.Chart) (b bool, e error) {
return o.Handle == handle && o.NamespaceID == namespaceID, nil
})
if len(oo) > 0 {
return oo[0], nil
} else {
return nil, repository.ErrChartNotFound
}
}
func (mock *pageMock) FindByHandle(namespaceID uint64, handle string) (o *types.Page, err error) {
oo, err := mock.set.Filter(func(o *types.Page) (b bool, e error) {
return o.Handle == handle && o.NamespaceID == namespaceID, nil
})
if len(oo) > 0 {
return oo[0], nil
} else {
return nil, repository.ErrPageNotFound
}
}
func impFixTester(t *testing.T, name string, tester interface{}) {
t.Run(name, func(t *testing.T) {
// We're not calling reset mocks BEFORE calling tester()
// because we want to have an option to set it up as we want
defer resetMocks()
var aux interface{}
req := require.New(t)
f, err := os.Open(fmt.Sprintf("testdata/%s.yaml", name))

View File

@@ -19,19 +19,28 @@ type (
imp *Importer
namespace *types.Namespace
set types.ModuleSet
dirty map[uint64]bool
}
// @todo remove finder strategy, directly provide set of items
moduleFinder interface {
FindByHandle(uint64, string) (*types.Module, error)
Find(filter types.ModuleFilter) (set types.ModuleSet, f types.ModuleFilter, err error)
}
)
func NewModuleImporter(imp *Importer, ns *types.Namespace) *Module {
return &Module{
out := &Module{
imp: imp,
namespace: ns,
set: types.ModuleSet{},
dirty: make(map[uint64]bool),
}
if imp.moduleFinder != nil && ns.ID > 0 {
out.set, _, _ = imp.moduleFinder.Find(types.ModuleFilter{NamespaceID: ns.ID})
}
return out
}
func (pImp *Module) getPageImporter() (*Page, error) {
@@ -69,10 +78,21 @@ func (mImp *Module) Cast(handle string, def interface{}) (err error) {
}
handle = importer.NormalizeHandle(handle)
if module, err = mImp.GetOrMake(handle); err != nil {
if module, err = mImp.Get(handle); err != nil {
return err
} else if module == nil {
module = &types.Module{
Handle: handle,
Name: handle,
}
mImp.set = append(mImp.set, module)
} else if module.ID == 0 {
return errors.Errorf("module handle %q already defined in this import session", module.Handle)
}
mImp.dirty[module.ID] = true
return deinterfacer.Each(def, func(_ int, key string, val interface{}) (err error) {
switch key {
case "namespace":
@@ -211,51 +231,6 @@ func (mImp *Module) castFieldOptions(field *types.ModuleField, def interface{})
})
}
func (mImp *Module) Exists(handle string) bool {
handle = importer.NormalizeHandle(handle)
var (
module *types.Module
err error
)
module = mImp.set.FindByHandle(handle)
if module != nil {
return true
}
if mImp.namespace.ID == 0 {
// Assuming new namespace, nothing exists yet..
return false
}
if mImp.imp.moduleFinder != nil {
module, err = mImp.imp.moduleFinder.FindByHandle(mImp.namespace.ID, handle)
if err == nil && module != nil {
mImp.set = append(mImp.set, module)
return true
}
}
return false
}
// Get finds or makes a new module
func (mImp *Module) GetOrMake(handle string) (module *types.Module, err error) {
if module, err = mImp.Get(handle); err != nil {
return nil, err
} else if module == nil {
module = &types.Module{
Handle: handle,
Name: handle,
}
mImp.set = append(mImp.set, module)
}
return module, nil
}
// Get existing modules
func (mImp *Module) Get(handle string) (*types.Module, error) {
handle = importer.NormalizeHandle(handle)
@@ -263,11 +238,7 @@ func (mImp *Module) Get(handle string) (*types.Module, error) {
return nil, errors.New("invalid module handle")
}
if mImp.Exists(handle) {
return mImp.set.FindByHandle(handle), nil
} else {
return nil, nil
}
return mImp.set.FindByHandle(handle), nil
}
func (mImp *Module) Store(ctx context.Context, k moduleKeeper) error {
@@ -281,7 +252,7 @@ func (mImp *Module) Store(ctx context.Context, k moduleKeeper) error {
if module.ID == 0 {
module.NamespaceID = mImp.namespace.ID
module, err = k.Create(module)
} else {
} else if mImp.dirty[module.ID] {
module, err = k.Update(module)
}
@@ -289,6 +260,7 @@ func (mImp *Module) Store(ctx context.Context, k moduleKeeper) error {
return
}
mImp.dirty[module.ID] = false
mImp.imp.permissions.UpdateResources(types.ModulePermissionResource.String(), handle, module.ID)
err = module.Fields.Walk(func(f *types.ModuleField) error {

View File

@@ -15,7 +15,8 @@ type (
Namespace struct {
imp *Importer
set types.NamespaceSet
set types.NamespaceSet
dirty map[uint64]bool
// modules per namespace
modules map[string]*Module
@@ -25,10 +26,14 @@ type (
// pages per namespace
pages map[string]*Page
// pages per namespace
scripts map[string]*AutomationScript
}
// @todo remove finder strategy, directly provide set of items
namespaceFinder interface {
FindByHandle(string) (*types.Namespace, error)
Find(filter types.NamespaceFilter) (set types.NamespaceSet, f types.NamespaceFilter, err error)
}
namespaceKeeper interface {
@@ -38,15 +43,23 @@ type (
)
func NewNamespaceImporter(imp *Importer) *Namespace {
return &Namespace{
out := &Namespace{
imp: imp,
set: types.NamespaceSet{},
set: types.NamespaceSet{},
dirty: make(map[uint64]bool),
modules: map[string]*Module{},
charts: map[string]*Chart{},
pages: map[string]*Page{},
scripts: map[string]*AutomationScript{},
}
if imp.namespaceFinder != nil {
out.set, _, _ = imp.namespaceFinder.Find(types.NamespaceFilter{})
}
return out
}
// CastSet resolves permission rules:
@@ -77,10 +90,23 @@ func (nsImp *Namespace) Cast(handle string, def interface{}) (err error) {
}
handle = importer.NormalizeHandle(handle)
if namespace, err = nsImp.Get(handle); err != nil {
return err
} else if namespace == nil {
namespace = &types.Namespace{
Slug: handle,
Name: handle,
Enabled: true,
}
} else if namespace.ID == 0 {
return errors.Errorf("namespace handle %q already defined in this import session", namespace.Slug)
} else {
nsImp.dirty[namespace.ID] = true
}
nsImp.Setup(namespace)
return deinterfacer.Each(def, func(_ int, key string, val interface{}) (err error) {
switch key {
case "handle", "slug":
@@ -94,6 +120,7 @@ func (nsImp *Namespace) Cast(handle string, def interface{}) (err error) {
case "meta":
namespace.Meta, err = nsImp.castMeta(namespace, val)
return
case "modules":
@@ -105,6 +132,9 @@ func (nsImp *Namespace) Cast(handle string, def interface{}) (err error) {
case "pages":
return nsImp.castPages(handle, val)
case "scripts":
return nsImp.castScripts(handle, val)
case "allow", "deny":
return nsImp.imp.permissions.CastSet(types.NamespacePermissionResource.String()+namespace.Slug, key, val)
@@ -162,28 +192,13 @@ func (nsImp *Namespace) castPages(handle string, def interface{}) error {
return nsImp.pages[handle].CastSet(def)
}
func (nsImp *Namespace) Exists(handle string) bool {
handle = importer.NormalizeHandle(handle)
func (nsImp *Namespace) castScripts(handle string, def interface{}) error {
if nsImp.scripts[handle] == nil {
return fmt.Errorf("unknown namespace %q", handle)
var (
namespace *types.Namespace
err error
)
namespace = nsImp.set.FindByHandle(handle)
if namespace != nil {
return true
}
if nsImp.imp.namespaceFinder != nil {
namespace, err = nsImp.imp.namespaceFinder.FindByHandle(handle)
if err == nil && namespace != nil {
nsImp.set = append(nsImp.set, namespace)
return true
}
}
return false
return nsImp.scripts[handle].CastSet(def)
}
// Get finds or creates a new namespace
@@ -194,30 +209,27 @@ func (nsImp *Namespace) Get(handle string) (*types.Namespace, error) {
return nil, errors.New("invalid namespace handle")
}
if !nsImp.Exists(handle) {
nsImp.set = append(nsImp.set, &types.Namespace{
Slug: handle,
Name: handle,
Enabled: true,
})
}
namespace := nsImp.set.FindByHandle(handle)
nsImp.pages[handle] = NewPageImporter(nsImp.imp, namespace)
nsImp.modules[handle] = NewModuleImporter(nsImp.imp, namespace)
nsImp.charts[handle] = NewChartImporter(nsImp.imp, namespace)
return namespace, nil
return nsImp.set.FindByHandle(handle), nil
}
func (nsImp *Namespace) Store(ctx context.Context, nsk namespaceKeeper, mk moduleKeeper, ck chartKeeper, pk pageKeeper) error {
func (nsImp *Namespace) Setup(namespace *types.Namespace) {
nsImp.set = append(nsImp.set, namespace)
if _, has := nsImp.modules[namespace.Slug]; !has {
nsImp.modules[namespace.Slug] = NewModuleImporter(nsImp.imp, namespace)
nsImp.pages[namespace.Slug] = NewPageImporter(nsImp.imp, namespace)
nsImp.charts[namespace.Slug] = NewChartImporter(nsImp.imp, namespace)
nsImp.scripts[namespace.Slug] = NewAutomationImporter(nsImp.imp, namespace)
}
}
func (nsImp *Namespace) Store(ctx context.Context, nsk namespaceKeeper, mk moduleKeeper, ck chartKeeper, pk pageKeeper, sk automationScriptKeeper) error {
return nsImp.set.Walk(func(namespace *types.Namespace) (err error) {
var handle = namespace.Slug
if namespace.ID == 0 {
namespace, err = nsk.Create(namespace)
} else {
} else if nsImp.dirty[namespace.ID] {
namespace, err = nsk.Update(namespace)
}
@@ -225,6 +237,7 @@ func (nsImp *Namespace) Store(ctx context.Context, nsk namespaceKeeper, mk modul
return
}
nsImp.dirty[namespace.ID] = false
nsImp.imp.permissions.UpdateResources(types.NamespacePermissionResource.String(), handle, namespace.ID)
if _, ok := nsImp.modules[handle]; ok {
@@ -232,15 +245,17 @@ func (nsImp *Namespace) Store(ctx context.Context, nsk namespaceKeeper, mk modul
if err = nsImp.modules[handle].Store(ctx, mk); err != nil {
return errors.Wrap(err, "could not import modules")
}
if err = nsImp.scripts[handle].Store(ctx, sk); err != nil {
return errors.Wrap(err, "could not import automation scripts")
}
}
if err = nsImp.charts[handle].Store(ctx, ck); err != nil {
return errors.Wrap(err, "could not import charts")
}
if err = nsImp.charts[handle].Store(ctx, ck); err != nil {
return errors.Wrap(err, "could not import charts")
}
if err = nsImp.pages[handle].Store(ctx, pk); err != nil {
return errors.Wrap(err, "could not import pages")
if err = nsImp.pages[handle].Store(ctx, pk); err != nil {
return errors.Wrap(err, "could not import pages")
}
}
return

View File

@@ -3,11 +3,13 @@ package importer
import (
"context"
"fmt"
"sort"
"strconv"
"github.com/pkg/errors"
"github.com/cortezaproject/corteza-server/compose/types"
"github.com/cortezaproject/corteza-server/pkg/automation"
"github.com/cortezaproject/corteza-server/pkg/deinterfacer"
"github.com/cortezaproject/corteza-server/pkg/importer"
)
@@ -18,6 +20,7 @@ type (
namespace *types.Namespace
set types.PageSet
dirty map[uint64]bool
// page => module handle
modules map[string]string
@@ -26,22 +29,30 @@ type (
tree map[string][]string
}
// @todo remove finder strategy, directly provide set of items
pageFinder interface {
FindByHandle(uint64, string) (*types.Page, error)
Find(filter types.PageFilter) (set types.PageSet, f types.PageFilter, err error)
}
)
func NewPageImporter(imp *Importer, ns *types.Namespace) *Page {
return &Page{
out := &Page{
imp: imp,
namespace: ns,
set: types.PageSet{},
set: types.PageSet{},
dirty: make(map[uint64]bool),
modules: map[string]string{},
tree: map[string][]string{},
}
if imp.pageFinder != nil && ns.ID > 0 {
out.set, _, _ = imp.pageFinder.Find(types.PageFilter{NamespaceID: ns.ID})
}
return out
}
func (pImp *Page) getModule(handle string) (*types.Module, error) {
@@ -52,6 +63,14 @@ func (pImp *Page) getModule(handle string) (*types.Module, error) {
}
}
func (pImp *Page) getScript(name string) (*automation.Script, error) {
if g, ok := pImp.imp.namespaces.scripts[pImp.namespace.Slug]; !ok {
return nil, errors.Errorf("could not get scripts %q from non existing namespace %q", name, pImp.namespace.Slug)
} else {
return g.Get(name)
}
}
func (pImp *Page) getChart(handle string) (*types.Chart, error) {
if g, ok := pImp.imp.namespaces.charts[pImp.namespace.Slug]; !ok {
return nil, errors.Errorf("could not get chart %q from non existing namespace %q", handle, pImp.namespace.Slug)
@@ -93,12 +112,28 @@ func (pImp *Page) cast(parent, handle string, def interface{}) (err error) {
}
handle = importer.NormalizeHandle(handle)
if page, err = pImp.GetOrMake(handle); err != nil {
if page, err = pImp.Get(handle); err != nil {
return err
} else if page == nil {
page = &types.Page{
Handle: handle,
Title: handle,
Visible: true,
}
pImp.set = append(pImp.set, page)
} else if page.ID == 0 {
return errors.Errorf("page handle %q already defined in this import session", page.Handle)
} else {
pImp.dirty[page.ID] = true
}
pImp.tree[parent] = append(pImp.tree[parent], handle)
// Make pages are always sorted
sort.Strings(pImp.tree[parent])
if title, ok := def.(string); ok && title != "" {
page.Title = title
return nil
@@ -224,52 +259,6 @@ func (pImp *Page) castBlockStyle(page *types.Page, n int, def interface{}) (s ty
})
}
func (pImp *Page) Exists(handle string) bool {
handle = importer.NormalizeHandle(handle)
var (
page *types.Page
err error
)
page = pImp.set.FindByHandle(handle)
if page != nil {
return true
}
if pImp.namespace.ID == 0 {
// Assuming new namespace, nothing exists yet..
return false
}
if pImp.imp.pageFinder != nil {
page, err = pImp.imp.pageFinder.FindByHandle(pImp.namespace.ID, handle)
if err == nil && page != nil {
pImp.set = append(pImp.set, page)
return true
}
}
return false
}
// Get finds or makes a new page
func (pImp *Page) GetOrMake(handle string) (page *types.Page, err error) {
if page, err = pImp.Get(handle); err != nil {
return nil, err
} else if page == nil {
page = &types.Page{
Handle: handle,
Title: handle,
Visible: true,
}
pImp.set = append(pImp.set, page)
}
return page, nil
}
// Get existing pages
func (pImp *Page) Get(handle string) (*types.Page, error) {
handle = importer.NormalizeHandle(handle)
@@ -277,12 +266,9 @@ func (pImp *Page) Get(handle string) (*types.Page, error) {
return nil, errors.New("invalid page handle")
}
if pImp.Exists(handle) {
return pImp.set.FindByHandle(handle), nil
} else {
return nil, nil
}
return pImp.set.FindByHandle(handle), nil
}
func (pImp *Page) Store(ctx context.Context, k pageKeeper) error {
return pImp.storeChildren(ctx, "", k)
}
@@ -308,7 +294,7 @@ func (pImp *Page) storeChildren(ctx context.Context, parent string, k pageKeeper
if page.ID == 0 {
page.NamespaceID = pImp.namespace.ID
page, err = k.Create(page)
} else {
} else if pImp.dirty[page.ID] {
page, err = k.Update(page)
}
@@ -316,6 +302,7 @@ func (pImp *Page) storeChildren(ctx context.Context, parent string, k pageKeeper
return
}
pImp.dirty[page.ID] = false
if page.Handle == "" {
continue
}
@@ -335,6 +322,9 @@ func (pImp *Page) resolveRefs(page *types.Page) error {
if moduleHandle, ok := pImp.modules[page.Handle]; ok {
if module, err := pImp.getModule(moduleHandle); err != nil {
return err
} else if module == nil {
return errors.Wrapf(err, "could not load module %q for page %q",
moduleHandle, page.Handle)
} else {
page.ModuleID = module.ID
}
@@ -347,8 +337,8 @@ func (pImp *Page) resolveRefs(page *types.Page) error {
if h, ok := b.Options["module"]; ok {
if refm, err := pImp.getModule(deinterfacer.ToString(h)); err != nil || refm == nil {
return errors.Wrapf(err, "could not load module %q for page %q block #%d",
h, page.Handle, i+1)
return errors.Errorf("could not load module %q for page %q block #%d (%v)",
h, page.Handle, i+1, err)
} else {
b.Options["moduleID"] = strconv.FormatUint(refm.ID, 10)
delete(b.Options, "module")
@@ -357,8 +347,8 @@ func (pImp *Page) resolveRefs(page *types.Page) error {
if h, ok := b.Options["page"]; ok {
if refp, err := pImp.Get(deinterfacer.ToString(h)); err != nil || refp == nil {
return errors.Wrapf(err, "could not load page %q for page %q block #%d",
h, page.Handle, i+1)
return errors.Errorf("could not load page %q for page %q block #%d (%v)",
h, page.Handle, i+1, err)
} else {
b.Options["pageID"] = strconv.FormatUint(refp.ID, 10)
delete(b.Options, "page")
@@ -367,13 +357,49 @@ func (pImp *Page) resolveRefs(page *types.Page) error {
if h, ok := b.Options["chart"]; ok {
if refc, err := pImp.getChart(deinterfacer.ToString(h)); err != nil || refc == nil {
return errors.Wrapf(err, "could not load chart %q for page %q block #%d",
h, page.Handle, i+1)
return errors.Errorf("could not load chart %q for page %q block #%d (%v)",
h, page.Handle, i+1, err)
} else {
b.Options["chartID"] = strconv.FormatUint(refc.ID, 10)
delete(b.Options, "chart")
}
}
if b.Kind == "Automation" {
bb := make([]interface{}, 0)
err := deinterfacer.Each(b.Options["buttons"], func(_ int, _ string, btn interface{}) (err error) {
button := map[string]interface{}{}
err = deinterfacer.Each(btn, func(_ int, k string, v interface{}) error {
switch k {
case "script":
if s, err := pImp.getScript(deinterfacer.ToString(v)); err != nil || s == nil {
return errors.Errorf("could not load script %q for page %q block #%d (%v)",
v, page.Handle, i+1, err)
} else {
button["scriptID"] = s.ID
}
default:
button[k] = v
}
return nil
})
if err != nil {
return err
}
bb = append(bb, button)
return nil
})
b.Options["buttons"] = bb
if err != nil {
return err
}
}
}
return nil