From 55da25788f25e11f797723c54ae896d2fcf36e82 Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Fri, 20 Sep 2019 07:31:32 +0200 Subject: [PATCH] CLI export tool for compose --- compose/commands/exporter.go | 315 +++++++++++++++++++++++++++++++---- 1 file changed, 283 insertions(+), 32 deletions(-) diff --git a/compose/commands/exporter.go b/compose/commands/exporter.go index 25fb1c433..68c33cd72 100644 --- a/compose/commands/exporter.go +++ b/compose/commands/exporter.go @@ -2,14 +2,21 @@ package commands import ( "context" + "encoding/json" + "fmt" "regexp" + "strconv" + "strings" + sqlTypes "github.com/jmoiron/sqlx/types" "github.com/spf13/cobra" "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/auth" + "github.com/cortezaproject/corteza-server/internal/permissions" "github.com/cortezaproject/corteza-server/pkg/cli" "github.com/cortezaproject/corteza-server/pkg/handle" ) @@ -18,102 +25,346 @@ func Exporter(ctx context.Context, c *cli.Config) *cobra.Command { cmd := &cobra.Command{ Use: "export", Short: "Export", + Long: `Specify one ("modules", "pages", "charts", "permissions") or more resources to export`, + Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { c.InitServices(ctx, c) ctx = auth.SetSuperUserContext(ctx) - mm, _, err := service.DefaultModule.Find(types.ModuleFilter{NamespaceID: 88714882739863655}) + var ( + nsFlag = cmd.Flags().Lookup("namespace").Value.String() + ns *types.Namespace + 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) + } + } + + 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) y := yaml.NewEncoder(cmd.OutOrStdout()) + out := Compose{} - out := Compose{ - Modules: expModules(mm), + for _, arg := range args { + switch arg { + case "module", "modules": + out.Modules = expModules(modules) + case "chart", "charts": + out.Charts = expCharts(charts, modules) + case "page", "pages": + out.Pages = expPages(0, pages, modules, charts) + case "allow", "deny", "permission", "permissions": + out.Allow = expServicePermissions(permissions.Allow) + out.Deny = expServicePermissions(permissions.Deny) + } } + _, _ = y, out cli.HandleError(y.Encode(out)) }, } + cmd.Flags().String("namespace", "crm", "Export namespace resources (by ID or string)") + return cmd } type ( Compose struct { - Modules []Module + Modules map[string]Module `yaml:",omitempty"` + Pages map[string]Page `yaml:",omitempty"` + Charts map[string]Chart `yaml:",omitempty"` + + Allow map[string]map[string][]string `yaml:",omitempty"` + Deny map[string]map[string][]string `yaml:",omitempty"` } Module struct { Name string - Handle string - Meta string + Meta string `yaml:"meta,omitempty"` Fields map[string]Field + + Allow map[string][]string `yaml:",omitempty"` + Deny map[string][]string `yaml:",omitempty"` } Field struct { - Label string + Label string `yaml:",omitempty"` Kind string - Options string + Options types.ModuleFieldOptions `yaml:",omitempty"` - Private bool - Required bool - Visible bool - Multi bool + 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 map[string]Page `yaml:",omitempty"` + + Visible bool + + Allow map[string][]string `yaml:",omitempty"` + Deny map[string][]string `yaml:",omitempty"` + } + + Chart struct { + Name string `yaml:",omitempty"` + + Config types.ChartConfig `yaml:",omitempty"` + + Allow map[string][]string `yaml:",omitempty"` + Deny map[string][]string `yaml:",omitempty"` } ) -func expModules(mm types.ModuleSet) (o []Module) { - o = make([]Module, len(mm)) +func expModules(mm types.ModuleSet) (o map[string]Module) { + o = map[string]Module{} - for i, m := range mm { - o[i] = Module{ + for _, m := range mm { + module := Module{ Name: m.Name, - Handle: makeHandle(m.Handle, m.Name), - Meta: m.Meta.String(), - Fields: expModuleFields(m.Fields), + Fields: expModuleFields(m.Fields, mm), + + Allow: expResourcePermissions(permissions.Allow, types.ModulePermissionResource), + Deny: expResourcePermissions(permissions.Deny, types.ModulePermissionResource), } - if o[i].Handle == "" { - h := regexp.MustCompile(`^[^A-Za-z][^0-9A-Za-z_\-.][^A-Za-z0-9]$`).ReplaceAllString(o[i].Name, "") - if handle.IsValid(h) { - o[i].Handle = h - } + if meta := expModuleMetaCleanup(m.Meta); len(meta) > 0 { + module.Meta = meta } + + handle := makeHandleFromName(m.Name, m.Handle, "module-id", m.ID) + o[handle] = module } return } -func expModuleFields(ff types.ModuleFieldSet) (o map[string]Field) { +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 map[string]Field) { o = make(map[string]Field) for _, f := range ff { o[f.Name] = Field{ Label: f.Label, Kind: f.Kind, - Options: f.Options.String(), + Options: expModuleFieldOptions(f.Options, modules), Private: f.Private, Required: f.Required, Visible: f.Visible, Multi: f.Multi, + + Allow: expResourcePermissions(permissions.Allow, types.ModuleFieldPermissionResource), + Deny: expResourcePermissions(permissions.Deny, types.ModuleFieldPermissionResource), } } return } -func makeHandle(h, n string) string { - if h == "" { - h = regexp.MustCompile(`^[^A-Za-z][^0-9A-Za-z_\-.][^A-Za-z0-9]$`).ReplaceAllString(n, "") - if handle.IsValid(h) { - return h +func expModuleFieldOptions(opt types.ModuleFieldOptions, modules types.ModuleSet) types.ModuleFieldOptions { + out := opt + + 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) + } } } - return n + return out +} + +func expPages(parentID uint64, pages types.PageSet, modules types.ModuleSet, charts types.ChartSet) (o map[string]Page) { + var ( + children = pages.FindByParent(parentID) + handle string + ) + o = map[string]Page{} + + 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: expResourcePermissions(permissions.Allow, types.PagePermissionResource), + Deny: expResourcePermissions(permissions.Deny, types.PagePermissionResource), + } + + 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) + + 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) + } + + o[handle] = page + } + + return +} + +func expPageBlocks(in types.PageBlocks, pages types.PageSet, modules types.ModuleSet, charts types.ChartSet) types.PageBlocks { + out := types.PageBlocks(in) + + for i := range out { + if ff, has := out[i].Options["fields"].([]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), + } + } + } + } + + 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) + } + } + } + } + + return out +} + +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: c.Config, + + Allow: expResourcePermissions(permissions.Allow, types.ChartPermissionResource), + Deny: expResourcePermissions(permissions.Deny, types.ChartPermissionResource), + } + // @todo moduleID => module Handle (CONFIG) + + handle := makeHandleFromName(c.Name, c.Handle, "chart-%d", c.ID) + + o[handle] = chart + } + + return +} + +func expServicePermissions(access permissions.Access) map[string]map[string][]string { + // @todo fetch all known roles + // @todo iterate over roles + // @todo iterate over service.DefaultPermissions.FindRulesByRoleID() + // @todo filter out all matching types.ComposePermissionResource + // @todo fill return value + return nil +} + +func expResourcePermissions(access permissions.Access, resource permissions.Resource) map[string][]string { + // @todo fetch all known roles + // @todo iterate over roles + // @todo iterate over service.DefaultPermissions.FindRulesByRoleID() + // @todo filter out all matching resource param + // @todo fill return value + return nil +} + +func makeHandleFromName(name, currentHandle, def string, id uint64) string { + if currentHandle != "" { + return currentHandle + } + + newHandle := strings.ReplaceAll(name, " ", "_") + newHandle = regexp.MustCompile(`[^0-9A-Za-z_\-.]+`).ReplaceAllString(newHandle, "") + if handle.IsValid(newHandle) { + return newHandle + } + + return fmt.Sprintf(def, id) }