3
0
Files
corteza/compose/commands/exporter.go
2020-11-30 19:51:18 +01:00

724 lines
19 KiB
Go

package commands
import (
"context"
"encoding/json"
"fmt"
"github.com/cortezaproject/corteza-server/compose/service"
"github.com/cortezaproject/corteza-server/compose/types"
"github.com/cortezaproject/corteza-server/pkg/auth"
"github.com/cortezaproject/corteza-server/pkg/cli"
"github.com/cortezaproject/corteza-server/pkg/errors"
"github.com/cortezaproject/corteza-server/pkg/handle"
"github.com/cortezaproject/corteza-server/pkg/rbac"
"github.com/cortezaproject/corteza-server/pkg/settings"
sysExporter "github.com/cortezaproject/corteza-server/system/exporter"
sysService "github.com/cortezaproject/corteza-server/system/service"
sysTypes "github.com/cortezaproject/corteza-server/system/types"
sqlTypes "github.com/jmoiron/sqlx/types"
"github.com/spf13/cast"
"github.com/spf13/cobra"
"gopkg.in/yaml.v2"
"regexp"
"strconv"
"strings"
)
func Exporter() *cobra.Command {
cmd := &cobra.Command{
Use: "export",
Short: "Export",
Long: `Specify one ("modules", "pages", "charts", "permissions") or more resources to export`,
Run: func(cmd *cobra.Command, args []string) {
var (
ctx = auth.SetSuperUserContext(cli.Context())
nsFlag = cmd.Flags().Lookup("namespace").Value.String()
sFlag = cmd.Flags().Lookup("settings").Changed
pFlag = cmd.Flags().Lookup("permissions").Changed
out = &Compose{
Namespaces: map[string]Namespace{},
Settings: yaml.MapSlice{},
}
)
if nsFlag == "" && !sFlag && !pFlag {
cli.HandleError(fmt.Errorf("Specify namespace or setting or permissions flag"))
}
if nsFlag != "" {
nsExporter(ctx, out, nsFlag, args)
}
if sFlag {
settingExporter(ctx, out)
}
if pFlag {
permissionExporter(ctx, out)
}
y := yaml.NewEncoder(cmd.OutOrStdout())
cli.HandleError(y.Encode(out))
},
}
cmd.Flags().String("namespace", "", "Export namespace resources (by ID or string)")
cmd.Flags().BoolP("settings", "s", false, "Export settings")
cmd.Flags().BoolP("permissions", "p", false, "Export system permissions")
return cmd
}
func nsExporter(ctx context.Context, out *Compose, nsFlag string, args []string) {
var (
ns *types.Namespace
err error
nsOut = Namespace{}
)
if namespaceID, _ := strconv.ParseUint(nsFlag, 10, 64); namespaceID > 0 {
ns, err = service.DefaultNamespace.FindByID(namespaceID)
if errors.IsNotFound(err) {
cli.HandleError(err)
}
} else if ns, err = service.DefaultNamespace.FindByHandle(nsFlag); err != nil {
if errors.IsNotFound(err) {
cli.HandleError(err)
}
}
// roles, err = service.DefaultSystemRole.Find(ctx)
// cli.HandleError(err)
// At the moment, we can not load roles from system service
// so we'll just use static set of known roles
//
// Roles are use for resolving access control
roles = sysTypes.RoleSet{
&sysTypes.Role{ID: rbac.EveryoneRoleID, Handle: "everyone"},
&sysTypes.Role{ID: rbac.AdminsRoleID, Handle: "admins"},
}
modules, _, err := service.DefaultModule.Find(types.ModuleFilter{NamespaceID: ns.ID})
cli.HandleError(err)
pages, _, err := service.DefaultPage.Find(types.PageFilter{NamespaceID: ns.ID})
cli.HandleError(err)
charts, _, err := service.DefaultChart.Find(types.ChartFilter{NamespaceID: ns.ID})
cli.HandleError(err)
// nsOut.Name = ns.Name
// nsOut.Handle = ns.Slug
// nsOut.Always = ns.Always
// nsOut.Meta = ns.Meta
//
// nsOut.Allow = sysExporter.ExportableResourcePermissions(roles, service.DefaultRBAC, permissions.Allow, ns.RBACResource())
// nsOut.Deny = sysExporter.ExportableResourcePermissions(roles, service.DefaultRBAC, permissions.Deny, ns.RBACResource())
for _, arg := range args {
switch arg {
case "module", "modules":
nsOut.Modules = expModules(modules)
case "chart", "charts":
nsOut.Charts = expCharts(charts, modules)
case "page", "pages":
nsOut.Pages = expPages(0, pages, modules, charts)
}
}
nsOut.Namespace = ns.Slug
out.Namespaces[ns.Slug] = nsOut
}
func settingExporter(ctx context.Context, out *Compose) {
var (
err error
)
ss, err := sysService.DefaultSettings.FindByPrefix(ctx)
cli.HandleError(err)
out.Settings = settings.Export(ss)
}
func permissionExporter(ctx context.Context, out *Compose) {
roles := sysTypes.RoleSet{
&sysTypes.Role{ID: rbac.EveryoneRoleID, Handle: "everyone"},
&sysTypes.Role{ID: rbac.AdminsRoleID, Handle: "admins"},
}
out.Allow = sysExporter.ExportableServicePermissions(roles, rbac.Global(), rbac.Allow)
out.Deny = sysExporter.ExportableServicePermissions(roles, rbac.Global(), rbac.Deny)
}
// This is PoC for exporting compose resources
//
type (
Compose struct {
Namespaces map[string]Namespace `yaml:",omitempty"`
Settings yaml.MapSlice `yaml:",omitempty"`
Allow map[string]map[string][]string `yaml:",omitempty"`
Deny map[string]map[string][]string `yaml:",omitempty"`
}
Namespace struct {
// This is used when exporting one single namespace
Namespace string `yaml:",omitempty"`
Name string `yaml:",omitempty"`
Handle string `yaml:",omitempty"`
Enabled bool `yaml:",omitempty"`
Meta types.NamespaceMeta `yaml:",omitempty"`
Modules map[string]Module `yaml:",omitempty"`
Pages yaml.MapSlice `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"`
}
Module struct {
Name string
Meta string `yaml:"meta,omitempty"`
Fields yaml.MapSlice
Allow map[string][]string `yaml:",omitempty"`
Deny map[string][]string `yaml:",omitempty"`
}
Field struct {
Label string `yaml:",omitempty"`
Kind string
Options types.ModuleFieldOptions `yaml:",omitempty"`
Private bool `yaml:",omitempty"`
Required bool `yaml:",omitempty"`
Visible bool `yaml:",omitempty"`
Multi bool `yaml:",omitempty"`
// DefaultValue string
Allow map[string][]string `yaml:",omitempty"`
Deny map[string][]string `yaml:",omitempty"`
}
Page struct {
Module string `yaml:",omitempty"`
Title string `yaml:",omitempty"`
Description string `yaml:",omitempty"`
Blocks types.PageBlocks `yaml:",omitempty"`
Pages yaml.MapSlice `yaml:",omitempty"`
Visible bool
Allow map[string][]string `yaml:",omitempty"`
Deny map[string][]string `yaml:",omitempty"`
}
Chart struct {
Name string `yaml:",omitempty"`
Config ChartConfig `yaml:",omitempty"`
Allow map[string][]string `yaml:",omitempty"`
Deny map[string][]string `yaml:",omitempty"`
}
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"`
}
ChartConfig struct {
Reports []map[string]interface{}
ColorScheme string `yaml:"colorScheme,omitempty"`
}
)
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 expSettings(ss sysTypes.SettingValueSet) (o yaml.MapSlice) {
o = yaml.MapSlice{}
for _, s := range ss {
setting := yaml.MapItem{
Key: s.Name,
Value: s.Value.String(),
}
o = append(o, setting)
}
return o
}
func expModules(mm types.ModuleSet) (o map[string]Module) {
o = map[string]Module{}
for _, m := range mm {
module := Module{
Name: m.Name,
Fields: expModuleFields(m.Fields, mm),
Allow: sysExporter.ExportableResourcePermissions(roles, rbac.Global(), rbac.Allow, types.ModuleRBACResource),
Deny: sysExporter.ExportableResourcePermissions(roles, rbac.Global(), rbac.Deny, types.ModuleRBACResource),
}
if meta := expModuleMetaCleanup(m.Meta); len(meta) > 0 {
module.Meta = meta
}
handle := makeHandleFromName(m.Name, m.Handle, "module-%d", m.ID)
o[handle] = module
}
return
}
func expModuleMetaCleanup(meta sqlTypes.JSONText) string {
var aux interface{}
err := meta.Unmarshal(&aux)
cli.HandleError(err)
if kv, ok := aux.(map[string]interface{}); !ok {
return ""
} else if _, has := kv["admin"]; has {
delete(kv, "admin")
if len(kv) == 0 {
return ""
}
meta, err = json.Marshal(kv)
cli.HandleError(err)
}
return meta.String()
}
func expModuleFields(ff types.ModuleFieldSet, modules types.ModuleSet) (o yaml.MapSlice) {
o = make(yaml.MapSlice, len(ff))
for i, f := range ff {
o[i] = yaml.MapItem{
Key: f.Name,
Value: Field{
Label: f.Label,
Kind: f.Kind,
Options: expModuleFieldOptions(f, modules),
Private: f.Private,
Required: f.Required,
Visible: f.Visible,
Multi: f.Multi,
Allow: sysExporter.ExportableResourcePermissions(roles, rbac.Global(), rbac.Allow, types.ModuleFieldRBACResource),
Deny: sysExporter.ExportableResourcePermissions(roles, rbac.Global(), rbac.Deny, types.ModuleFieldRBACResource),
},
}
}
return
}
func expModuleFieldOptions(f *types.ModuleField, modules types.ModuleSet) types.ModuleFieldOptions {
out := f.Options
if moduleIDstr, has := out["moduleID"].(string); has {
delete(out, "moduleID")
out["module"] = "Error: module with ID " + moduleIDstr + " does not exist"
if moduleID, _ := strconv.ParseUint(moduleIDstr, 10, 64); moduleID > 0 {
if module := modules.FindByID(moduleID); module != nil {
out["module"] = makeHandleFromName(module.Name, module.Handle, "module-%d", module.ID)
}
}
}
// Remove extra options to keep the output tidy
rmDefault := func(k string, def interface{}) {
if v, ok := out[k]; ok && v == def {
delete(out, k)
}
}
rmFalse := func(f string) {
rmDefault(f, false)
}
rmDefault("multiDelimiter", "\n")
switch f.Kind {
case "Number":
rmDefault("format", 0)
rmDefault("precision", 0)
rmDefault("prefix", "")
rmDefault("suffix", "")
case "DateTime":
rmFalse("onlyDate")
rmFalse("onlyFutureValues")
rmFalse("onlyPastValues")
rmFalse("onlyTime")
rmFalse("outputRelative")
rmDefault("format", "")
case "Url":
rmFalse("onlySecure")
rmFalse("outputPlain")
rmFalse("trimFragment")
rmFalse("trimPath")
rmFalse("trimQuery")
case "Email":
rmFalse("outputPlain")
case "String":
rmFalse("multiLine")
rmFalse("useRichTextEditor")
}
return out
}
func expPages(parentID uint64, pages types.PageSet, modules types.ModuleSet, charts types.ChartSet) (o yaml.MapSlice) {
var (
children = pages.FindByParent(parentID)
handle string
)
o = yaml.MapSlice{}
for _, child := range children {
page := Page{
Title: child.Title,
Description: child.Description,
Blocks: expPageBlocks(child.Blocks, pages, modules, charts),
Pages: expPages(child.ID, pages, modules, charts),
Visible: child.Visible,
Allow: sysExporter.ExportableResourcePermissions(roles, rbac.Global(), rbac.Allow, types.PageRBACResource),
Deny: sysExporter.ExportableResourcePermissions(roles, rbac.Global(), rbac.Deny, types.PageRBACResource),
}
if child.ModuleID > 0 {
m := modules.FindByID(child.ModuleID)
if m == nil {
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)
}
}
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 = append(o, yaml.MapItem{
Key: handle,
Value: page,
})
}
return
}
func expPageBlocks(in types.PageBlocks, pages types.PageSet, modules types.ModuleSet, charts types.ChartSet) types.PageBlocks {
out := types.PageBlocks(in)
// Remove extra options to keep the output tidy
rmDefault := func(kv map[string]interface{}, k string, def interface{}) {
if v, ok := kv[k]; ok && v == def {
delete(kv, k)
}
}
rmFalse := func(kv map[string]interface{}, f string) {
rmDefault(kv, f, false)
}
for i := range out {
expEncodeRefFields(out[i].Options, "fields")
expEncodeRefFields(out[i].Options, "editFields")
if moduleIDstr, has := out[i].Options["moduleID"].(string); has {
delete(out[i].Options, "moduleID")
out[i].Options["module"] = "Error: module with ID " + moduleIDstr + " does not exist"
if moduleID, _ := strconv.ParseUint(moduleIDstr, 10, 64); moduleID > 0 {
if module := modules.FindByID(moduleID); module != nil {
out[i].Options["module"] = makeHandleFromName(module.Name, module.Handle, "module-%d", module.ID)
}
}
}
if pageIDstr, has := out[i].Options["pageID"].(string); has {
delete(out[i].Options, "pageID")
out[i].Options["page"] = "Error: page with ID " + pageIDstr + " does not exist"
if pageID, _ := strconv.ParseUint(pageIDstr, 10, 64); pageID > 0 {
if page := pages.FindByID(pageID); page != nil {
out[i].Options["page"] = makeHandleFromName(page.Title, page.Handle, "page-%d", page.ID)
}
}
}
if chartIDstr, has := out[i].Options["chartID"].(string); has {
delete(out[i].Options, "chartID")
out[i].Options["chart"] = "Error: chart with ID " + chartIDstr + " does not exist"
if chartID, _ := strconv.ParseUint(chartIDstr, 10, 64); chartID > 0 {
if chart := charts.FindByID(chartID); chart != nil {
out[i].Options["chart"] = makeHandleFromName(chart.Name, chart.Handle, "chart-%d", chart.ID)
}
}
}
rmFalse(out[i].Options, "hideAddButton")
rmFalse(out[i].Options, "hideHeader")
rmFalse(out[i].Options, "hidePaging")
rmFalse(out[i].Options, "hideSearch")
rmFalse(out[i].Options, "hideSorting")
rmFalse(out[i].Options, "allowExport")
switch out[i].Kind {
case "Automation":
expPageBlocks_Automation(&out[i])
case "Calendar":
expPageBlocks_Calendar(&out[i], modules)
case "Metric":
expPageBlocks_Metric(&out[i], modules)
}
}
return out
}
func expPageBlocks_Automation(b *types.PageBlock) {
bb := make([]interface{}, 0)
_ = each(b.Options["buttons"], func(_ int, _ string, btn interface{}) error {
button := map[string]interface{}{}
_ = 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
})
b.Options["buttons"] = bb
}
func expPageBlocks_Calendar(b *types.PageBlock, modules types.ModuleSet) {
ff := make([]interface{}, 0)
_ = each(b.Options["feeds"], func(_ int, _ string, def interface{}) error {
feed := map[string]interface{}{}
_ = each(def, func(_ int, k string, v interface{}) error {
switch k {
case "options":
expEncodeModuleInMap(v, modules)
feed["options"] = v
default:
feed[k] = v
}
return nil
})
ff = append(ff, feed)
return nil
})
b.Options["feeds"] = ff
}
func expPageBlocks_Metric(b *types.PageBlock, modules types.ModuleSet) {
mm := make([]interface{}, 0)
_ = each(b.Options["metrics"], func(_ int, _ string, def interface{}) error {
metric := map[string]interface{}{}
_ = each(def, func(_ int, k string, v interface{}) error {
switch k {
case "moduleID":
metric["module"] = expEncodeModule(v, modules)
default:
metric[k] = v
}
return nil
})
mm = append(mm, metric)
return nil
})
b.Options["metrics"] = mm
}
func expEncodeModule(id interface{}, modules types.ModuleSet) string {
if moduleID := cast.ToUint64(id); moduleID > 0 {
if module := modules.FindByID(moduleID); module != nil {
return makeHandleFromName(module.Name, module.Handle, "module-%d", module.ID)
}
}
return fmt.Sprintf("Error: module with ID %v does not exist", id)
}
func expEncodeModuleInMap(in interface{}, modules types.ModuleSet) {
m, ok := in.(map[string]interface{})
if !ok {
return
}
id, has := m["moduleID"]
if !has {
return
}
delete(m, "moduleID")
m["module"] = expEncodeModule(id, modules)
}
// Trims out any extra info on referenced fields, keeps just name
func expEncodeRefFields(m map[string]interface{}, name string) {
if ff, has := m[name].([]interface{}); has {
// Trim out obsolete field info
for fi := range ff {
if f, ok := ff[fi].(map[string]interface{}); ok {
ff[fi] = map[string]string{
"name": f["name"].(string),
}
}
}
}
}
func expCharts(charts types.ChartSet, modules types.ModuleSet) (o map[string]Chart) {
o = map[string]Chart{}
for _, c := range charts {
chart := Chart{
Name: c.Name,
Config: ChartConfig{
Reports: make([]map[string]interface{}, len(c.Config.Reports)),
ColorScheme: c.Config.ColorScheme,
},
Allow: sysExporter.ExportableResourcePermissions(roles, rbac.Global(), rbac.Allow, types.ChartRBACResource),
Deny: sysExporter.ExportableResourcePermissions(roles, rbac.Global(), rbac.Deny, types.ChartRBACResource),
}
for i, r := range c.Config.Reports {
if len(r.Metrics) == 0 {
continue
}
rOut := map[string]interface{}{
"filter": r.Filter,
"metrics": r.Metrics,
"dimensions": r.Dimensions,
"renderer": r.Renderer,
}
if r.YAxis != nil {
rOut["yAxis"] = r.YAxis
}
if r.ModuleID > 0 {
module := modules.FindByID(r.ModuleID)
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)
o[handle] = chart
}
return
}
func makeHandleFromName(name, currentHandle, def string, id uint64) string {
if currentHandle != "" {
return currentHandle
}
newHandle := strings.ReplaceAll(strings.Title(name), " ", "")
newHandle = regexp.MustCompile(`[^0-9A-Za-z_\-.]+`).ReplaceAllString(newHandle, "")
if handle.IsValid(newHandle) {
return newHandle
}
return fmt.Sprintf(def, id)
}
// each() tries to resolve interface{} into map[interface{}]interface{} or []interface{}
// and calls fn() for each entry
func each(i interface{}, fn func(int, string, interface{}) error) (err error) {
if kv, ok := i.(map[interface{}]interface{}); ok {
for k, v := range kv {
if key, ok := k.(string); !ok {
err = fmt.Errorf("unsupported key type: %T (%+v)", k, k)
} else {
err = fn(-1, key, v)
}
if err != nil {
return
}
}
} else if kv, ok := i.(map[string]interface{}); ok {
for k, v := range kv {
err = fn(-1, k, v)
if err != nil {
return
}
}
} else if slice, ok := i.([]interface{}); ok {
_ = slice
for index, i := range slice {
if err = fn(index, "", i); err != nil {
return
}
}
}
return
}