3
0

Extend envoy to support resource translations

This commit is contained in:
Tomaž Jerman
2021-09-15 17:24:09 +02:00
parent a33856b66c
commit 895b40da36
38 changed files with 1891 additions and 57 deletions

View File

@@ -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 }}

View File

@@ -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
}

View File

@@ -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 }}

View File

@@ -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
}

65
pkg/envoy/locale.go Normal file
View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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())
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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,
)
}

View File

@@ -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),
)

View File

@@ -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:

View File

@@ -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())
}

View File

@@ -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
}
}

View File

@@ -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...),
)
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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":

View File

@@ -32,6 +32,8 @@ type (
// module's RBAC rules
rbac rbacRuleSet
locale resourceTranslationSet
}
composeNamespaceSet []*composeNamespace

View File

@@ -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),
)
}

View File

@@ -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{}

View File

@@ -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:

View File

@@ -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,
}
}

View File

@@ -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
}

View File

@@ -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
}