From 895b40da362ca9291adfa3479e4cf4038e5a624c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=C5=BE=20Jerman?= Date: Wed, 15 Sep 2021 17:24:09 +0200 Subject: [PATCH] Extend envoy to support resource translations --- .../resource-resource_translation.go.tpl | 31 ++ ...resource-resource_translation_parse.go.tpl | 73 ++++ ...rce-resource_translation_references.go.tpl | 33 ++ pkg/codegen-v3/internal/gen/envoy.go | 86 ++++ pkg/envoy/locale.go | 65 ++++ pkg/envoy/marshaler.go | 3 + pkg/envoy/resource/application.go | 5 - pkg/envoy/resource/automation_workflow.go | 12 +- pkg/envoy/resource/compose_chart.go | 12 +- pkg/envoy/resource/compose_module.go | 121 +++++- pkg/envoy/resource/compose_namespace.go | 12 +- pkg/envoy/resource/compose_page.go | 16 +- pkg/envoy/resource/resource.go | 9 + .../resource/resource_translation.gen.go | 89 +++++ pkg/envoy/resource/resource_translation.go | 54 +++ .../resource_translation_parse.gen.go | 138 +++++++ ...e_translation_references_automation.gen.go | 25 ++ ...urce_translation_references_compose.gen.go | 78 ++++ pkg/envoy/resource/role.go | 5 - pkg/envoy/resource/template.go | 5 - pkg/envoy/resource/types.go | 33 +- pkg/envoy/resource/user.go | 5 - pkg/envoy/store/compose_module_unmarshal.go | 9 +- pkg/envoy/store/decoder.go | 30 +- pkg/envoy/store/encoder.go | 2 + pkg/envoy/store/resource_translation.go | 26 ++ .../store/resource_translation_marshal.go | 206 ++++++++++ .../store/resource_translation_unmarshal.go | 27 ++ pkg/envoy/store/system.go | 96 ++++- pkg/envoy/yaml/compose_module.go | 4 +- pkg/envoy/yaml/compose_module_unmarshal.go | 53 ++- pkg/envoy/yaml/compose_namespace.go | 2 + pkg/envoy/yaml/compose_namespace_unmarshal.go | 16 + pkg/envoy/yaml/document.go | 25 ++ pkg/envoy/yaml/encoder.go | 2 + pkg/envoy/yaml/resource_translation.go | 54 +++ .../yaml/resource_translation_marshal.go | 366 ++++++++++++++++++ .../yaml/resource_translation_unmarshal.go | 120 ++++++ 38 files changed, 1891 insertions(+), 57 deletions(-) create mode 100644 pkg/codegen-v3/assets/templates/gocode/envoy/resource-resource_translation.go.tpl create mode 100644 pkg/codegen-v3/assets/templates/gocode/envoy/resource-resource_translation_parse.go.tpl create mode 100644 pkg/codegen-v3/assets/templates/gocode/envoy/resource-resource_translation_references.go.tpl create mode 100644 pkg/envoy/locale.go create mode 100644 pkg/envoy/resource/resource_translation.gen.go create mode 100644 pkg/envoy/resource/resource_translation.go create mode 100644 pkg/envoy/resource/resource_translation_parse.gen.go create mode 100644 pkg/envoy/resource/resource_translation_references_automation.gen.go create mode 100644 pkg/envoy/resource/resource_translation_references_compose.gen.go create mode 100644 pkg/envoy/store/resource_translation.go create mode 100644 pkg/envoy/store/resource_translation_marshal.go create mode 100644 pkg/envoy/store/resource_translation_unmarshal.go create mode 100644 pkg/envoy/yaml/resource_translation.go create mode 100644 pkg/envoy/yaml/resource_translation_marshal.go create mode 100644 pkg/envoy/yaml/resource_translation_unmarshal.go diff --git a/pkg/codegen-v3/assets/templates/gocode/envoy/resource-resource_translation.go.tpl b/pkg/codegen-v3/assets/templates/gocode/envoy/resource-resource_translation.go.tpl new file mode 100644 index 000000000..4210d4701 --- /dev/null +++ b/pkg/codegen-v3/assets/templates/gocode/envoy/resource-resource_translation.go.tpl @@ -0,0 +1,31 @@ +package {{ .Package }} + +{{ template "header-gentext.tpl" }} +{{ template "header-definitions.tpl" . }} + +import ( + systemTypes "github.com/cortezaproject/corteza-server/system/types" +{{- range .Imports }} + {{ . }} +{{- end }} +) + +{{- range .Def }} +{{ $Component := .Component }} +{{ $Resource := .Resource }} +{{ $GoType := printf "types.%s" .Resource }} +func (r *{{export $Component}}{{$Resource}}) EncodeTranslations() ([]*ResourceTranslation, error) { + out := make([]*ResourceTranslation, 0, 10) + + rr := r.Res.EncodeTranslations() + rr.SetLanguage(defaultLanguage) + res, ref, pp := r.ResourceTranslationParts() + out = append(out, NewResourceTranslation(systemTypes.FromLocale(rr), res, ref, pp...)) +{{ if .Locale.Extended }} + tmp, err := r.encodeTranslations() + return append(out, tmp...), err +{{ else }} + return out, nil +{{- end }} +} +{{- end }} diff --git a/pkg/codegen-v3/assets/templates/gocode/envoy/resource-resource_translation_parse.go.tpl b/pkg/codegen-v3/assets/templates/gocode/envoy/resource-resource_translation_parse.go.tpl new file mode 100644 index 000000000..a630a7aeb --- /dev/null +++ b/pkg/codegen-v3/assets/templates/gocode/envoy/resource-resource_translation_parse.go.tpl @@ -0,0 +1,73 @@ +package {{ .Package }} + +{{ template "header-gentext.tpl" }} +{{ template "header-definitions.tpl" . }} + +import ( + "fmt" + "strings" +{{- range .Imports }} + {{ . }} +{{- end }} +) + + +// Parse generates resource setting logic for each resource +// +// Resources with "envoy: false" are skipped +// +// This function is auto-generated +func ParseResourceTranslation(res string) (string, *Ref, []*Ref, error) { + if res == "" { + return "", nil, nil, fmt.Errorf("empty resource") + } + + sp := "/" + + if strings.Index(res, "corteza::") == 0 { + res = res[9:] + } + + res = strings.TrimSpace(res) + res = strings.TrimRight(res, sp) + rr := strings.Split(res, sp) + + // only service defined (corteza::system, corteza::compose, ...) + if len(rr) == 1 { + return "", nil, nil, fmt.Errorf("only service defined: %s", res) + } + + // full thing + resourceType, path := rr[0], rr[1:] + for p := 1; p < len(path); p++ { + if path[p] == "*" { + return "", nil, nil, fmt.Errorf("path wildcard not allowed for locale resources: '%s'", res) + } + } + + // make the resource provide the slice of parent resources we should nest under + switch resourceType { + {{- range .Def }} + case {{ unexport .Component "types" }}.{{ export .Resource }}ResourceTranslationType: + if len(path) != {{ len .Locale.Resource.References }} { + return "", nil, nil, fmt.Errorf("expecting {{ len .Locale.Resource.References }} reference components in path, got %d", len(path)) + } + {{- if gt (len .Locale.Resource.References) 0 }} + ref, pp, err := {{ export .Component .Resource }}ResourceTranslationReferences( + {{- range $i, $r := .Locale.Resource.References }} + // {{ unexport $r.Resource }} + path[{{ $i }}], + {{ end }} + ) + return {{ unexport .Component "types" }}.{{ export .Resource }}ResourceTranslationType, ref, pp, err + {{ else }} + + // Component resource, no path + return {{ unexport .Component "types" }}.{{ export .Resource }}ResourceTranslationType, nil, nil, nil + {{- end }} + {{- end}} + } + + // return unhandled resource as-is + return resourceType, nil, nil, nil +} diff --git a/pkg/codegen-v3/assets/templates/gocode/envoy/resource-resource_translation_references.go.tpl b/pkg/codegen-v3/assets/templates/gocode/envoy/resource-resource_translation_references.go.tpl new file mode 100644 index 000000000..065516b5b --- /dev/null +++ b/pkg/codegen-v3/assets/templates/gocode/envoy/resource-resource_translation_references.go.tpl @@ -0,0 +1,33 @@ +package {{ .Package }} + +{{ template "header-gentext.tpl" }} +{{ template "header-definitions.tpl" . }} + +import ( +{{- range .Imports }} + {{ . }} +{{- end }} +) + +{{- range .Def }} +{{- if .Locale }} +{{- if gt (len .Locale.Resource.References) 0 }} +// {{ export .Component .Resource }}ResourceTranslationReferences generates Locale references +// +// Resources with "envoy: false" are skipped +// +// This function is auto-generated +func {{ export .Component .Resource }}ResourceTranslationReferences({{- range .Locale.Resource.References }}{{ unexport .Resource }} string, {{- end }}) (res *Ref, pp []*Ref, err error) { + {{- range .Locale.Resource.References }} + {{- if eq .Field "ID" }} + res = &Ref{ResourceType: types.{{ export .Resource }}ResourceType, Identifiers: MakeIdentifiers({{ unexport .Resource }})} + {{- else }} + pp = append(pp, &Ref{ResourceType: types.{{ export .Resource }}ResourceType, Identifiers: MakeIdentifiers({{ unexport .Resource }})}) + {{- end }} + {{- end }} + + return +} +{{- end }} +{{- end }} +{{- end }} diff --git a/pkg/codegen-v3/internal/gen/envoy.go b/pkg/codegen-v3/internal/gen/envoy.go index 4ca895cb4..48fa357e5 100644 --- a/pkg/codegen-v3/internal/gen/envoy.go +++ b/pkg/codegen-v3/internal/gen/envoy.go @@ -13,6 +13,10 @@ func Envoy(t *template.Template, dd []*def.Document) error { return List{ "resource rbac parse": envoyResourceRbacUnmarshal, "resource rbac references": envoyResourceRbacReferences, + + "resource translation": envoyResourceTranslation, + "resource translation parse": envoyResourceTranslationUnmarshal, + "resource translation references": envoyResourceTranslationReferences, }.Generate(t, dd) } @@ -80,3 +84,85 @@ func envoyResourceRbacReferences(t *template.Template, dd []*def.Document) (err return } + +func envoyResourceTranslation(t *template.Template, dd []*def.Document) (err error) { + const ( + templateName = "envoy/resource-resource_translation.go.tpl" + outputPathTpl = "pkg/envoy/resource/resource_translation.gen.go" + ) + + dd = filter(dd, func(d *def.Document) bool { return d.Envoy && d.Locale != nil }) + + // build list of component type imports + ctImports := make([]string, 0) + + w := tpl.Wrap{ + Package: "resource", + Def: dd, + Imports: append(collectImports(dd...), ctImports...), + } + + err = tpl.GoTemplate(outputPathTpl, t.Lookup(templateName), w) + if err != nil { + return + } + + return +} + +func envoyResourceTranslationUnmarshal(t *template.Template, dd []*def.Document) (err error) { + const ( + templateName = "envoy/resource-resource_translation_parse.go.tpl" + outputPathTpl = "pkg/envoy/resource/resource_translation_parse.gen.go" + ) + + dd = filter(dd, func(d *def.Document) bool { return d.Envoy && d.Locale != nil }) + + // build list of component type imports + ctImports := make([]string, 0) + for _, d := range dd { + imp := d.Component + "Types " + cImport(d.Component, "types") + if !slice.HasString(ctImports, imp) { + ctImports = append(ctImports, imp) + } + } + + w := tpl.Wrap{ + Package: "resource", + Def: dd, + Imports: append(collectImports(dd...), ctImports...), + } + + err = tpl.GoTemplate(outputPathTpl, t.Lookup(templateName), w) + if err != nil { + return + } + + return +} + +func envoyResourceTranslationReferences(t *template.Template, dd []*def.Document) (err error) { + const ( + templateName = "envoy/resource-resource_translation_references.go.tpl" + outputPathTpl = "pkg/envoy/resource/resource_translation_references_%s.gen.go" + ) + + dd = filter(dd, func(d *def.Document) bool { return d.Envoy && d.Locale != nil }) + + for component, perComponent := range partByComponent(dd) { + + w := tpl.Wrap{ + Package: "resource", + Component: component, + Def: perComponent, + Imports: append(collectImports(perComponent...), cImport(component, "types")), + } + + err = tpl.GoTemplate(fmt.Sprintf(outputPathTpl, component), t.Lookup(templateName), w) + if err != nil { + return + } + } + + return +} diff --git a/pkg/envoy/locale.go b/pkg/envoy/locale.go new file mode 100644 index 000000000..38f834415 --- /dev/null +++ b/pkg/envoy/locale.go @@ -0,0 +1,65 @@ +package envoy + +import "github.com/cortezaproject/corteza-server/pkg/envoy/resource" + +// NormalizeResourceTranslations takes the provided resource.ResourceTranslation +// and merges duplicates based on the Priority parameter +func NormalizeResourceTranslations(rr ...resource.Interface) []resource.Interface { + out := make([]resource.Interface, 0, len(rr)) + locales := make([]*resource.ResourceTranslation, 0, len(rr)) + + // - collect all locale resources + for _, r := range rr { + if l, ok := r.(*resource.ResourceTranslation); ok { + locales = append(locales, l) + } else { + out = append(out, r) + } + } + + // make an index + var byResource map[string][2]*resource.ResourceTranslation + byResource = make(map[string][2]*resource.ResourceTranslation) + + for _, locale := range locales { + pp := byResource[locale.RefResource] + pp[locale.Priority] = locale + byResource[locale.RefResource] = pp + } + + // squash index by priority ascending + for _, pp := range byResource { + var aux *resource.ResourceTranslation + seen := make(map[string]bool) + + for _, p := range pp { + if p == nil { + continue + } + + if aux == nil { + aux = p + } else { + for _, r := range p.Res { + if seen[r.Lang.String()+r.K] { + continue + } + + aux.Res = append(aux.Res, r) + } + } + + for _, r := range p.Res { + if r.K != "" { + seen[r.Lang.String()+r.K] = true + } + } + } + + if aux != nil { + out = append(out, aux) + } + } + + return out +} diff --git a/pkg/envoy/marshaler.go b/pkg/envoy/marshaler.go index 609e2ed4c..9718dd19a 100644 --- a/pkg/envoy/marshaler.go +++ b/pkg/envoy/marshaler.go @@ -27,6 +27,9 @@ func CollectNodes(ii ...interface{}) (nn []resource.Interface, err error) { case resource.Interface: nn = append(nn, c) + case []resource.Interface: + nn = append(nn, c...) + case Marshaller: if tmp, err := c.MarshalEnvoy(); err != nil { return nil, err diff --git a/pkg/envoy/resource/application.go b/pkg/envoy/resource/application.go index 7d1ead5ec..b47914930 100644 --- a/pkg/envoy/resource/application.go +++ b/pkg/envoy/resource/application.go @@ -2,7 +2,6 @@ package resource import ( "fmt" - "strconv" "github.com/cortezaproject/corteza-server/system/types" ) @@ -38,10 +37,6 @@ func (r *Application) SysID() uint64 { return r.Res.ID } -func (r *Application) Ref() string { - return firstOkString(r.Res.Name, strconv.FormatUint(r.Res.ID, 10)) -} - // FindApplication looks for the app in the resource set func FindApplication(rr InterfaceSet, ii Identifiers) (ap *types.Application) { var apRes *Application diff --git a/pkg/envoy/resource/automation_workflow.go b/pkg/envoy/resource/automation_workflow.go index e1a5e30b3..ec0f3fa24 100644 --- a/pkg/envoy/resource/automation_workflow.go +++ b/pkg/envoy/resource/automation_workflow.go @@ -73,6 +73,14 @@ func (r *AutomationWorkflow) AddAutomationTrigger(res *types.Trigger) *Automatio return t } +func (r *AutomationWorkflow) ResourceTranslationParts() (resource string, ref *Ref, path []*Ref) { + ref = r.Ref() + path = nil + resource = fmt.Sprintf(types.WorkflowResourceTranslationTpl(), types.WorkflowResourceTranslationType, firstOkString(strconv.FormatUint(r.Res.ID, 10), r.Res.Handle)) + + return +} + func (r *AutomationWorkflow) AddAutomationWorkflowStep(res *types.WorkflowStep) *AutomationWorkflowStep { s := &AutomationWorkflowStep{ base: &base{}, @@ -107,10 +115,6 @@ func (r *AutomationWorkflow) SysID() uint64 { return r.Res.ID } -func (r *AutomationWorkflow) Ref() string { - return firstOkString(r.Res.Handle, r.Res.Meta.Name, strconv.FormatUint(r.Res.ID, 10)) -} - // FindAutomationWorkflow looks for the workflow in the resource set func FindAutomationWorkflow(rr InterfaceSet, ii Identifiers) (ns *types.Workflow) { var wfRes *AutomationWorkflow diff --git a/pkg/envoy/resource/compose_chart.go b/pkg/envoy/resource/compose_chart.go index dba3870fc..fd9db1cec 100644 --- a/pkg/envoy/resource/compose_chart.go +++ b/pkg/envoy/resource/compose_chart.go @@ -44,14 +44,18 @@ func (r *ComposeChart) SysID() uint64 { return r.Res.ID } -func (r *ComposeChart) Ref() string { - return firstOkString(r.Res.Handle, r.Res.Name, strconv.FormatUint(r.Res.ID, 10)) -} - func (r *ComposeChart) RBACPath() []*Ref { return []*Ref{r.RefNs} } +func (r *ComposeChart) ResourceTranslationParts() (resource string, ref *Ref, path []*Ref) { + ref = r.Ref() + path = []*Ref{r.RefNs} + resource = fmt.Sprintf(types.ChartResourceTranslationTpl(), types.ChartResourceTranslationType, r.RefNs.Identifiers.First(), firstOkString(strconv.FormatUint(r.Res.ID, 10), r.Res.Handle)) + + return +} + // FindComposeChart looks for the chart in the resources func FindComposeChart(rr InterfaceSet, ii Identifiers) (ch *types.Chart) { var chRes *ComposeChart diff --git a/pkg/envoy/resource/compose_module.go b/pkg/envoy/resource/compose_module.go index 541d8eb47..3da7c7c76 100644 --- a/pkg/envoy/resource/compose_module.go +++ b/pkg/envoy/resource/compose_module.go @@ -5,12 +5,23 @@ import ( "strconv" "github.com/cortezaproject/corteza-server/compose/types" + systemTypes "github.com/cortezaproject/corteza-server/system/types" + "github.com/spf13/cast" ) type ( + ComposeModuleField struct { + *base + Res *types.ModuleField + + RefNs *Ref + RefMod *Ref + } + ComposeModule struct { *base - Res *types.Module + Res *types.Module + ResFields []*ComposeModuleField // Might keep track of related NS RefNs *Ref @@ -59,8 +70,25 @@ func (r *ComposeModule) RBACPath() []*Ref { return []*Ref{r.RefNs} } -func (r *ComposeModule) Ref() string { - return firstOkString(r.Res.Handle, r.Res.Name, strconv.FormatUint(r.Res.ID, 10)) +func (r *ComposeModule) ResourceTranslationParts() (resource string, ref *Ref, path []*Ref) { + ref = r.Ref() + path = []*Ref{r.RefNs} + resource = fmt.Sprintf(types.ModuleResourceTranslationTpl(), types.ModuleResourceTranslationType, r.RefNs.Identifiers.First(), firstOkString(strconv.FormatUint(r.Res.ID, 10), r.Res.Handle)) + + return +} + +func (r *ComposeModule) encodeTranslations() ([]*ResourceTranslation, error) { + out := make([]*ResourceTranslation, 0, len(r.ResFields)) + + for _, f := range r.ResFields { + rr := f.Res.EncodeTranslations() + rr.SetLanguage(defaultLanguage) + res, ref, pp := f.ResourceTranslationParts() + out = append(out, NewResourceTranslation(systemTypes.FromLocale(rr), res, ref, pp...)) + } + + return out, nil } // FindComposeModule looks for the module in the resource set @@ -86,6 +114,93 @@ func FindComposeModule(rr InterfaceSet, ii Identifiers) (ns *types.Module) { return nil } +func (r *ComposeModule) AddField(f *ComposeModuleField) { + r.ResFields = append(r.ResFields, f) +} + +// func FindComposeModuleField(mod *ComposeModule, ii Identifiers) (f *ComposeModuleField) { +// for _, f := range mod.ResFields { +// if f.Identifiers().HasAny(ii) { +// return f +// } +// } + +// return nil +// } + +// FindComposeModuleField looks for the module field in the given module +func FindComposeModuleField(mod *types.Module, ii Identifiers) (f *types.ModuleField) { + ids := make(map[uint64]bool) + handles := make(map[string]bool) + for i := range ii { + auxID, err := cast.ToUint64E(i) + if err == nil { + ids[auxID] = true + continue + } + + handles[i] = true + } + + var ok bool + ff, _ := mod.Fields.Filter(func(mf *types.ModuleField) (bool, error) { + if _, ok = ids[mf.ID]; ok { + return true, nil + } + if _, ok = handles[mf.Name]; ok { + return true, nil + } + + return false, nil + }) + + if len(ff) == 0 { + return nil + } + return ff[0] +} + +func (r *ComposeModuleField) ResourceTranslationParts() (resource string, ref *Ref, path []*Ref) { + ref = r.Ref() + path = []*Ref{r.RefNs, r.RefMod} + resource = fmt.Sprintf(types.ModuleFieldResourceTranslationTpl(), types.ModuleFieldResourceTranslationType, r.RefNs.Identifiers.First(), r.RefMod.Identifiers.First(), firstOkString(strconv.FormatUint(r.Res.ID, 10), r.Res.Name)) + + return +} + +func (r *ComposeModuleField) ResourceTranslations() ([]*ResourceTranslation, error) { + out := make([]*ResourceTranslation, 0, 10) + + rr := r.Res.EncodeTranslations() + rr.SetLanguage(defaultLanguage) + res, ref, pp := r.ResourceTranslationParts() + out = append(out, NewResourceTranslation(systemTypes.FromLocale(rr), res, ref, pp...)) + + return out, nil +} + func ComposeModuleErrUnresolved(ii Identifiers) error { return fmt.Errorf("compose module unresolved %v", ii.StringSlice()) } + +func ComposeModuleFieldErrUnresolved(ii Identifiers) error { + return fmt.Errorf("compose module field unresolved %v", ii.StringSlice()) +} + +func NewComposeModuleField(res *types.ModuleField, nsRef, modRef string) *ComposeModuleField { + r := &ComposeModuleField{ + base: &base{}, + } + r.SetResourceType(types.ModuleFieldResourceType) + r.Res = res + + r.AddIdentifier(identifiers(res.Name, res.ID)...) + + r.RefNs = r.AddRef(types.NamespaceResourceType, nsRef) + r.RefMod = r.AddRef(types.ModuleResourceType, modRef) + + // Initial timestamps + r.SetTimestamps(MakeTimestampsCUDA(&res.CreatedAt, res.UpdatedAt, res.DeletedAt, nil)) + + return r +} diff --git a/pkg/envoy/resource/compose_namespace.go b/pkg/envoy/resource/compose_namespace.go index f7cca3773..37b5d3e42 100644 --- a/pkg/envoy/resource/compose_namespace.go +++ b/pkg/envoy/resource/compose_namespace.go @@ -27,12 +27,16 @@ func NewComposeNamespace(ns *types.Namespace) *ComposeNamespace { return r } -func (r *ComposeNamespace) SysID() uint64 { - return r.Res.ID +func (r *ComposeNamespace) ResourceTranslationParts() (resource string, ref *Ref, path []*Ref) { + ref = r.Ref() + path = nil + resource = fmt.Sprintf(types.NamespaceResourceTranslationTpl(), types.NamespaceResourceTranslationType, firstOkString(strconv.FormatUint(r.Res.ID, 10), r.Res.Slug)) + + return } -func (r *ComposeNamespace) Ref() string { - return firstOkString(r.Res.Slug, r.Res.Name, strconv.FormatUint(r.Res.ID, 10)) +func (r *ComposeNamespace) SysID() uint64 { + return r.Res.ID } // FindComposeNamespace looks for the namespace in the resource set diff --git a/pkg/envoy/resource/compose_page.go b/pkg/envoy/resource/compose_page.go index 826d3b84b..a803f7d08 100644 --- a/pkg/envoy/resource/compose_page.go +++ b/pkg/envoy/resource/compose_page.go @@ -142,14 +142,22 @@ func (r *ComposePage) SysID() uint64 { return r.Res.ID } -func (r *ComposePage) Ref() string { - return firstOkString(r.Res.Handle, r.Res.Title, strconv.FormatUint(r.Res.ID, 10)) -} - func (r *ComposePage) RBACPath() []*Ref { return []*Ref{r.RefNs} } +func (r *ComposePage) ResourceTranslationParts() (resource string, ref *Ref, path []*Ref) { + ref = r.Ref() + path = []*Ref{r.RefNs} + resource = fmt.Sprintf(types.PageResourceTranslationTpl(), types.PageResourceTranslationType, r.RefNs.Identifiers.First(), firstOkString(strconv.FormatUint(r.Res.ID, 10), r.Res.Handle)) + + return +} + +func (r *ComposePage) encodeTranslations() ([]*ResourceTranslation, error) { + return nil, nil +} + // FindComposePage looks for the page in the resources func FindComposePage(rr InterfaceSet, ii Identifiers) (pg *types.Page) { var pgRes *ComposePage diff --git a/pkg/envoy/resource/resource.go b/pkg/envoy/resource/resource.go index 77b79c22e..9b929d817 100644 --- a/pkg/envoy/resource/resource.go +++ b/pkg/envoy/resource/resource.go @@ -4,6 +4,7 @@ import ( "fmt" "strconv" + composeTypes "github.com/cortezaproject/corteza-server/compose/types" "github.com/cortezaproject/corteza-server/system/types" ) @@ -188,6 +189,14 @@ func (t *base) Refs() RefSet { return t.rr } +func (t *base) Ref() *Ref { + return &Ref{ResourceType: t.rt, Identifiers: t.ii} +} + func (t *base) HasRefs() bool { return t.rr == nil || len(t.rr) == 0 } + +func IgnoreDepResolution(ref *Ref) bool { + return ref.ResourceType == composeTypes.ModuleFieldResourceType +} diff --git a/pkg/envoy/resource/resource_translation.gen.go b/pkg/envoy/resource/resource_translation.gen.go new file mode 100644 index 000000000..1c09c8195 --- /dev/null +++ b/pkg/envoy/resource/resource_translation.gen.go @@ -0,0 +1,89 @@ +package resource + +// This file is auto-generated. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// + +// Definitions file that controls how this file is generated: +// - automation.workflow.yaml +// - compose.chart.yaml +// - compose.module-field.yaml +// - compose.module.yaml +// - compose.namespace.yaml +// - compose.page.yaml + +import ( + systemTypes "github.com/cortezaproject/corteza-server/system/types" +) + +func (r *AutomationWorkflow) EncodeTranslations() ([]*ResourceTranslation, error) { + out := make([]*ResourceTranslation, 0, 10) + + rr := r.Res.EncodeTranslations() + rr.SetLanguage(defaultLanguage) + res, ref, pp := r.ResourceTranslationParts() + out = append(out, NewResourceTranslation(systemTypes.FromLocale(rr), res, ref, pp...)) + + return out, nil +} + +func (r *ComposeChart) EncodeTranslations() ([]*ResourceTranslation, error) { + out := make([]*ResourceTranslation, 0, 10) + + rr := r.Res.EncodeTranslations() + rr.SetLanguage(defaultLanguage) + res, ref, pp := r.ResourceTranslationParts() + out = append(out, NewResourceTranslation(systemTypes.FromLocale(rr), res, ref, pp...)) + + return out, nil +} + +func (r *ComposeModuleField) EncodeTranslations() ([]*ResourceTranslation, error) { + out := make([]*ResourceTranslation, 0, 10) + + rr := r.Res.EncodeTranslations() + rr.SetLanguage(defaultLanguage) + res, ref, pp := r.ResourceTranslationParts() + out = append(out, NewResourceTranslation(systemTypes.FromLocale(rr), res, ref, pp...)) + + return out, nil +} + +func (r *ComposeModule) EncodeTranslations() ([]*ResourceTranslation, error) { + out := make([]*ResourceTranslation, 0, 10) + + rr := r.Res.EncodeTranslations() + rr.SetLanguage(defaultLanguage) + res, ref, pp := r.ResourceTranslationParts() + out = append(out, NewResourceTranslation(systemTypes.FromLocale(rr), res, ref, pp...)) + + tmp, err := r.encodeTranslations() + return append(out, tmp...), err + +} + +func (r *ComposeNamespace) EncodeTranslations() ([]*ResourceTranslation, error) { + out := make([]*ResourceTranslation, 0, 10) + + rr := r.Res.EncodeTranslations() + rr.SetLanguage(defaultLanguage) + res, ref, pp := r.ResourceTranslationParts() + out = append(out, NewResourceTranslation(systemTypes.FromLocale(rr), res, ref, pp...)) + + return out, nil +} + +func (r *ComposePage) EncodeTranslations() ([]*ResourceTranslation, error) { + out := make([]*ResourceTranslation, 0, 10) + + rr := r.Res.EncodeTranslations() + rr.SetLanguage(defaultLanguage) + res, ref, pp := r.ResourceTranslationParts() + out = append(out, NewResourceTranslation(systemTypes.FromLocale(rr), res, ref, pp...)) + + tmp, err := r.encodeTranslations() + return append(out, tmp...), err + +} diff --git a/pkg/envoy/resource/resource_translation.go b/pkg/envoy/resource/resource_translation.go new file mode 100644 index 000000000..eb222918c --- /dev/null +++ b/pkg/envoy/resource/resource_translation.go @@ -0,0 +1,54 @@ +package resource + +import ( + "fmt" + + "github.com/cortezaproject/corteza-server/system/types" + "golang.org/x/text/language" +) + +type ( + ResourceTranslation struct { + *base + Res types.ResourceTranslationSet + + // RelResource string + RefResource string + RefRes *Ref + RefPath []*Ref + + Priority int + } +) + +var ( + defaultLanguage = language.English +) + +func NewResourceTranslation(res types.ResourceTranslationSet, refResource string, refRes *Ref, refPath ...*Ref) *ResourceTranslation { + r := &ResourceTranslation{base: &base{}} + r.SetResourceType(ResourceTranslationType) + r.Res = res + + r.RefResource = refResource + r.RefRes = r.AddRef(refRes.ResourceType, refRes.Identifiers.StringSlice()...) + + // any additional constraints + for _, rp := range refPath { + r.RefPath = append(r.RefPath, r.AddRef(rp.ResourceType, rp.Identifiers.StringSlice()...)) + } + + return r +} + +func (l *ResourceTranslation) MarkDefault() { + l.Priority = 1 +} + +func (l *ResourceTranslation) MarkGeneric() { + l.Priority = 0 +} + +func ResourceTranslationErrNotFound(ii Identifiers) error { + return fmt.Errorf("resource translation not found %v", ii.StringSlice()) +} diff --git a/pkg/envoy/resource/resource_translation_parse.gen.go b/pkg/envoy/resource/resource_translation_parse.gen.go new file mode 100644 index 000000000..6cba461fc --- /dev/null +++ b/pkg/envoy/resource/resource_translation_parse.gen.go @@ -0,0 +1,138 @@ +package resource + +// This file is auto-generated. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// + +// Definitions file that controls how this file is generated: +// - automation.workflow.yaml +// - compose.chart.yaml +// - compose.module-field.yaml +// - compose.module.yaml +// - compose.namespace.yaml +// - compose.page.yaml + +import ( + "fmt" + automationTypes "github.com/cortezaproject/corteza-server/automation/types" + composeTypes "github.com/cortezaproject/corteza-server/compose/types" + "strings" +) + +// Parse generates resource setting logic for each resource +// +// Resources with "envoy: false" are skipped +// +// This function is auto-generated +func ParseResourceTranslation(res string) (string, *Ref, []*Ref, error) { + if res == "" { + return "", nil, nil, fmt.Errorf("empty resource") + } + + sp := "/" + + if strings.Index(res, "corteza::") == 0 { + res = res[9:] + } + + res = strings.TrimSpace(res) + res = strings.TrimRight(res, sp) + rr := strings.Split(res, sp) + + // only service defined (corteza::system, corteza::compose, ...) + if len(rr) == 1 { + return "", nil, nil, fmt.Errorf("only service defined: %s", res) + } + + // full thing + resourceType, path := rr[0], rr[1:] + for p := 1; p < len(path); p++ { + if path[p] == "*" { + return "", nil, nil, fmt.Errorf("path wildcard not allowed for locale resources: '%s'", res) + } + } + + // make the resource provide the slice of parent resources we should nest under + switch resourceType { + case automationTypes.WorkflowResourceTranslationType: + if len(path) != 1 { + return "", nil, nil, fmt.Errorf("expecting 1 reference components in path, got %d", len(path)) + } + ref, pp, err := AutomationWorkflowResourceTranslationReferences( + // workflow + path[0], + ) + return automationTypes.WorkflowResourceTranslationType, ref, pp, err + + case composeTypes.ChartResourceTranslationType: + if len(path) != 2 { + return "", nil, nil, fmt.Errorf("expecting 2 reference components in path, got %d", len(path)) + } + ref, pp, err := ComposeChartResourceTranslationReferences( + // namespace + path[0], + + // chart + path[1], + ) + return composeTypes.ChartResourceTranslationType, ref, pp, err + + case composeTypes.ModuleFieldResourceTranslationType: + if len(path) != 3 { + return "", nil, nil, fmt.Errorf("expecting 3 reference components in path, got %d", len(path)) + } + ref, pp, err := ComposeModuleFieldResourceTranslationReferences( + // namespace + path[0], + + // module + path[1], + + // moduleField + path[2], + ) + return composeTypes.ModuleFieldResourceTranslationType, ref, pp, err + + case composeTypes.ModuleResourceTranslationType: + if len(path) != 2 { + return "", nil, nil, fmt.Errorf("expecting 2 reference components in path, got %d", len(path)) + } + ref, pp, err := ComposeModuleResourceTranslationReferences( + // namespace + path[0], + + // module + path[1], + ) + return composeTypes.ModuleResourceTranslationType, ref, pp, err + + case composeTypes.NamespaceResourceTranslationType: + if len(path) != 1 { + return "", nil, nil, fmt.Errorf("expecting 1 reference components in path, got %d", len(path)) + } + ref, pp, err := ComposeNamespaceResourceTranslationReferences( + // namespace + path[0], + ) + return composeTypes.NamespaceResourceTranslationType, ref, pp, err + + case composeTypes.PageResourceTranslationType: + if len(path) != 2 { + return "", nil, nil, fmt.Errorf("expecting 2 reference components in path, got %d", len(path)) + } + ref, pp, err := ComposePageResourceTranslationReferences( + // namespace + path[0], + + // page + path[1], + ) + return composeTypes.PageResourceTranslationType, ref, pp, err + + } + + // return unhandled resource as-is + return resourceType, nil, nil, nil +} diff --git a/pkg/envoy/resource/resource_translation_references_automation.gen.go b/pkg/envoy/resource/resource_translation_references_automation.gen.go new file mode 100644 index 000000000..22bef1344 --- /dev/null +++ b/pkg/envoy/resource/resource_translation_references_automation.gen.go @@ -0,0 +1,25 @@ +package resource + +// This file is auto-generated. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// + +// Definitions file that controls how this file is generated: +// - automation.workflow.yaml + +import ( + "github.com/cortezaproject/corteza-server/automation/types" +) + +// AutomationWorkflowResourceTranslationReferences generates Locale references +// +// Resources with "envoy: false" are skipped +// +// This function is auto-generated +func AutomationWorkflowResourceTranslationReferences(workflow string) (res *Ref, pp []*Ref, err error) { + res = &Ref{ResourceType: types.WorkflowResourceType, Identifiers: MakeIdentifiers(workflow)} + + return +} diff --git a/pkg/envoy/resource/resource_translation_references_compose.gen.go b/pkg/envoy/resource/resource_translation_references_compose.gen.go new file mode 100644 index 000000000..28cf8a820 --- /dev/null +++ b/pkg/envoy/resource/resource_translation_references_compose.gen.go @@ -0,0 +1,78 @@ +package resource + +// This file is auto-generated. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// + +// Definitions file that controls how this file is generated: +// - compose.chart.yaml +// - compose.module-field.yaml +// - compose.module.yaml +// - compose.namespace.yaml +// - compose.page.yaml + +import ( + "github.com/cortezaproject/corteza-server/compose/types" +) + +// ComposeChartResourceTranslationReferences generates Locale references +// +// Resources with "envoy: false" are skipped +// +// This function is auto-generated +func ComposeChartResourceTranslationReferences(namespace string, chart string) (res *Ref, pp []*Ref, err error) { + pp = append(pp, &Ref{ResourceType: types.NamespaceResourceType, Identifiers: MakeIdentifiers(namespace)}) + res = &Ref{ResourceType: types.ChartResourceType, Identifiers: MakeIdentifiers(chart)} + + return +} + +// ComposeModuleFieldResourceTranslationReferences generates Locale references +// +// Resources with "envoy: false" are skipped +// +// This function is auto-generated +func ComposeModuleFieldResourceTranslationReferences(namespace string, module string, moduleField string) (res *Ref, pp []*Ref, err error) { + pp = append(pp, &Ref{ResourceType: types.NamespaceResourceType, Identifiers: MakeIdentifiers(namespace)}) + pp = append(pp, &Ref{ResourceType: types.ModuleResourceType, Identifiers: MakeIdentifiers(module)}) + res = &Ref{ResourceType: types.ModuleFieldResourceType, Identifiers: MakeIdentifiers(moduleField)} + + return +} + +// ComposeModuleResourceTranslationReferences generates Locale references +// +// Resources with "envoy: false" are skipped +// +// This function is auto-generated +func ComposeModuleResourceTranslationReferences(namespace string, module string) (res *Ref, pp []*Ref, err error) { + pp = append(pp, &Ref{ResourceType: types.NamespaceResourceType, Identifiers: MakeIdentifiers(namespace)}) + res = &Ref{ResourceType: types.ModuleResourceType, Identifiers: MakeIdentifiers(module)} + + return +} + +// ComposeNamespaceResourceTranslationReferences generates Locale references +// +// Resources with "envoy: false" are skipped +// +// This function is auto-generated +func ComposeNamespaceResourceTranslationReferences(namespace string) (res *Ref, pp []*Ref, err error) { + res = &Ref{ResourceType: types.NamespaceResourceType, Identifiers: MakeIdentifiers(namespace)} + + return +} + +// ComposePageResourceTranslationReferences generates Locale references +// +// Resources with "envoy: false" are skipped +// +// This function is auto-generated +func ComposePageResourceTranslationReferences(namespace string, page string) (res *Ref, pp []*Ref, err error) { + pp = append(pp, &Ref{ResourceType: types.NamespaceResourceType, Identifiers: MakeIdentifiers(namespace)}) + res = &Ref{ResourceType: types.PageResourceType, Identifiers: MakeIdentifiers(page)} + + return +} diff --git a/pkg/envoy/resource/role.go b/pkg/envoy/resource/role.go index 626a33607..06d3b024b 100644 --- a/pkg/envoy/resource/role.go +++ b/pkg/envoy/resource/role.go @@ -2,7 +2,6 @@ package resource import ( "fmt" - "strconv" "github.com/cortezaproject/corteza-server/system/types" ) @@ -32,10 +31,6 @@ func (r *Role) SysID() uint64 { return r.Res.ID } -func (r *Role) Ref() string { - return firstOkString(r.Res.Handle, r.Res.Name, strconv.FormatUint(r.Res.ID, 10)) -} - // FindRole looks for the role in the resources func FindRole(rr InterfaceSet, ii Identifiers) (rl *types.Role) { var rlRes *Role diff --git a/pkg/envoy/resource/template.go b/pkg/envoy/resource/template.go index ecccb34a3..f480aa032 100644 --- a/pkg/envoy/resource/template.go +++ b/pkg/envoy/resource/template.go @@ -2,7 +2,6 @@ package resource import ( "fmt" - "strconv" "github.com/cortezaproject/corteza-server/system/types" ) @@ -32,10 +31,6 @@ func (r *Template) SysID() uint64 { return r.Res.ID } -func (r *Template) Ref() string { - return firstOkString(r.Res.Handle, r.Res.Meta.Short, strconv.FormatUint(r.Res.ID, 10)) -} - // FindTemplate looks for the template in the resources func FindTemplate(rr InterfaceSet, ii Identifiers) (u *types.Template) { var tRes *Template diff --git a/pkg/envoy/resource/types.go b/pkg/envoy/resource/types.go index 79067c766..d24560a59 100644 --- a/pkg/envoy/resource/types.go +++ b/pkg/envoy/resource/types.go @@ -1,5 +1,7 @@ package resource +import "strconv" + type ( Interface interface { Identifiers() Identifiers @@ -27,6 +29,13 @@ type ( RBACPath() []*Ref } + LocaleInterface interface { + Interface + + ResourceTranslationParts() (string, *Ref, []*Ref) + EncodeTranslations() ([]*ResourceTranslation, error) + } + RefSet []*Ref Ref struct { // @todo check with Denis regarding strings here (the cdocs comment) @@ -40,9 +49,10 @@ type ( ) var ( - DataSourceResourceType = "data:raw" - SettingsResourceType = "setting" - RbacResourceType = "rbac-rule" + DataSourceResourceType = "data:raw" + SettingsResourceType = "setting" + RbacResourceType = "rbac-rule" + ResourceTranslationType = "resource-translation" ) func MakeIdentifiers(ss ...string) Identifiers { @@ -87,6 +97,23 @@ func (ri Identifiers) First() string { return ss[0] } +func (ri Identifiers) FirstID() uint64 { + ss := ri.StringSlice() + if len(ss) <= 0 { + return 0 + } + + for _, s := range ss { + if v, err := strconv.ParseUint(s, 10, 64); err != nil { + continue + } else { + return v + } + } + + return 0 +} + func (rr InterfaceSet) Walk(f func(r Interface) error) (err error) { for _, r := range rr { err = f(r) diff --git a/pkg/envoy/resource/user.go b/pkg/envoy/resource/user.go index 3240f6f48..70ce18d37 100644 --- a/pkg/envoy/resource/user.go +++ b/pkg/envoy/resource/user.go @@ -2,7 +2,6 @@ package resource import ( "fmt" - "strconv" "github.com/cortezaproject/corteza-server/system/types" ) @@ -32,10 +31,6 @@ func (r *User) SysID() uint64 { return r.Res.ID } -func (r *User) Ref() string { - return firstOkString(r.Res.Handle, r.Res.Username, r.Res.Email, r.Res.Name, strconv.FormatUint(r.Res.ID, 10)) -} - // FindUser looks for the user in the resources func FindUser(rr InterfaceSet, ii Identifiers) (u *types.User) { var uRes *User diff --git a/pkg/envoy/store/compose_module_unmarshal.go b/pkg/envoy/store/compose_module_unmarshal.go index b68d9ac24..7a576309f 100644 --- a/pkg/envoy/store/compose_module_unmarshal.go +++ b/pkg/envoy/store/compose_module_unmarshal.go @@ -16,8 +16,15 @@ func newComposeModule(mod *types.Module) *composeModule { func (mod *composeModule) MarshalEnvoy() ([]resource.Interface, error) { refNs := strconv.FormatUint(mod.mod.NamespaceID, 10) + refMod := strconv.FormatUint(mod.mod.ID, 10) + + rMod := resource.NewComposeModule(mod.mod, refNs) + for _, f := range mod.mod.Fields { + r := resource.NewComposeModuleField(f, refNs, refMod) + rMod.AddField(r) + } return envoy.CollectNodes( - resource.NewComposeModule(mod.mod, refNs), + rMod, ) } diff --git a/pkg/envoy/store/decoder.go b/pkg/envoy/store/decoder.go index 322308579..c65cfdc7c 100644 --- a/pkg/envoy/store/decoder.go +++ b/pkg/envoy/store/decoder.go @@ -24,12 +24,13 @@ type ( composeChart []*composeChartFilter // System stuff - roles []*roleFilter - users []*userFilter - templates []*templateFilter - applications []*applicationFilter - settings []*settingFilter - rbac []*rbacFilter + roles []*roleFilter + users []*userFilter + templates []*templateFilter + applications []*applicationFilter + settings []*settingFilter + rbac []*rbacFilter + resourceTranslations []*resourceTranslationFilter // Automation stuff automationWorkflow []*automationWorkflowFilter @@ -72,6 +73,22 @@ func (aum auxMarshaller) MarshalEnvoy() ([]resource.Interface, error) { if err != nil { return nil, err } + + // Default locales for applicable resources + for _, t := range tmp { + if li, ok := t.(resource.LocaleInterface); ok { + ll, err := li.EncodeTranslations() + if err != nil { + return nil, err + } + + for _, l := range ll { + l.MarkDefault() + ii = append(ii, l) + } + } + } + ii = append(ii, tmp...) } @@ -122,6 +139,7 @@ func (d *decoder) Decode(ctx context.Context, s store.Storer, f *DecodeFilter) ( system.decodeTemplates(ctx, s, f.templates), system.decodeApplications(ctx, s, f.applications), system.decodeSettings(ctx, s, f.settings), + system.decodeResourceTranslation(ctx, s, f.resourceTranslations), automation.decodeWorkflows(ctx, s, f.automationWorkflow), ) diff --git a/pkg/envoy/store/encoder.go b/pkg/envoy/store/encoder.go index d54d8f581..975a11902 100644 --- a/pkg/envoy/store/encoder.go +++ b/pkg/envoy/store/encoder.go @@ -141,6 +141,8 @@ func (se *storeEncoder) Prepare(ctx context.Context, ee ...*envoy.ResourceState) err = f(NewSettingFromResource(res, se.cfg), ers) case *resource.RbacRule: err = f(newRbacRuleFromResource(res, se.cfg), ers) + case *resource.ResourceTranslation: + err = f(newResourceTranslationFromResource(res, se.cfg), ers) // Automation resources case *resource.AutomationWorkflow: diff --git a/pkg/envoy/store/resource_translation.go b/pkg/envoy/store/resource_translation.go new file mode 100644 index 000000000..adc617def --- /dev/null +++ b/pkg/envoy/store/resource_translation.go @@ -0,0 +1,26 @@ +package store + +import ( + "fmt" + + "github.com/cortezaproject/corteza-server/pkg/envoy/resource" + "github.com/cortezaproject/corteza-server/system/types" +) + +type ( + resourceTranslation struct { + cfg *EncoderConfig + + locales types.ResourceTranslationSet + + // point to the resource translation + refResourceTranslation string + refLocaleRes *resource.Ref + + refPathRes []*resource.Ref + } +) + +func resourceTranslationErrUnidentifiable(ii resource.Identifiers) error { + return fmt.Errorf("resource translation unidentifiable %v", ii.StringSlice()) +} diff --git a/pkg/envoy/store/resource_translation_marshal.go b/pkg/envoy/store/resource_translation_marshal.go new file mode 100644 index 000000000..2e9e2b44a --- /dev/null +++ b/pkg/envoy/store/resource_translation_marshal.go @@ -0,0 +1,206 @@ +package store + +import ( + "context" + "fmt" + + composeTypes "github.com/cortezaproject/corteza-server/compose/types" + "github.com/cortezaproject/corteza-server/pkg/envoy/resource" + "github.com/cortezaproject/corteza-server/store" + "github.com/cortezaproject/corteza-server/system/types" + "golang.org/x/text/language" +) + +var ( + gResourceTranslations map[language.Tag]map[string]*types.ResourceTranslation + + resourceTranslationIndex = func(l *types.ResourceTranslation) string { + return fmt.Sprintf("%s/%s", l.Resource, l.K) + } +) + +func newResourceTranslationFromResource(res *resource.ResourceTranslation, cfg *EncoderConfig) resourceState { + return &resourceTranslation{ + cfg: mergeConfig(cfg, res.Config()), + + locales: res.Res, + + refResourceTranslation: res.Res[0].Resource, + refLocaleRes: res.RefRes, + + refPathRes: res.RefPath, + } +} + +func (n *resourceTranslation) Prepare(ctx context.Context, pl *payload) (err error) { + // Init global state + if gResourceTranslations == nil { + err = n.initGlobalIndex(ctx, pl.s) + if err != nil { + return err + } + } + + // For now we will only allow resource specific locale resources if that resource is + // also present. + // Here we check if we can find it in case we're handling a resource specific rule. + refRes := n.refLocaleRes + if refRes.ResourceType == composeTypes.ModuleFieldResourceType { + for _, r := range pl.state.ParentResources { + if r.ResourceType() == composeTypes.ModuleResourceType && r.Identifiers().HasAny(n.refPathRes[1].Identifiers) { + return + } + } + } else { + for _, r := range pl.state.ParentResources { + if refRes.ResourceType == r.ResourceType() && r.Identifiers().HasAny(refRes.Identifiers) { + return + } + } + // We couldn't find it... + return resource.ResourceTranslationErrNotFound(refRes.Identifiers) + } + + return +} + +func (n *resourceTranslation) Encode(ctx context.Context, pl *payload) (err error) { + localeRes, err := n.makeResourceTranslation(pl) + if err != nil { + return err + } + + // Assure correct resources + for _, l := range n.locales { + l.Resource = localeRes + } + + ll := make(types.ResourceTranslationSet, 0, len(n.locales)) + for _, l := range n.locales { + old, ok := gResourceTranslations[l.Lang.Tag][resourceTranslationIndex(l)] + + // not exist + if !ok { + l.ID = NextID() + l.CreatedAt = *now() + l.CreatedBy = pl.invokerID + + ll = append(ll, l) + continue + } + + // exists + switch n.cfg.OnExisting { + case resource.Skip, + resource.MergeLeft: + continue + + case resource.MergeRight, + resource.Default: + old.Message = l.Message + + ll = append(ll, old) + } + } + + // Update global index for following runs + n.updateGlobalIndex(ll) + + return store.UpsertResourceTranslation(ctx, pl.s, ll...) +} + +func (n *resourceTranslation) makeResourceTranslation(pl *payload) (string, error) { + p0ID := uint64(0) + p1ID := uint64(0) + p2ID := uint64(0) + + localeRes := "" + _ = localeRes + + switch n.refLocaleRes.ResourceType { + case composeTypes.NamespaceResourceType: + p1 := resource.FindComposeNamespace(pl.state.ParentResources, n.refLocaleRes.Identifiers) + if p1 == nil { + return "", resource.ComposeNamespaceErrUnresolved(n.refLocaleRes.Identifiers) + } + p1ID = p1.ID + + return composeTypes.NamespaceResourceTranslation(p1ID), nil + case composeTypes.ModuleResourceType: + p0 := resource.FindComposeNamespace(pl.state.ParentResources, n.refPathRes[0].Identifiers) + if p0 == nil { + return "", resource.ComposeNamespaceErrUnresolved(n.refPathRes[0].Identifiers) + } + p0ID = p0.ID + + p1 := resource.FindComposeModule(pl.state.ParentResources, n.refLocaleRes.Identifiers) + if p1 == nil { + return "", resource.ComposeModuleErrUnresolved(n.refLocaleRes.Identifiers) + } + p1ID = p1.ID + + return composeTypes.ModuleResourceTranslation(p0ID, p1ID), nil + + case composeTypes.PageResourceType: + p0 := resource.FindComposeNamespace(pl.state.ParentResources, n.refPathRes[0].Identifiers) + if p0 == nil { + return "", resource.ComposeNamespaceErrUnresolved(n.refPathRes[0].Identifiers) + } + p0ID = p0.ID + + p1 := resource.FindComposePage(pl.state.ParentResources, n.refLocaleRes.Identifiers) + if p1 == nil { + return "", resource.ComposePageErrUnresolved(n.refLocaleRes.Identifiers) + } + p1ID = p1.ID + + return composeTypes.PageResourceTranslation(p0ID, p1ID), nil + + case composeTypes.ModuleFieldResourceType: + p0 := resource.FindComposeNamespace(pl.state.ParentResources, n.refPathRes[0].Identifiers) + if p0 == nil { + return "", resource.ComposeNamespaceErrUnresolved(n.refPathRes[0].Identifiers) + } + p0ID = p0.ID + + p1 := resource.FindComposeModule(pl.state.ParentResources, n.refPathRes[1].Identifiers) + if p1 == nil { + return "", resource.ComposeModuleErrUnresolved(n.refPathRes[1].Identifiers) + } + p1ID = p1.ID + + // field + f := resource.FindComposeModuleField(p1, n.refLocaleRes.Identifiers) + if f == nil { + return "", resource.ComposeModuleFieldErrUnresolved(n.refLocaleRes.Identifiers) + } + p2ID = f.ID + + return composeTypes.ModuleFieldResourceTranslation(p0ID, p1ID, p2ID), 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. + return "", fmt.Errorf("unsupported resource type '%s' for RBAC store encode", n.refLocaleRes.ResourceType) + } +} + +func (n *resourceTranslation) initGlobalIndex(ctx context.Context, s store.Storer) (err error) { + gResourceTranslations = make(map[language.Tag]map[string]*types.ResourceTranslation) + ll, _, err := store.SearchResourceTranslations(ctx, s, types.ResourceTranslationFilter{}) + if err != store.ErrNotFound && err != nil { + return err + } + + n.updateGlobalIndex(ll) + return nil +} + +func (n *resourceTranslation) updateGlobalIndex(ll types.ResourceTranslationSet) { + for _, l := range ll { + if _, ok := gResourceTranslations[l.Lang.Tag]; !ok { + gResourceTranslations[l.Lang.Tag] = make(map[string]*types.ResourceTranslation) + } + gResourceTranslations[l.Lang.Tag][resourceTranslationIndex(l)] = l + } +} diff --git a/pkg/envoy/store/resource_translation_unmarshal.go b/pkg/envoy/store/resource_translation_unmarshal.go new file mode 100644 index 000000000..95e625801 --- /dev/null +++ b/pkg/envoy/store/resource_translation_unmarshal.go @@ -0,0 +1,27 @@ +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 newResourceTranslation(l types.ResourceTranslationSet) (*resourceTranslation, error) { + res := l[0].Resource + _, ref, pp, err := resource.ParseResourceTranslation(res) + + return &resourceTranslation{ + locales: l, + + refResourceTranslation: res, + refLocaleRes: ref, + + refPathRes: pp, + }, err +} + +func (lr *resourceTranslation) MarshalEnvoy() ([]resource.Interface, error) { + return envoy.CollectNodes( + resource.NewResourceTranslation(lr.locales, lr.refResourceTranslation, lr.refLocaleRes, lr.refPathRes...), + ) +} diff --git a/pkg/envoy/store/system.go b/pkg/envoy/store/system.go index 0115b5703..21bd9bf7f 100644 --- a/pkg/envoy/store/system.go +++ b/pkg/envoy/store/system.go @@ -25,6 +25,7 @@ type ( resourceID map[uint64]bool strict bool } + resourceTranslationFilter types.ResourceTranslationFilter systemDecoder struct { resourceID []uint64 @@ -266,6 +267,7 @@ func (d *systemDecoder) decodeSettings(ctx context.Context, s store.Storer, ff [ mm: mm, } } + func (d *systemDecoder) decodeRbac(ctx context.Context, s store.Storer, ff []*rbacFilter) *auxRsp { mm := make([]envoy.Marshaller, 0, 100) if ff == nil { @@ -355,6 +357,81 @@ func (d *systemDecoder) decodeRbac(ctx context.Context, s store.Storer, ff []*rb } } +func (d *systemDecoder) decodeResourceTranslation(ctx context.Context, s store.Storer, ff []*resourceTranslationFilter) *auxRsp { + mm := make([]envoy.Marshaller, 0, 100) + if ff == nil { + return &auxRsp{ + mm: mm, + } + } + + var ( + resMap = make(map[string]*resourceTranslation) + + nn types.ResourceTranslationSet + fn types.ResourceTranslationFilter + err error + ) + + for _, f := range ff { + aux := *f + + if aux.Limit == 0 { + aux.Limit = 1000 + } + + for { + nn, fn, err = s.SearchResourceTranslations(ctx, types.ResourceTranslationFilter(aux)) + if err != nil { + return &auxRsp{ + err: err, + } + } + + for _, n := range nn { + if _, ok := resMap[n.Resource]; !ok { + resMap[n.Resource], err = newResourceTranslation(types.ResourceTranslationSet{n}) + if err != nil { + return &auxRsp{ + err: err, + } + } + + // parse the resource wo we can define relations/check if we can unmarshal it + _, ref, pp, err := resource.ParseResourceTranslation(n.Resource) + resMap[n.Resource].refLocaleRes = ref + resMap[n.Resource].refPathRes = pp + if err != nil { + return &auxRsp{ + err: err, + } + } + + continue + } + + resMap[n.Resource].locales = append(resMap[n.Resource].locales, n) + } + + if fn.NextPage != nil { + aux.PageCursor = fn.NextPage + } else { + break + } + + break + } + } + + for _, lr := range resMap { + mm = append(mm, lr) + } + + return &auxRsp{ + mm: mm, + } +} + func (df *DecodeFilter) systemFromResource(rr ...string) *DecodeFilter { for _, r := range rr { if !strings.HasPrefix(r, "system") { @@ -377,7 +454,8 @@ func (df *DecodeFilter) systemFromResource(rr ...string) *DecodeFilter { }) case "system:user": df = df.Users(&types.UserFilter{ - Query: id, + Query: id, + AllKinds: true, }) case "system:template": df = df.Templates(&types.TemplateFilter{ @@ -397,6 +475,8 @@ func (df *DecodeFilter) systemFromResource(rr ...string) *DecodeFilter { df = df.Settings(&types.SettingsFilter{}) case "system:rbac": df = df.Rbac(&rbac.RuleFilter{}) + case "system:resource-translation": + df = df.Rbac(&rbac.RuleFilter{}) } } @@ -462,6 +542,20 @@ func (df *DecodeFilter) Rbac(f *rbac.RuleFilter) *DecodeFilter { return df } +// ResourceTranslation adds a new ResourceTranslationFilter +func (df *DecodeFilter) ResourceTranslation(f *types.ResourceTranslationFilter) *DecodeFilter { + if df.resourceTranslations == nil { + df.resourceTranslations = make([]*resourceTranslationFilter, 0, 1) + } else { + // There can only be a single resourceTranslations filter + // since it makes no sense to have multiple of + return df + } + + df.resourceTranslations = append(df.resourceTranslations, (*resourceTranslationFilter)(f)) + return df +} + func (df *DecodeFilter) RbacStrict(f *rbac.RuleFilter) *DecodeFilter { if df.rbac == nil { df.rbac = make([]*rbacFilter, 0, 1) diff --git a/pkg/envoy/yaml/compose_module.go b/pkg/envoy/yaml/compose_module.go index 00c0124a8..342040d77 100644 --- a/pkg/envoy/yaml/compose_module.go +++ b/pkg/envoy/yaml/compose_module.go @@ -30,6 +30,7 @@ type ( relNs *types.Namespace refNamespace string rbac rbacRuleSet + locale resourceTranslationSet recTpl *composeRecordTpl } @@ -43,7 +44,8 @@ type ( relMod *types.Module - rbac rbacRuleSet + rbac rbacRuleSet + locale resourceTranslationSet } composeModuleFieldSet []*composeModuleField diff --git a/pkg/envoy/yaml/compose_module_unmarshal.go b/pkg/envoy/yaml/compose_module_unmarshal.go index 6f2c0a09e..2e4de736a 100644 --- a/pkg/envoy/yaml/compose_module_unmarshal.go +++ b/pkg/envoy/yaml/compose_module_unmarshal.go @@ -49,6 +49,10 @@ func (wrap *composeModule) UnmarshalYAML(n *yaml.Node) (err error) { return } + if wrap.locale, err = decodeLocale(n); err != nil { + return + } + if wrap.envoyConfig, err = decodeEnvoyConfig(n); err != nil { return } @@ -70,15 +74,11 @@ func (wrap *composeModule) UnmarshalYAML(n *yaml.Node) (err error) { return y7s.NodeErr(n, "field definition must be a map") } - var ( - aux = composeModuleFieldSet{} - ) - - if err = v.Decode(&aux); err != nil { + if err = v.Decode(&wrap.fields); err != nil { return err } - wrap.res.Fields = aux.set() + wrap.res.Fields = wrap.fields.set() return nil case "records": @@ -137,10 +137,47 @@ func (wrap composeModule) MarshalEnvoy() ([]resource.Interface, error) { crs = resource.NewComposeRecordTemplate(rs.Identifiers().First(), wrap.refNamespace, s.Source, s.Defaultable, mtt, s.Key...) } + // process module fields + // - update field-defined locale + // - collect default field locale + fieldTranslations := make(resourceTranslationSet, 0, len(wrap.fields)) + defaultFieldTranslations := make([]resource.Interface, 0) + for _, wrapField := range wrap.fields { + // generic field things + f := resource.NewComposeModuleField(wrapField.res, wrap.refNamespace, rs.Ref().Identifiers.First()) + rs.AddField(f) + fieldTranslations = append(fieldTranslations, wrapField.locale.bindResource(f)...) + + // default translations + dft, err := f.EncodeTranslations() + if err != nil { + return nil, err + } + for _, d := range dft { + d.MarkDefault() + defaultFieldTranslations = append(defaultFieldTranslations, d) + } + } + + // default module translations + var defaultModuleTranslations []resource.Interface + dft, err := rs.EncodeTranslations() + if err != nil { + return nil, err + } + for _, d := range dft { + d.MarkDefault() + defaultModuleTranslations = append(defaultModuleTranslations, d) + } + return envoy.CollectNodes( rs, crs, + defaultModuleTranslations, + defaultFieldTranslations, wrap.rbac.bindResource(rs), + wrap.locale.bindResource(rs), + fieldTranslations, ) } @@ -257,6 +294,10 @@ func (wrap *composeModuleField) UnmarshalYAML(n *yaml.Node) (err error) { return } + if wrap.locale, err = decodeLocale(n); err != nil { + return + } + return y7s.EachMap(n, func(k, v *yaml.Node) (err error) { switch k.Value { case "name": diff --git a/pkg/envoy/yaml/compose_namespace.go b/pkg/envoy/yaml/compose_namespace.go index 3d08b6ef0..d00d8b35e 100644 --- a/pkg/envoy/yaml/compose_namespace.go +++ b/pkg/envoy/yaml/compose_namespace.go @@ -32,6 +32,8 @@ type ( // module's RBAC rules rbac rbacRuleSet + + locale resourceTranslationSet } composeNamespaceSet []*composeNamespace diff --git a/pkg/envoy/yaml/compose_namespace_unmarshal.go b/pkg/envoy/yaml/compose_namespace_unmarshal.go index af0866e88..718a2faf9 100644 --- a/pkg/envoy/yaml/compose_namespace_unmarshal.go +++ b/pkg/envoy/yaml/compose_namespace_unmarshal.go @@ -61,6 +61,10 @@ func (wrap *composeNamespace) UnmarshalYAML(n *yaml.Node) (err error) { return } + if wrap.locale, err = decodeLocale(n); err != nil { + return + } + if wrap.envoyConfig, err = decodeEnvoyConfig(n); err != nil { return } @@ -113,12 +117,24 @@ func (wrap composeNamespace) MarshalEnvoy() ([]resource.Interface, error) { nsr.SetTimestamps(wrap.ts) nsr.SetConfig(wrap.envoyConfig) + var defaultNsTranslations []resource.Interface + dft, err := nsr.EncodeTranslations() + if err != nil { + return nil, err + } + for _, t := range dft { + t.MarkDefault() + defaultNsTranslations = append(defaultNsTranslations, t) + } + return envoy.CollectNodes( nsr, + defaultNsTranslations, wrap.modules, wrap.pages, wrap.records, wrap.charts, wrap.rbac.bindResource(nsr), + wrap.locale.bindResource(nsr), ) } diff --git a/pkg/envoy/yaml/document.go b/pkg/envoy/yaml/document.go index 90f334ac2..176bf41e8 100644 --- a/pkg/envoy/yaml/document.go +++ b/pkg/envoy/yaml/document.go @@ -21,6 +21,7 @@ type ( applications applicationSet settings settingSet rbac rbacRuleSet + locale resourceTranslationSet cfg *EncoderConfig } @@ -39,6 +40,10 @@ func (doc *Document) UnmarshalYAML(n *yaml.Node) (err error) { return } + if doc.locale, err = decodeLocale(n); err != nil { + return + } + return y7s.EachMap(n, func(k, v *yaml.Node) error { switch k.Value { case "roles": @@ -134,6 +139,15 @@ func (doc *Document) MarshalYAML() (interface{}, error) { } } + if doc.locale != nil { + doc.templates.configureEncoder(doc.cfg) + + dn, err = encodeResource(dn, "locale", doc.locale, doc.cfg.MappedOutput, "lang") + if err != nil { + return nil, err + } + } + if doc.rbac != nil && len(doc.rbac) > 0 { // rbac doesn't support map representation m, err := encodeNode(doc.rbac) @@ -177,6 +191,9 @@ func (doc *Document) Decode(ctx context.Context) ([]resource.Interface, error) { mm = append(mm, s) } } + if doc.locale != nil { + mm = append(mm, doc.locale) + } if doc.rbac != nil { mm = append(mm, doc.rbac) } @@ -306,6 +323,14 @@ func (doc *Document) addRbacRule(r *rbacRule) { doc.rbac = append(doc.rbac, r) } +func (doc *Document) addResourceTranslation(l *resourceTranslation) { + if doc.locale == nil { + doc.locale = make(resourceTranslationSet, 0, 100) + } + + doc.locale = append(doc.locale, l) +} + func (doc *Document) nestComposeModule(ns string, m *composeModule) error { if doc.compose == nil { doc.compose = &compose{} diff --git a/pkg/envoy/yaml/encoder.go b/pkg/envoy/yaml/encoder.go index 6399fdf1f..449112013 100644 --- a/pkg/envoy/yaml/encoder.go +++ b/pkg/envoy/yaml/encoder.go @@ -134,6 +134,8 @@ func (ye *yamlEncoder) Prepare(ctx context.Context, ee ...*envoy.ResourceState) err = f(settingFromResource(res, ye.cfg), e) case *resource.RbacRule: err = f(rbacRuleFromResource(res, ye.cfg), e) + case *resource.ResourceTranslation: + err = f(resourceTranslationFromResource(res, ye.cfg), e) // Automation resources case *resource.AutomationWorkflow: diff --git a/pkg/envoy/yaml/resource_translation.go b/pkg/envoy/yaml/resource_translation.go new file mode 100644 index 000000000..37d3b02a6 --- /dev/null +++ b/pkg/envoy/yaml/resource_translation.go @@ -0,0 +1,54 @@ +package yaml + +import ( + "github.com/cortezaproject/corteza-server/pkg/envoy/resource" + "github.com/cortezaproject/corteza-server/system/types" +) + +type ( + resourceTranslation struct { + // set of locales + locales types.ResourceTranslationSet + + // point to the locale resource + refResourceTranslation string + refLocaleRes *resource.Ref + + // PathRes and PathResource slices hold parent resources we should nest the rule by + refPathRes []*resource.Ref + } + + resourceTranslationSet []*resourceTranslation +) + +func (ll resourceTranslationSet) groupByResourceTranslation() (out resourceTranslationSet) { + resMap := make(map[string]*resourceTranslation) + + for _, _l := range ll { + l := _l + + if _, ok := resMap[l.refResourceTranslation]; !ok { + resMap[l.refResourceTranslation] = l + continue + } + + resMap[l.refResourceTranslation].locales = append(resMap[l.refResourceTranslation].locales, l.locales...) + } + + for _, l := range resMap { + out = append(out, l) + } + + return +} + +func resourceTranslationFromResource(r *resource.ResourceTranslation, cfg *EncoderConfig) *resourceTranslation { + return &resourceTranslation{ + locales: r.Res, + + refResourceTranslation: r.Res[0].Resource, + refLocaleRes: r.RefRes, + + refPathRes: r.RefPath, + } +} diff --git a/pkg/envoy/yaml/resource_translation_marshal.go b/pkg/envoy/yaml/resource_translation_marshal.go new file mode 100644 index 000000000..3cbbb7f40 --- /dev/null +++ b/pkg/envoy/yaml/resource_translation_marshal.go @@ -0,0 +1,366 @@ +package yaml + +import ( + "context" + "fmt" + "sort" + "strconv" + "strings" + + automationTypes "github.com/cortezaproject/corteza-server/automation/types" + composeTypes "github.com/cortezaproject/corteza-server/compose/types" + "github.com/cortezaproject/corteza-server/pkg/envoy" + "github.com/cortezaproject/corteza-server/pkg/envoy/resource" + "golang.org/x/text/language" +) + +type ( + auxLocaleMarshalRes struct { + res string + parts []string + keys [][2]string + nested []*auxLocaleMarshalRes + } + auxLocaleMarshalLang struct { + lang string + res []*auxLocaleMarshalRes + } +) + +func (n *resourceTranslation) Prepare(ctx context.Context, state *envoy.ResourceState) (err error) { + // For now we will only allow resource specific resource translations if that resource is + // also present. + // Here we check if we can find it in case we're handling a resource specific rule. + // + // @todo how can we improve this? + res := state.Res.(*resource.ResourceTranslation) + refRes := res.RefRes + if refRes.ResourceType == composeTypes.ModuleFieldResourceType { + for _, r := range state.ParentResources { + if r.ResourceType() == composeTypes.ModuleResourceType && r.Identifiers().HasAny(res.RefPath[1].Identifiers) { + return + } + } + } else { + for _, r := range state.ParentResources { + if refRes.ResourceType == r.ResourceType() && r.Identifiers().HasAny(refRes.Identifiers) { + return + } + } + + // We couldn't find it... + return resource.ResourceTranslationErrNotFound(refRes.Identifiers) + } + + return nil +} + +func (r *resourceTranslation) Encode(ctx context.Context, doc *Document, state *envoy.ResourceState) (err error) { + // @todo Improve locale placement + // + // In cases where a specific rule is created for a specific resource, nest the rule + // under the related namespace. + // For now all rules will be nested under a root node for simplicity sake. + + refResource, err := r.makeResourceTranslationResource(state) + if err != nil { + return err + } + + r.refResourceTranslation = refResource + + doc.addResourceTranslation(r) + + return nil +} + +func (r *resourceTranslation) makeResourceTranslationResource(state *envoy.ResourceState) (string, error) { + res := state.Res.(*resource.ResourceTranslation) + + p0ID := "*" + p1ID := "*" + p2ID := "*" + + switch r.refLocaleRes.ResourceType { + case composeTypes.NamespaceResourceType: + if res.RefRes != nil { + p1 := resource.FindComposeNamespace(state.ParentResources, res.RefRes.Identifiers) + if p1 == nil { + return "", resource.ComposeNamespaceErrUnresolved(res.RefRes.Identifiers) + } + p1ID = p1.Slug + } + + return fmt.Sprintf(composeTypes.NamespaceResourceTranslationTpl(), composeTypes.NamespaceResourceTranslationType, p1ID), nil + + case composeTypes.ModuleResourceType: + if len(res.RefPath) > 0 { + p0 := resource.FindComposeNamespace(state.ParentResources, res.RefPath[0].Identifiers) + if p0 == nil { + return "", resource.ComposeNamespaceErrUnresolved(res.RefPath[0].Identifiers) + } + p0ID = p0.Slug + } + + if res.RefRes != nil { + p1 := resource.FindComposeModule(state.ParentResources, res.RefRes.Identifiers) + if p1 == nil { + return "", resource.ComposeModuleErrUnresolved(res.RefRes.Identifiers) + } + p1ID = p1.Handle + } + + return fmt.Sprintf(composeTypes.ModuleResourceTranslationTpl(), composeTypes.ModuleResourceTranslationType, p0ID, p1ID), nil + + case composeTypes.PageResourceType: + if len(res.RefPath) > 0 { + p0 := resource.FindComposeNamespace(state.ParentResources, res.RefPath[0].Identifiers) + if p0 == nil { + return "", resource.ComposeNamespaceErrUnresolved(res.RefPath[0].Identifiers) + } + p0ID = p0.Slug + } + + if res.RefRes != nil { + p1 := resource.FindComposePage(state.ParentResources, res.RefRes.Identifiers) + if p1 == nil { + return "", resource.ComposePageErrUnresolved(res.RefRes.Identifiers) + } + + if p1.Handle == "" { + p1ID = strconv.FormatUint(p1.ID, 10) + } else { + p1ID = p1.Handle + } + } + + return fmt.Sprintf(composeTypes.PageResourceTranslationTpl(), composeTypes.PageResourceTranslationType, p0ID, p1ID), nil + + case composeTypes.ModuleFieldResourceType: + p0 := resource.FindComposeNamespace(state.ParentResources, res.RefPath[0].Identifiers) + if p0 == nil { + return "", resource.ComposeNamespaceErrUnresolved(res.RefPath[0].Identifiers) + } + p0ID = p0.Slug + + p1 := resource.FindComposeModule(state.ParentResources, res.RefPath[1].Identifiers) + if p1 == nil { + return "", resource.ComposeModuleErrUnresolved(res.RefPath[1].Identifiers) + } + p1ID = p1.Handle + + // field + f := resource.FindComposeModuleField(p1, res.RefRes.Identifiers) + if f == nil { + return "", resource.ComposeModuleFieldErrUnresolved(res.RefRes.Identifiers) + } + p2ID = f.Name + + return fmt.Sprintf(composeTypes.ModuleFieldResourceTranslationTpl(), composeTypes.ModuleFieldResourceTranslationType, p0ID, p1ID, p2ID), nil + + case composeTypes.ChartResourceType: + if len(res.RefPath) > 0 { + p0 := resource.FindComposeNamespace(state.ParentResources, res.RefPath[0].Identifiers) + if p0 == nil { + return "", resource.ComposeNamespaceErrUnresolved(res.RefPath[0].Identifiers) + } + p0ID = p0.Slug + } + + if res.RefRes != nil { + p1 := resource.FindComposeChart(state.ParentResources, res.RefRes.Identifiers) + if p1 == nil { + return "", resource.ComposeChartErrUnresolved(res.RefRes.Identifiers) + } + p1ID = p1.Handle + } + + return fmt.Sprintf(composeTypes.ChartResourceTranslationTpl(), composeTypes.ChartResourceTranslationType, p0ID, p1ID), nil + + case automationTypes.WorkflowResourceType: + if res.RefRes != nil { + p0 := resource.FindAutomationWorkflow(state.ParentResources, res.RefRes.Identifiers) + if p0 == nil { + return "", resource.AutomationWorkflowErrUnresolved(res.RefRes.Identifiers) + } + p0ID = p0.Handle + } + + return fmt.Sprintf(automationTypes.WorkflowResourceTranslationTpl(), automationTypes.WorkflowResourceTranslationType, p0ID), nil + + default: + return "", fmt.Errorf("unsupported resource type '%s' for locale resource YAML encode", r.refLocaleRes.ResourceType) + } +} + +func (ll resourceTranslationSet) MarshalYAML() (interface{}, error) { + + // Flow outline + // . group by language & trans. resource + // . sort and slice based on the resource path + // . nest resources based on their path parameters + // . prepare YAML nodes + + var err error + _ = err + + if ll == nil || len(ll) == 0 { + return nil, nil + } + + // . group by language & trans. resource + // + // We use an array+map combo to help assure consistent order when outputing. + resourceList := make([]*auxLocaleMarshalLang, 0, 2) + lngIndex := make(map[language.Tag]int) + resIndex := make(map[string]int) + for _, l := range ll { + for _, lr := range l.locales { + lang := lr.Lang.Tag + + i, ok := lngIndex[lang] + if !ok { + i = len(resourceList) + lngIndex[lang] = i + resourceList = append(resourceList, &auxLocaleMarshalLang{ + lang: lang.String(), + res: make([]*auxLocaleMarshalRes, 0, 4), + }) + } + + key := fmt.Sprintf("%s/%s", lang.String(), l.refResourceTranslation) + j, ok := resIndex[key] + if !ok { + j = len(resourceList[i].res) + resIndex[key] = j + resourceList[i].res = append(resourceList[i].res, &auxLocaleMarshalRes{res: l.refResourceTranslation, keys: make([][2]string, 0, 2)}) + } + + resourceList[i].res[j].keys = append(resourceList[i].res[j].keys, [2]string{lr.K, lr.Message}) + } + } + + for li, l := range resourceList { + // . sort and slice based on the resource path + // + // Firstly sort; have resources with less path items (slashes) at the top. + sort.Slice(l.res, func(i, j int) bool { + a := l.res[i] + b := l.res[j] + + a.parts = strings.Split(a.res, "/") + b.parts = strings.Split(b.res, "/") + + return len(a.parts) < len(b.parts) + }) + // + // Next chunk; go through the slice and group resources based on path items. + // Since they are ordered, this is a simple traversal. + bySize := make([][]*auxLocaleMarshalRes, 0) + for i := len(l.res) - 1; i >= 0; i-- { + if i == len(l.res)-1 { + bySize = append(bySize, []*auxLocaleMarshalRes{l.res[i]}) + continue + } + + if len(l.res[i].parts) != len(l.res[i+1].parts) { + bySize = append(bySize, []*auxLocaleMarshalRes{l.res[i]}) + } else { + bySize[len(bySize)-1] = append(bySize[len(bySize)-1], l.res[i]) + } + } + + // . nest resources based on their path parameters. + // + // Going from most specific resources to less specific resources assures + // that we will have a correct and minimal output. + for i := 1; i < len(bySize); i++ { + aa := bySize[i-1] + bb := bySize[i] + + for _, b := range bb { + for i, a := range aa { + // We're not deleting rows, just setting to nil + if a == nil { + continue + } + + if isAuxLocaleParent(a, b) { + b.nested = append(b.nested, a) + aa[i] = nil + } + } + } + } + // + // Lastly, remove the nested hierarchy for easier YAML node construction + tmpRes := make([]*auxLocaleMarshalRes, 0) + for i := len(bySize) - 1; i >= 0; i-- { + bs := bySize[i] + for _, b := range bs { + tmpRes = append(tmpRes, unpackAuxLocale(b)...) + } + } + + l.res = tmpRes + resourceList[li] = l + } + + // . prepare YAML nodes + localeNode, _ := makeMap() + for _, byLang := range resourceList { + resNode, _ := makeMap() + + for _, res := range byLang.res { + kmNode, _ := makeMap() + + for _, km := range res.keys { + kmNode, err = addMap(kmNode, km[0], km[1]) + if err != nil { + return nil, err + } + } + + resNode, err = addMap(resNode, res.res, kmNode) + if err != nil { + return nil, err + } + + } + + localeNode, err = addMap(localeNode, byLang.lang, resNode) + if err != nil { + return nil, err + } + } + + return localeNode, nil +} + +func isAuxLocaleParent(main, sub *auxLocaleMarshalRes) bool { + for i, p := range sub.parts { + if i == 0 { + continue + } + if main.parts[i] != p { + return false + } + } + + return true +} + +func unpackAuxLocale(l *auxLocaleMarshalRes) []*auxLocaleMarshalRes { + if l == nil { + return nil + } + + out := make([]*auxLocaleMarshalRes, 0) + nn := l.nested + l.nested = nil + out = append(out, l) + for _, n := range nn { + out = append(out, unpackAuxLocale(n)...) + } + return out +} diff --git a/pkg/envoy/yaml/resource_translation_unmarshal.go b/pkg/envoy/yaml/resource_translation_unmarshal.go new file mode 100644 index 000000000..25db8d410 --- /dev/null +++ b/pkg/envoy/yaml/resource_translation_unmarshal.go @@ -0,0 +1,120 @@ +package yaml + +import ( + "fmt" + + "github.com/cortezaproject/corteza-server/pkg/envoy/resource" + "github.com/cortezaproject/corteza-server/pkg/y7s" + "github.com/cortezaproject/corteza-server/system/types" + "golang.org/x/text/language" + "gopkg.in/yaml.v3" +) + +func decodeLocale(n *yaml.Node) (resourceTranslationSet, error) { + var ( + rr = make(resourceTranslationSet, 0, 20) + ) + + err := y7s.EachMap(n, func(k, v *yaml.Node) (err error) { + if k.Value != "locale" { + return nil + } + + return y7s.EachMap(v, func(k, v *yaml.Node) error { + lang := "" + if err = y7s.DecodeScalar(k, "language", &lang); err != nil { + return err + } + + rr, err = rr.decodeLocale(lang, v) + if err != nil { + return err + } + return nil + }) + }) + + return rr, err +} + +func (rr resourceTranslationSet) decodeLocale(lang string, locRes *yaml.Node) (oo resourceTranslationSet, err error) { + if rr == nil { + oo = make(resourceTranslationSet, 0, 10) + } else { + oo = rr + } + + parseOps := func(km *yaml.Node, lang, resource string) error { + return y7s.EachMap(km, func(k, msg *yaml.Node) error { + lr := &resourceTranslation{ + locales: types.ResourceTranslationSet{{ + Lang: types.Lang{Tag: language.Make(lang)}, + Resource: resource, + K: k.Value, + Message: msg.Value, + }}, + } + + if resource != "" { + if err = lr.setResourceTranslation(resource); err != nil { + return fmt.Errorf("failed to decode locale resource: %w", err) + } + } + + oo = append(oo, lr) + return nil + }) + } + + // If its a mapping node, keys represent resources + if locRes.Content[1].Kind == yaml.MappingNode { + return oo, y7s.EachMap(locRes, func(k, v *yaml.Node) error { + return parseOps(v, lang, k.Value) + }) + } else { + err = parseOps(locRes, lang, "") + if err != nil { + return nil, err + } + } + + return +} + +func (rr resourceTranslationSet) bindResource(resI resource.Interface) resourceTranslationSet { + res, ref, pp := resI.(resource.LocaleInterface).ResourceTranslationParts() + + rtr := make(resourceTranslationSet, 0, len(rr)) + for _, r := range rr { + + r.refResourceTranslation = res + r.refLocaleRes = ref + r.refPathRes = pp + rtr = append(rtr, r) + } + + return rtr +} + +func (rr resourceTranslationSet) MarshalEnvoy() ([]resource.Interface, error) { + rr = rr.groupByResourceTranslation() + + var nn = make([]resource.Interface, 0, len(rr)) + + for _, r := range rr { + nn = append(nn, resource.NewResourceTranslation(r.locales, r.refResourceTranslation, r.refLocaleRes, r.refPathRes...)) + } + return nn, nil +} + +func (r *resourceTranslation) setResourceTranslation(res string) error { + _, ref, pp, err := resource.ParseResourceTranslation(res) + if err != nil { + return err + } + + r.refResourceTranslation = res + r.refLocaleRes = ref + r.refPathRes = pp + return nil +}