snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s

This commit is contained in:
upstream-mirror
2026-09-18 01:22:23 +05:00
commit 7e385fc126
8391 changed files with 1766236 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
package yaml
import (
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
apiGateway struct {
res *types.ApigwRoute
filters apiGwFilterSet
ts *resource.Timestamps
us *resource.Userstamps
envoyConfig *resource.EnvoyConfig
encoderConfig *EncoderConfig
rbac rbacRuleSet
}
apiGatewaySet []*apiGateway
apiGwFilter struct {
res *types.ApigwFilter
ts *resource.Timestamps
us *resource.Userstamps
envoyConfig *resource.EnvoyConfig
encoderConfig *EncoderConfig
}
apiGwFilterSet []*apiGwFilter
)
func (nn apiGatewaySet) configureEncoder(cfg *EncoderConfig) {
for _, n := range nn {
n.encoderConfig = cfg
}
}

View File

@@ -0,0 +1,116 @@
package yaml
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
systemTypes "github.com/cortezaproject/corteza/server/system/types"
)
func apiGatewayFromResource(r *resource.APIGateway, cfg *EncoderConfig) *apiGateway {
ff := make(apiGwFilterSet, len(r.Filters))
for i, t := range r.Filters {
ff[i] = &apiGwFilter{
res: t.Res,
encoderConfig: cfg,
}
}
return &apiGateway{
res: r.Res,
filters: ff,
encoderConfig: cfg,
}
}
func (n *apiGateway) Prepare(ctx context.Context, state *envoy.ResourceState) (err error) {
wf, ok := state.Res.(*resource.APIGateway)
if !ok {
return encoderErrInvalidResource(systemTypes.ApigwRouteResourceType, state.Res.ResourceType())
}
n.res = wf.Res
n.us = wf.Userstamps()
return nil
}
func (n *apiGateway) Encode(ctx context.Context, doc *Document, state *envoy.ResourceState) (err error) {
if n.res.ID <= 0 {
n.res.ID = nextID()
}
n.ts, err = resource.MakeTimestampsCUDA(&n.res.CreatedAt, n.res.UpdatedAt, n.res.DeletedAt, nil).
Model(n.encoderConfig.TimeLayout, n.encoderConfig.Timezone)
if err != nil {
return err
}
n.us, err = resolveUserstamps(state.ParentResources, n.us)
if err != nil {
return err
}
// @todo skip eval?
doc.addApiGateway(n)
return err
}
func (g *apiGateway) MarshalYAML() (interface{}, error) {
var err error
nn, err := makeMap(
"endpoint", g.res.Endpoint,
"method", g.res.Method,
"enabled", g.res.Enabled,
"group", g.res.Group,
"meta", g.res.Meta,
"filters", g.filters,
)
if err != nil {
return nil, err
}
nn, err = encodeTimestamps(nn, g.ts)
if err != nil {
return nil, err
}
nn, err = encodeUserstamps(nn, g.us)
if err != nil {
return nil, err
}
return nn, nil
}
func (f *apiGwFilter) MarshalYAML() (interface{}, error) {
var err error
nn, err := makeMap(
"weight", f.res.Weight,
"ref", f.res.Ref,
"kind", f.res.Kind,
"enabled", f.res.Enabled,
"params", f.res.Params,
)
if err != nil {
return nil, err
}
nn, err = encodeTimestamps(nn, f.ts)
if err != nil {
return nil, err
}
nn, err = encodeUserstamps(nn, f.us)
if err != nil {
return nil, err
}
return nn, nil
}

View File

@@ -0,0 +1,156 @@
package yaml
import (
"strings"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/pkg/y7s"
"github.com/cortezaproject/corteza/server/system/types"
"gopkg.in/yaml.v3"
)
func (wset *apiGatewaySet) UnmarshalYAML(n *yaml.Node) error {
return y7s.Each(n, func(k, v *yaml.Node) (err error) {
var (
wrap = &apiGateway{}
)
if v == nil {
return y7s.NodeErr(n, "malformed api gateway definition")
}
if err = v.Decode(&wrap); err != nil {
return
}
*wset = append(*wset, wrap)
return
})
}
func (wrap *apiGateway) UnmarshalYAML(n *yaml.Node) (err error) {
if wrap.res == nil {
wrap.rbac = make(rbacRuleSet, 0, 10)
wrap.res = &types.ApigwRoute{}
}
if wrap.rbac, err = decodeRbac(n); err != nil {
return
}
if wrap.envoyConfig, err = decodeEnvoyConfig(n); err != nil {
return
}
if wrap.ts, err = decodeTimestamps(n); err != nil {
return
}
if wrap.us, err = decodeUserstamps(n); err != nil {
return
}
return y7s.EachMap(n, func(k, v *yaml.Node) (err error) {
switch strings.ToLower(k.Value) {
case "endpoint":
return y7s.DecodeScalar(v, "api gw endpoint", &wrap.res.Endpoint)
case "method":
return y7s.DecodeScalar(v, "api gw method", &wrap.res.Method)
case "enabled":
return y7s.DecodeScalar(v, "api gw enabled", &wrap.res.Enabled)
case "group":
return y7s.DecodeScalar(v, "api gw group", &wrap.res.Group)
case "meta":
aux := &types.ApigwRouteMeta{}
err = v.Decode(&aux)
if err != nil {
return err
}
wrap.res.Meta = *aux
return nil
case "filters":
wrap.filters = make(apiGwFilterSet, 0, 10)
err = v.Decode(&wrap.filters)
if err != nil {
return err
}
}
return nil
})
}
func (wrap *apiGwFilter) UnmarshalYAML(n *yaml.Node) (err error) {
if wrap.res == nil {
wrap.res = &types.ApigwFilter{}
}
if wrap.envoyConfig, err = decodeEnvoyConfig(n); err != nil {
return
}
if wrap.ts, err = decodeTimestamps(n); err != nil {
return
}
if wrap.us, err = decodeUserstamps(n); err != nil {
return
}
return y7s.EachMap(n, func(k, v *yaml.Node) (err error) {
switch strings.ToLower(k.Value) {
case "weight":
return y7s.DecodeScalar(v, "route filter weight", &wrap.res.Weight)
case "ref":
return y7s.DecodeScalar(v, "route filter ref", &wrap.res.Ref)
case "kind":
return y7s.DecodeScalar(v, "route filter kind", &wrap.res.Kind)
case "enabled":
return y7s.DecodeScalar(v, "route filter enabled", &wrap.res.Enabled)
case "params":
return v.Decode(&wrap.res.Params)
}
return nil
})
}
func (wset apiGatewaySet) MarshalEnvoy() ([]resource.Interface, error) {
nn := make([]resource.Interface, 0, len(wset)*2)
for _, res := range wset {
if tmp, err := res.MarshalEnvoy(); err != nil {
return nil, err
} else {
nn = append(nn, tmp...)
}
}
return nn, nil
}
func (wrap apiGateway) MarshalEnvoy() ([]resource.Interface, error) {
rs := resource.NewAPIGateway(wrap.res)
rs.SetTimestamps(wrap.ts)
rs.SetUserstamps(wrap.us)
rs.SetConfig(wrap.envoyConfig)
for _, f := range wrap.filters {
trs := rs.AddGatewayFilter(f.res)
trs.SetTimestamps(f.ts)
trs.SetUserstamps(f.us)
}
return envoy.CollectNodes(
rs,
wrap.rbac.bindResource(rs),
)
}

View File

@@ -0,0 +1,26 @@
package yaml
import (
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
application struct {
res *types.Application
ts *resource.Timestamps
us *resource.Userstamps
envoyConfig *resource.EnvoyConfig
encoderConfig *EncoderConfig
rbac rbacRuleSet
}
applicationSet []*application
)
func (nn applicationSet) configureEncoder(cfg *EncoderConfig) {
for _, n := range nn {
n.encoderConfig = cfg
}
}

View File

@@ -0,0 +1,81 @@
package yaml
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/system/types"
)
func applicationFromResource(r *resource.Application, cfg *EncoderConfig) *application {
return &application{
res: r.Res,
encoderConfig: cfg,
}
}
func (n *application) Prepare(ctx context.Context, state *envoy.ResourceState) (err error) {
app, ok := state.Res.(*resource.Application)
if !ok {
return encoderErrInvalidResource(types.ApplicationResourceType, state.Res.ResourceType())
}
n.res = app.Res
n.us = app.Userstamps()
return nil
}
func (n *application) Encode(ctx context.Context, doc *Document, state *envoy.ResourceState) (err error) {
if n.res.ID <= 0 {
n.res.ID = nextID()
}
n.ts, err = resource.MakeTimestampsCUDA(&n.res.CreatedAt, n.res.UpdatedAt, n.res.DeletedAt, nil).
Model(n.encoderConfig.TimeLayout, n.encoderConfig.Timezone)
if err != nil {
return err
}
n.us, err = resolveUserstamps(state.ParentResources, n.us)
if err != nil {
return err
}
// @todo 1skip eval?
doc.addApplication(n)
return err
}
func (c *application) MarshalYAML() (interface{}, error) {
var err error
nn, err := makeMap(
"name", c.res.Name,
"enabled", c.res.Enabled,
"weight", c.res.Weight,
"unify", c.res.Unify,
"labels", c.res.Labels,
)
if err != nil {
return nil, err
}
nn, err = encodeTimestamps(nn, c.ts)
if err != nil {
return nil, err
}
if c.us != nil {
nn, err = addMap(nn, "owner", c.us.OwnedBy)
if err != nil {
return nil, err
}
}
return nn, nil
}

View File

@@ -0,0 +1,49 @@
package yaml
import (
"testing"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
func TestApplication_UnmarshalYAML(t *testing.T) {
var (
parseString = func(src string) (*application, error) {
w := &application{}
return w, yaml.Unmarshal([]byte(src), w)
}
)
t.Run("empty", func(t *testing.T) {
req := require.New(t)
w, err := parseString(``)
req.NoError(err)
req.NotNil(w)
req.Nil(w.res)
})
t.Run("simple name", func(t *testing.T) {
req := require.New(t)
w, err := parseString(`{ name: Test }`)
req.NoError(err)
req.NotNil(w)
req.NotNil(w.res)
req.NotEmpty(w.res.Name)
})
t.Run("application 1", func(t *testing.T) {
req := require.New(t)
doc, err := parseDocument("application_1")
req.NoError(err)
req.NotNil(doc)
req.Len(doc.applications, 2)
req.NotNil(doc.applications[0])
req.NotNil(doc.applications[1])
req.Equal("one", doc.applications[0].res.Name)
req.Equal("two", doc.applications[1].res.Name)
})
}

View File

@@ -0,0 +1,98 @@
package yaml
import (
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/pkg/y7s"
"github.com/cortezaproject/corteza/server/system/types"
"gopkg.in/yaml.v3"
)
func (wset *applicationSet) UnmarshalYAML(n *yaml.Node) error {
return y7s.EachSeq(n, func(v *yaml.Node) (err error) {
var (
wrap = &application{}
)
if v == nil {
return y7s.NodeErr(n, "malformed application definition")
}
wrap.res = &types.Application{
Enabled: true,
}
switch v.Kind {
case yaml.ScalarNode:
if err = y7s.DecodeScalar(v, "application name", &wrap.res.Name); err != nil {
return
}
case yaml.MappingNode:
if err = v.Decode(&wrap); err != nil {
return
}
}
*wset = append(*wset, wrap)
return
})
}
func (wrap *application) UnmarshalYAML(n *yaml.Node) (err error) {
if !y7s.IsKind(n, yaml.MappingNode) {
return y7s.NodeErr(n, "application definition must be a map")
}
if wrap.res == nil {
wrap.res = &types.Application{}
}
if wrap.envoyConfig, err = decodeEnvoyConfig(n); err != nil {
return
}
if wrap.ts, err = decodeTimestamps(n); err != nil {
return
}
if wrap.us, err = decodeUserstamps(n); err != nil {
return
}
if err = n.Decode(&wrap.res); err != nil {
return
}
if wrap.rbac, err = decodeRbac(n); err != nil {
return
}
return nil
}
func (wset applicationSet) MarshalEnvoy() ([]resource.Interface, error) {
nn := make([]resource.Interface, 0, len(wset))
for _, res := range wset {
if tmp, err := res.MarshalEnvoy(); err != nil {
return nil, err
} else {
nn = append(nn, tmp...)
}
}
return nn, nil
}
func (wrap application) MarshalEnvoy() ([]resource.Interface, error) {
rs := resource.NewApplication(wrap.res)
rs.SetTimestamps(wrap.ts)
rs.SetUserstamps(wrap.us)
rs.SetConfig(wrap.envoyConfig)
return envoy.CollectNodes(
rs,
wrap.rbac.bindResource(rs),
)
}

View File

@@ -0,0 +1,64 @@
package yaml
import (
"reflect"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/pkg/y7s"
"gopkg.in/yaml.v3"
)
type (
automation struct {
Workflows automationWorkflowSet
EncoderConfig *EncoderConfig `yaml:"-"`
}
)
func (c *automation) MarshalYAML() (interface{}, error) {
cn, _ := makeMap()
var err error
if len(c.Workflows) > 0 {
c.Workflows.configureEncoder(c.EncoderConfig)
cn, err = encodeResource(cn, "workflows", c.Workflows, c.EncoderConfig.MappedOutput, "handle")
if err != nil {
return nil, err
}
}
return cn, nil
}
func (a *automation) UnmarshalYAML(n *yaml.Node) error {
var err error
return y7s.EachMap(n, func(k, v *yaml.Node) error {
switch k.Value {
case "workflows":
err = v.Decode(&a.Workflows)
if err != nil {
return err
}
}
return nil
})
}
func (a automation) MarshalEnvoy() ([]resource.Interface, error) {
nn := make([]resource.Interface, 0, 100)
rf := reflect.ValueOf(a)
for i := 0; i < rf.NumField(); i++ {
if mr, ok := rf.Field(i).Interface().(EnvoyMarshler); ok {
tmp, err := mr.MarshalEnvoy()
if err != nil {
return nil, err
}
nn = append(nn, tmp...)
}
}
return nn, nil
}

View File

@@ -0,0 +1,63 @@
package yaml
import (
"github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
)
type (
automationWorkflow struct {
res *types.Workflow
triggers automationTriggerSet
steps automationWorkflowStepSet
paths automationWorkflowPathSet
ts *resource.Timestamps
us *resource.Userstamps
envoyConfig *resource.EnvoyConfig
encoderConfig *EncoderConfig
rbac rbacRuleSet
}
automationWorkflowSet []*automationWorkflow
automationTrigger struct {
res *types.Trigger
ts *resource.Timestamps
us *resource.Userstamps
envoyConfig *resource.EnvoyConfig
encoderConfig *EncoderConfig
}
automationTriggerSet []*automationTrigger
automationWorkflowStep struct {
res *types.WorkflowStep
ts *resource.Timestamps
us *resource.Userstamps
envoyConfig *resource.EnvoyConfig
encoderConfig *EncoderConfig
}
automationWorkflowStepSet []*automationWorkflowStep
automationWorkflowPath struct {
res *types.WorkflowPath
ts *resource.Timestamps
us *resource.Userstamps
envoyConfig *resource.EnvoyConfig
encoderConfig *EncoderConfig
}
automationWorkflowPathSet []*automationWorkflowPath
)
func (nn automationWorkflowSet) configureEncoder(cfg *EncoderConfig) {
for _, n := range nn {
n.encoderConfig = cfg
}
}

View File

@@ -0,0 +1,147 @@
package yaml
import (
"context"
automationTypes "github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
)
func automationWorkflowFromResource(r *resource.AutomationWorkflow, cfg *EncoderConfig) *automationWorkflow {
tt := make(automationTriggerSet, len(r.Triggers))
for i, t := range r.Triggers {
tt[i] = &automationTrigger{
res: t.Res,
encoderConfig: cfg,
}
}
ss := make(automationWorkflowStepSet, len(r.Steps))
for i, s := range r.Steps {
ss[i] = &automationWorkflowStep{
res: s.Res,
encoderConfig: cfg,
}
}
pp := make(automationWorkflowPathSet, len(r.Paths))
for i, p := range r.Paths {
pp[i] = &automationWorkflowPath{
res: p.Res,
encoderConfig: cfg,
}
}
return &automationWorkflow{
res: r.Res,
triggers: tt,
steps: ss,
paths: pp,
encoderConfig: cfg,
}
}
func (n *automationWorkflow) Prepare(ctx context.Context, state *envoy.ResourceState) (err error) {
wf, ok := state.Res.(*resource.AutomationWorkflow)
if !ok {
return encoderErrInvalidResource(automationTypes.WorkflowResourceType, state.Res.ResourceType())
}
n.res = wf.Res
n.us = wf.Userstamps()
return nil
}
func (n *automationWorkflow) Encode(ctx context.Context, doc *Document, state *envoy.ResourceState) (err error) {
if n.res.ID <= 0 {
n.res.ID = nextID()
}
n.ts, err = resource.MakeTimestampsCUDA(&n.res.CreatedAt, n.res.UpdatedAt, n.res.DeletedAt, nil).
Model(n.encoderConfig.TimeLayout, n.encoderConfig.Timezone)
if err != nil {
return err
}
n.us, err = resolveUserstamps(state.ParentResources, n.us)
if err != nil {
return err
}
// @todo skip eval?
doc.addAutomationWorkflow(n)
return err
}
func (wf *automationWorkflow) MarshalYAML() (interface{}, error) {
var err error
nn, err := makeMap(
"handle", wf.res.Handle,
"meta", wf.res.Meta,
"enabled", wf.res.Enabled,
"trace", wf.res.Trace,
"keepSessions", wf.res.KeepSessions,
"scope", wf.res.Scope,
"triggers", wf.triggers,
"steps", wf.res.Steps,
"paths", wf.res.Paths,
// "issues", wf.res.Issues,
"labels", wf.res.Labels,
)
if err != nil {
return nil, err
}
nn, err = encodeTimestamps(nn, wf.ts)
if err != nil {
return nil, err
}
nn, err = encodeUserstamps(nn, wf.us)
if err != nil {
return nil, err
}
return nn, nil
}
func (t *automationTrigger) MarshalYAML() (interface{}, error) {
var err error
nn, err := makeMap(
"resourceType", t.res.ResourceType,
"eventType", t.res.EventType,
"constraints", t.res.Constraints,
"enabled", t.res.Enabled,
"stepID", t.res.StepID,
"input", t.res.Input,
"meta", t.res.Meta,
"labels", t.res.Labels,
)
if err != nil {
return nil, err
}
nn, err = encodeTimestamps(nn, t.ts)
if err != nil {
return nil, err
}
nn, err = encodeUserstamps(nn, t.us)
if err != nil {
return nil, err
}
return nn, nil
}

View File

@@ -0,0 +1,39 @@
package yaml
import (
"testing"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
func TestAutomationWorkflow_UnmarshalYAML(t *testing.T) {
var (
parseString = func(src string) (*automationWorkflow, error) {
w := &automationWorkflow{}
return w, yaml.Unmarshal([]byte(src), w)
}
)
t.Run("empty", func(t *testing.T) {
req := require.New(t)
w, err := parseString(``)
req.NoError(err)
req.NotNil(w)
req.Nil(w.res)
})
t.Run("workflow 1", func(t *testing.T) {
req := require.New(t)
doc, err := parseDocument("workflow_1")
req.NoError(err)
req.NotNil(doc)
req.Len(doc.automation.Workflows, 1)
req.NotNil(doc.automation.Workflows[0])
req.Len(doc.automation.Workflows[0].triggers, 1)
req.Len(doc.automation.Workflows[0].steps, 1)
req.Len(doc.automation.Workflows[0].paths, 1)
})
}

View File

@@ -0,0 +1,297 @@
package yaml
import (
"strings"
"github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/pkg/y7s"
"gopkg.in/yaml.v3"
)
func (wset *automationWorkflowSet) UnmarshalYAML(n *yaml.Node) error {
return y7s.Each(n, func(k, v *yaml.Node) (err error) {
var (
wrap = &automationWorkflow{}
)
if v == nil {
return y7s.NodeErr(n, "malformed automation workflow definition")
}
if err = v.Decode(&wrap); err != nil {
return
}
if err = decodeRef(k, "automation workflow handle", &wrap.res.Handle); err != nil {
return y7s.NodeErr(n, "Automation workflow reference must be a valid handle")
}
if wrap.res.Meta == nil {
wrap.res.Meta = &types.WorkflowMeta{}
}
if wrap.res.Meta.Name == "" {
// if name is not set, use handle
wrap.res.Meta.Name = wrap.res.Handle
}
*wset = append(*wset, wrap)
return
})
}
func (wrap *automationWorkflow) UnmarshalYAML(n *yaml.Node) (err error) {
if wrap.res == nil {
wrap.rbac = make(rbacRuleSet, 0, 10)
wrap.res = &types.Workflow{}
}
if wrap.rbac, err = decodeRbac(n); err != nil {
return
}
if wrap.envoyConfig, err = decodeEnvoyConfig(n); err != nil {
return
}
if wrap.ts, err = decodeTimestamps(n); err != nil {
return
}
if wrap.us, err = decodeUserstamps(n); err != nil {
return
}
return y7s.EachMap(n, func(k, v *yaml.Node) (err error) {
switch k.Value {
case "handle":
return y7s.DecodeScalar(v, "workflow handle", &wrap.res.Handle)
case "meta":
return v.Decode(&wrap.res.Meta)
case "enabled":
return y7s.DecodeScalar(v, "workflow enabled", &wrap.res.Enabled)
case "trace":
return y7s.DecodeScalar(v, "workflow trace", &wrap.res.Trace)
case "keepSessions":
return y7s.DecodeScalar(v, "workflow keepSessions", &wrap.res.KeepSessions)
case "scope":
err = v.Decode(&wrap.res.Scope)
if err != nil {
return err
}
case "triggers":
wrap.triggers = make(automationTriggerSet, 0, 100)
err = v.Decode(&wrap.triggers)
if err != nil {
return err
}
case "steps":
wrap.steps = make(automationWorkflowStepSet, 0, 100)
err = v.Decode(&wrap.steps)
if err != nil {
return err
}
case "paths":
wrap.paths = make(automationWorkflowPathSet, 0, 100)
err = v.Decode(&wrap.paths)
if err != nil {
return err
}
}
return nil
})
}
func (wrap *automationTrigger) UnmarshalYAML(n *yaml.Node) (err error) {
if wrap.res == nil {
wrap.res = &types.Trigger{}
}
if wrap.envoyConfig, err = decodeEnvoyConfig(n); err != nil {
return
}
if wrap.ts, err = decodeTimestamps(n); err != nil {
return
}
if wrap.us, err = decodeUserstamps(n); err != nil {
return
}
return y7s.EachMap(n, func(k, v *yaml.Node) (err error) {
switch k.Value {
case "enabled":
return y7s.DecodeScalar(v, "trigger enabled", &wrap.res.Enabled)
case "stepID":
return y7s.DecodeScalar(v, "trigger step", &wrap.res.StepID)
case "resourceType":
return y7s.DecodeScalar(v, "trigger resourceType", &wrap.res.ResourceType)
case "eventType":
return y7s.DecodeScalar(v, "trigger eventType", &wrap.res.EventType)
case "constraints":
return v.Decode(&wrap.res.Constraints)
case "input":
err = v.Decode(&wrap.res.Input)
if err != nil {
return err
}
case "meta":
return v.Decode(&wrap.res.Meta)
}
return nil
})
}
func (wrap *automationWorkflowStep) UnmarshalYAML(n *yaml.Node) (err error) {
if wrap.res == nil {
wrap.res = &types.WorkflowStep{}
}
if wrap.envoyConfig, err = decodeEnvoyConfig(n); err != nil {
return
}
return y7s.EachMap(n, func(k, v *yaml.Node) (err error) {
switch strings.ToLower(k.Value) {
case "stepid",
"id":
return y7s.DecodeScalar(v, "trigger step", &wrap.res.ID)
case "kind":
return y7s.DecodeScalar(v, "step kind", &wrap.res.Kind)
case "ref":
return y7s.DecodeScalar(v, "step ref", &wrap.res.Ref)
case "arguments":
wrap.res.Arguments, err = unmarshalExprSet(v)
return err
case "results":
wrap.res.Results, err = unmarshalExprSet(v)
return err
case "meta":
return v.Decode(&wrap.res.Meta)
}
return nil
})
}
func (wrap *automationWorkflowPath) UnmarshalYAML(n *yaml.Node) (err error) {
if wrap.res == nil {
wrap.res = &types.WorkflowPath{}
}
if wrap.envoyConfig, err = decodeEnvoyConfig(n); err != nil {
return
}
return y7s.EachMap(n, func(k, v *yaml.Node) (err error) {
switch strings.ToLower(k.Value) {
case "expr":
return y7s.DecodeScalar(v, "path expr", &wrap.res.Expr)
case "parentid",
"parent":
return y7s.DecodeScalar(v, "parent ref", &wrap.res.ParentID)
case "childid",
"child":
return y7s.DecodeScalar(v, "child ref", &wrap.res.ChildID)
case "meta":
return v.Decode(&wrap.res.Meta)
}
return nil
})
}
func unmarshalExprSet(n *yaml.Node) ([]*types.Expr, error) {
ee := make([]*types.Expr, 0, 10)
err := y7s.EachSeq(n, func(v *yaml.Node) (err error) {
wrap, err := unmarshalExpr(v)
ee = append(ee, wrap)
return err
})
return ee, err
}
func unmarshalExpr(n *yaml.Node) (*types.Expr, error) {
wrap := &types.Expr{}
err := y7s.EachMap(n, func(k, v *yaml.Node) (err error) {
switch k.Value {
case "target":
return y7s.DecodeScalar(v, "expression target", &wrap.Target)
case "source":
return y7s.DecodeScalar(v, "expression source", &wrap.Source)
case "expr":
return y7s.DecodeScalar(v, "expression expr", &wrap.Expr)
case "value":
return y7s.DecodeScalar(v, "expression value", &wrap.Value)
case "type":
return y7s.DecodeScalar(v, "expression type", &wrap.Type)
case "tests":
tt := make(types.TestSet, 0, 2)
err = v.Decode(&tt)
if err != nil {
return err
}
wrap.Tests = tt
return nil
}
return nil
})
return wrap, err
}
func (wset automationWorkflowSet) MarshalEnvoy() ([]resource.Interface, error) {
// namespace usually have bunch of sub-resources defined
nn := make([]resource.Interface, 0, len(wset)*10)
for _, res := range wset {
if tmp, err := res.MarshalEnvoy(); err != nil {
return nil, err
} else {
nn = append(nn, tmp...)
}
}
return nn, nil
}
func (wrap automationWorkflow) MarshalEnvoy() ([]resource.Interface, error) {
rs := resource.NewAutomationWorkflow(wrap.res)
rs.SetTimestamps(wrap.ts)
rs.SetUserstamps(wrap.us)
rs.SetConfig(wrap.envoyConfig)
for _, t := range wrap.triggers {
trs := rs.AddAutomationTrigger(t.res)
trs.SetTimestamps(t.ts)
trs.SetUserstamps(t.us)
}
for _, s := range wrap.steps {
rs.AddAutomationWorkflowStep(s.res)
}
for _, p := range wrap.paths {
rs.AddAutomationWorkflowPath(p.res)
}
return envoy.CollectNodes(
rs,
wrap.rbac.bindResource(rs),
)
}

View File

@@ -0,0 +1,202 @@
package yaml
import (
"reflect"
"github.com/cortezaproject/corteza/server/pkg/y7s"
. "github.com/cortezaproject/corteza/server/pkg/y7s"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"gopkg.in/yaml.v3"
)
type (
compose struct {
Namespaces composeNamespaceSet
Modules composeModuleSet
Records composeRecordSet
Pages composePageSet
Charts composeChartSet
EncoderConfig *EncoderConfig `yaml:"-"`
}
)
func (c *compose) MarshalYAML() (interface{}, error) {
nsDefined := c.Namespaces != nil && len(c.Namespaces) > 0
cn, _ := makeMap()
var err error
addNsRef := func(n *yaml.Node, ref string) (*yaml.Node, error) {
if !nsDefined {
n, err = addMap(n,
"namespace", ref,
)
if err != nil {
return nil, err
}
}
return n, nil
}
if nsDefined {
c.Namespaces.configureEncoder(c.EncoderConfig)
cn, err = encodeResource(cn, "namespaces", c.Namespaces, c.EncoderConfig.MappedOutput, "slug")
if err != nil {
return nil, err
}
}
if len(c.Modules) > 0 {
cn, err = addNsRef(cn, c.Modules[0].refNamespace)
if err != nil {
return nil, err
}
c.Modules.configureEncoder(c.EncoderConfig)
cn, err = encodeResource(cn, "modules", c.Modules, c.EncoderConfig.MappedOutput, "handle")
if err != nil {
return nil, err
}
}
if len(c.Records) > 0 {
cn, err = addNsRef(cn, c.Records[0].refNamespace)
if err != nil {
return nil, err
}
c.Records.configureEncoder(c.EncoderConfig)
// Records don't have this
cn, err = encodeResource(cn, "records", c.Records, false, "")
if err != nil {
return nil, err
}
}
if len(c.Pages) > 0 {
cn, err = addNsRef(cn, c.Pages[0].refNamespace)
if err != nil {
return nil, err
}
c.Pages.configureEncoder(c.EncoderConfig)
// @todo A bit of a complication with pages and handles...
// Will probably just leave it as so, but might change it later.
cn, err = encodeResource(cn, "pages", c.Pages, false, "handle")
if err != nil {
return nil, err
}
}
if len(c.Charts) > 0 {
cn, err = addNsRef(cn, c.Charts[0].refNamespace)
if err != nil {
return nil, err
}
c.Charts.configureEncoder(c.EncoderConfig)
cn, err = encodeResource(cn, "charts", c.Charts, c.EncoderConfig.MappedOutput, "handle")
if err != nil {
return nil, err
}
}
return cn, nil
}
func (c *compose) UnmarshalYAML(n *yaml.Node) error {
var (
nsRef string
err error
)
// 1st pass: handle doc-level references
err = y7s.EachMap(n, func(k, v *yaml.Node) error {
switch k.Value {
case "namespace":
if def := FindKeyNode(n, "namespaces"); def != nil {
return y7s.NodeErr(def, "cannot combine namespace reference and namespaces definition")
}
if err := decodeRef(v, "namespace", &nsRef); err != nil {
return err
}
}
return nil
})
if err != nil {
return err
}
// 2nd pass: handle definitions
return y7s.EachMap(n, func(k, v *yaml.Node) error {
switch k.Value {
case "namespaces":
err = v.Decode(&c.Namespaces)
if err != nil {
return err
}
// If we're defining a fresh namespace and we're not nesting
if nsRef == "" {
nsRef = c.Namespaces[0].res.Slug
}
case "modules":
if err = v.Decode(&c.Modules); err != nil {
return err
}
return c.Modules.setNamespaceRef(nsRef)
case "pages":
if err = v.Decode(&c.Pages); err != nil {
return err
}
return c.Pages.setNamespaceRef(nsRef)
case "charts":
if err = v.Decode(&c.Charts); err != nil {
return err
}
return c.Charts.setNamespaceRef(nsRef)
case "records":
if err = v.Decode(&c.Records); err != nil {
return err
}
return c.Records.setNamespaceRef(nsRef)
}
return nil
})
}
func (c compose) MarshalEnvoy() ([]resource.Interface, error) {
nn := make([]resource.Interface, 0, 100)
rf := reflect.ValueOf(c)
for i := 0; i < rf.NumField(); i++ {
if mr, ok := rf.Field(i).Interface().(EnvoyMarshler); ok {
tmp, err := mr.MarshalEnvoy()
if err != nil {
return nil, err
}
nn = append(nn, tmp...)
}
}
return nn, nil
}

View File

@@ -0,0 +1,52 @@
package yaml
import (
"strconv"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
)
type (
composeChart struct {
res *types.Chart
chartConfig *composeChartConfig
ts *resource.Timestamps
envoyConfig *resource.EnvoyConfig
encoderConfig *EncoderConfig
relNs *types.Namespace
refNamespace string
// pointer to report and module reference
refReportModules map[int]string
rbac rbacRuleSet
}
composeChartSet []*composeChart
composeChartConfig struct {
config types.ChartConfig
reports []*composeChartConfigReport
refReportModules map[int]string
}
composeChartConfigReport struct {
report *types.ChartConfigReport
refModule string
relModule *types.Module
}
)
func (nn composeChartSet) configureEncoder(cfg *EncoderConfig) {
for _, n := range nn {
n.encoderConfig = cfg
}
}
func relChartToRef(chr *types.Chart) string {
return firstOkString(chr.Handle, strconv.FormatUint(chr.ID, 10))
}

View File

@@ -0,0 +1,131 @@
package yaml
import (
"context"
"strconv"
composeTypes "github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
)
func composeChartFromResource(r *resource.ComposeChart, cfg *EncoderConfig) *composeChart {
return &composeChart{
res: r.Res,
encoderConfig: cfg,
}
}
func (n *composeChart) Prepare(ctx context.Context, state *envoy.ResourceState) (err error) {
chr, ok := state.Res.(*resource.ComposeChart)
if !ok {
return encoderErrInvalidResource(composeTypes.ChartResourceType, state.Res.ResourceType())
}
// Get the related namespace
n.relNs = resource.FindComposeNamespace(state.ParentResources, chr.RefNs.Identifiers)
if n.relNs == nil {
return resource.ComposeNamespaceErrUnresolved(chr.RefNs.Identifiers)
}
n.res = chr.Res
n.refNamespace = relNsToRef(n.relNs)
n.chartConfig = &composeChartConfig{
config: n.res.Config,
reports: make([]*composeChartConfigReport, 0, 1),
}
for i, r := range chr.Res.Config.Reports {
refMod := chr.RefMods[i]
relMod := resource.FindComposeModule(state.ParentResources, refMod.Identifiers)
if relMod == nil {
return resource.ComposeModuleErrUnresolved(refMod.Identifiers)
}
ccr := &composeChartConfigReport{
report: r,
relModule: relMod,
}
n.chartConfig.reports = append(n.chartConfig.reports, ccr)
}
return nil
}
func (n *composeChart) Encode(ctx context.Context, doc *Document, state *envoy.ResourceState) (err error) {
res := n.res
if res.ID <= 0 {
res.ID = nextID()
}
if state.Conflicting {
return nil
}
// Handle timestamps
n.ts, err = resource.MakeTimestampsCUDA(&n.res.CreatedAt, n.res.UpdatedAt, n.res.DeletedAt, nil).
Model(n.encoderConfig.TimeLayout, n.encoderConfig.Timezone)
if err != nil {
return err
}
// @todo skip eval?
// if n.encoderConfig.CompactOutput {
// err = doc.nestComposeChart(n.refNamespace, n)
// } else {
// doc.addComposeChart(n)
// }
doc.addComposeChart(n)
return err
}
func (c *composeChart) MarshalYAML() (interface{}, error) {
nn, err := makeMap(
"chartID", c.res.ID,
"handle", c.res.Handle,
"name", c.res.Name,
"config", c.chartConfig,
"labels", c.res.Labels,
)
if err != nil {
return nil, err
}
nn, err = encodeTimestamps(nn, c.ts)
if err != nil {
return nil, err
}
return nn, nil
}
func (c *composeChartConfig) MarshalYAML() (interface{}, error) {
nn, err := makeMap(
"reports", c.reports,
"colorScheme", c.config.ColorScheme,
)
if err != nil {
return nil, err
}
return nn, nil
}
func (c *composeChartConfigReport) MarshalYAML() (interface{}, error) {
nn, err := makeMap(
"filter", c.report.Filter,
"module", firstOkString(c.relModule.Handle, strconv.FormatUint(c.relModule.ID, 10)),
"metrics", c.report.Metrics,
"dimensions", c.report.Dimensions,
"yAxis", c.report.YAxis,
)
if err != nil {
return nil, err
}
return nn, nil
}

View File

@@ -0,0 +1,68 @@
package yaml
import (
"testing"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
func TestComposeChart_UnmarshalYAML(t *testing.T) {
var (
parseString = func(src string) (*composeChart, error) {
w := &composeChart{}
return w, yaml.Unmarshal([]byte(src), w)
}
)
t.Run("empty", func(t *testing.T) {
req := require.New(t)
w, err := parseString(``)
req.NoError(err)
req.NotNil(w)
req.Nil(w.res)
})
t.Run("simple name", func(t *testing.T) {
req := require.New(t)
w, err := parseString(`{ name: Test }`)
req.NoError(err)
req.NotNil(w)
req.NotNil(w.res)
req.NotEmpty(w.res.Name)
})
t.Run("doc 1", func(t *testing.T) {
req := require.New(t)
doc, err := parseDocument("compose_chart_1")
req.NoError(err)
req.NotNil(doc)
req.NotNil(doc.compose)
req.Len(doc.compose.Charts, 2)
req.Equal(1, len(doc.compose.Charts[0].res.Config.Reports))
req.NotNil(doc.compose.Charts[1].rbac)
req.NotEmpty(doc.compose.Charts[1].rbac)
})
t.Run("doc rbac", func(t *testing.T) {
req := require.New(t)
doc, err := parseDocument("compose_chart_rbac")
req.NoError(err)
req.NotNil(doc)
req.NotNil(doc.compose)
ch := doc.compose.Charts[0]
req.Len(ch.rbac, 2)
a := ch.rbac[0]
b := ch.rbac[1]
req.Equal(a.res.Operation, "read")
req.Equal(a.res.Access, rbac.Allow)
req.Equal(b.res.Operation, "delete")
req.Equal(b.res.Access, rbac.Deny)
})
}

View File

@@ -0,0 +1,181 @@
package yaml
import (
"fmt"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/pkg/y7s"
"gopkg.in/yaml.v3"
)
func (wset *composeChartSet) UnmarshalYAML(n *yaml.Node) error {
return y7s.Each(n, func(k, v *yaml.Node) (err error) {
var (
wrap = &composeChart{}
)
if v == nil {
return y7s.NodeErr(n, "malformed chart definition")
}
if err = v.Decode(&wrap); err != nil {
return
}
if err = decodeRef(k, "chart", &wrap.res.Handle); err != nil {
return y7s.NodeErr(n, "Chart reference must be a valid handle")
}
if wrap.res.Name == "" {
// if name is not set, use handle
wrap.res.Name = wrap.res.Handle
}
*wset = append(*wset, wrap)
return
})
}
func (wrap *composeChart) UnmarshalYAML(n *yaml.Node) (err error) {
if wrap.res == nil {
wrap.rbac = make(rbacRuleSet, 0, 10)
wrap.res = &types.Chart{}
}
if wrap.rbac, err = decodeRbac(n); err != nil {
return
}
if wrap.envoyConfig, err = decodeEnvoyConfig(n); err != nil {
return
}
if wrap.ts, err = decodeTimestamps(n); err != nil {
return
}
return y7s.EachMap(n, func(k, v *yaml.Node) (err error) {
switch k.Value {
case "chartID":
return y7s.DecodeScalar(v, "chart ID", &wrap.res.ID)
case "handle":
return y7s.DecodeScalar(v, "chart handle", &wrap.res.Handle)
case "name":
return y7s.DecodeScalar(v, "chart name", &wrap.res.Name)
case "config":
cfg := composeChartConfig{
refReportModules: make(map[int]string),
}
if err = v.Decode(&cfg); err != nil {
return err
}
// copy decoded values from aux type
wrap.res.Config = cfg.config
wrap.refReportModules = cfg.refReportModules
}
return nil
})
}
func (wset composeChartSet) setNamespaceRef(ref string) error {
for _, res := range wset {
if res.refNamespace != "" && ref != res.refNamespace {
return fmt.Errorf("cannot override namespace reference %s with %s", res.refNamespace, ref)
}
res.refNamespace = ref
}
return nil
}
func (wset composeChartSet) MarshalEnvoy() ([]resource.Interface, error) {
// namespace usually have bunch of sub-resources defined
nn := make([]resource.Interface, 0, len(wset))
for _, res := range wset {
if tmp, err := res.MarshalEnvoy(); err != nil {
return nil, err
} else {
nn = append(nn, tmp...)
}
}
return nn, nil
}
func (wrap composeChart) MarshalEnvoy() ([]resource.Interface, error) {
vv := make(resource.RefSet, 0, len(wrap.refReportModules))
for _, v := range wrap.refReportModules {
vv = append(vv, resource.MakeModuleRef(0, v, ""))
}
rs := resource.NewComposeChart(wrap.res, resource.MakeNamespaceRef(0, wrap.refNamespace, ""), vv)
rs.SetTimestamps(wrap.ts)
rs.SetConfig(wrap.envoyConfig)
return envoy.CollectNodes(
rs,
wrap.rbac.bindResource(rs),
)
}
func (wrap *composeChartConfig) UnmarshalYAML(n *yaml.Node) error {
return y7s.EachMap(n, func(k, v *yaml.Node) (err error) {
switch k.Value {
case "reports":
reports := make([]composeChartConfigReport, 0)
if err = v.Decode(&reports); err != nil {
return y7s.NodeErr(v, "could not decode reports: %w", err)
}
// collect reports and referenced modules from wrapped type
wrap.config.Reports = make([]*types.ChartConfigReport, len(reports))
for r := range reports {
wrap.config.Reports[r] = reports[r].report
if reports[r].refModule != "" {
wrap.refReportModules[r] = reports[r].refModule
}
}
case "colorScheme":
if err = v.Decode(&wrap.config.ColorScheme); err != nil {
return y7s.NodeErr(v, "could not decode color scheme: %w", err)
}
}
return nil
})
}
func (wrap *composeChartConfigReport) UnmarshalYAML(n *yaml.Node) error {
if err := n.Decode(&wrap.report); err != nil {
return err
}
return y7s.EachMap(n, func(k, v *yaml.Node) (err error) {
switch k.Value {
case "y",
"yAxis",
"YAxis":
wrap.report.YAxis = make(map[string]interface{})
return v.Decode(&wrap.report.YAxis)
case "module":
// custom decoder for referenced module
// we'll copy this to the dedicated prop of the wrapping structure
// so that the parent decoder can collect it
return y7s.DecodeScalar(v, "chart report module", &wrap.refModule)
}
return nil
})
}

View File

@@ -0,0 +1,68 @@
package yaml
import (
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
systemTypes "github.com/cortezaproject/corteza/server/system/types"
)
type (
mappingTpl struct {
resource.MappingTpl `yaml:",inline"`
}
mappingTplSet []*mappingTpl
composeRecordTpl struct {
Source string `yaml:"from"`
Key []string
Mapping mappingTplSet
Defaultable bool
}
composeModule struct {
res *types.Module
fields composeModuleFieldSet
ts *resource.Timestamps
envoyConfig *resource.EnvoyConfig
encoderConfig *EncoderConfig
relNs *types.Namespace
refNamespace string
rbac rbacRuleSet
locale resourceTranslationSet
recTpl *composeRecordTpl
}
composeModuleSet []*composeModule
composeModuleField struct {
res *types.ModuleField
ts *resource.Timestamps
cfg *EncoderConfig
expr composeModuleFieldExpr
relMod *types.Module
relRoles systemTypes.RoleSet
rbac rbacRuleSet
locale resourceTranslationSet
}
composeModuleFieldSet []*composeModuleField
// aux struct for decoding module field expressions
composeModuleFieldExpr types.ModuleFieldExpr
)
func (nn composeModuleSet) configureEncoder(cfg *EncoderConfig) {
for _, n := range nn {
n.encoderConfig = cfg
}
}
func (ff composeModuleFieldSet) configureEncoder(cfg *EncoderConfig) {
for _, f := range ff {
f.cfg = cfg
}
}

View File

@@ -0,0 +1,227 @@
package yaml
import (
"context"
"encoding/json"
"strconv"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
)
func composeModuleFromResource(r *resource.ComposeModule, cfg *EncoderConfig) *composeModule {
return &composeModule{
res: r.Res,
encoderConfig: cfg,
}
}
func (n *composeModule) Prepare(ctx context.Context, state *envoy.ResourceState) (err error) {
mod, ok := state.Res.(*resource.ComposeModule)
if !ok {
return encoderErrInvalidResource(types.ModuleResourceType, state.Res.ResourceType())
}
// Get the related namespace
n.relNs = resource.FindComposeNamespace(state.ParentResources, mod.RefNs.Identifiers)
if n.relNs == nil {
return resource.ComposeNamespaceErrUnresolved(mod.RefNs.Identifiers)
}
n.res = mod.Res
n.refNamespace = relNsToRef(n.relNs)
n.fields = make(composeModuleFieldSet, 0, len(mod.Res.Fields))
for _, f := range mod.Res.Fields {
cmf := &composeModuleField{
res: f,
expr: composeModuleFieldExpr(f.Expressions),
}
switch f.Kind {
case "Record":
refMod := f.Options.String("module")
if refMod == "" {
refMod = f.Options.String("moduleID")
}
cmf.relMod = resource.FindComposeModule(state.ParentResources, resource.MakeIdentifiers(refMod))
if cmf.relMod == nil {
return resource.ComposeModuleErrUnresolved(resource.MakeIdentifiers(refMod))
}
case "User":
refRoles := resource.ComposeModuleFieldExtractUserFieldRoles(f.Options["roles"])
if len(refRoles) == 0 {
refRoles = resource.ComposeModuleFieldExtractUserFieldRoles(f.Options["role"])
}
if len(refRoles) == 0 {
refRoles = resource.ComposeModuleFieldExtractUserFieldRoles(f.Options["roleID"])
}
for _, ref := range refRoles {
aux := resource.FindRole(state.ParentResources, resource.MakeIdentifiers(ref))
if aux == nil {
return resource.RoleErrUnresolved(resource.MakeIdentifiers(ref))
}
cmf.relRoles = append(cmf.relRoles, aux)
}
}
n.fields = append(n.fields, cmf)
}
if err != nil {
return err
}
return nil
}
func (n *composeModule) Encode(ctx context.Context, doc *Document, state *envoy.ResourceState) (err error) {
if n.res.ID <= 0 {
n.res.ID = nextID()
}
if state.Conflicting {
return nil
}
n.ts, err = resource.MakeTimestampsCUDA(&n.res.CreatedAt, n.res.UpdatedAt, n.res.DeletedAt, nil).
Model(n.encoderConfig.TimeLayout, n.encoderConfig.Timezone)
if err != nil {
return err
}
// Fields
for _, f := range n.fields {
// Timestaps
n.ts, err = resource.MakeTimestampsCUDA(&f.res.CreatedAt, f.res.UpdatedAt, f.res.DeletedAt, nil).
Model(n.encoderConfig.TimeLayout, n.encoderConfig.Timezone)
if err != nil {
return err
}
}
// @todo skip eval?
// if n.encoderConfig.CompactOutput {
// err = doc.nestComposeModule(n.refNamespace, n)
// } else {
// doc.addComposeModule(n)
// }
doc.addComposeModule(n)
return err
}
func (c *composeModule) MarshalYAML() (interface{}, error) {
// Get a struct from the raw JSON string
auxMeta := make(map[string]interface{})
if err := json.Unmarshal(c.res.Meta, &auxMeta); err != nil {
return nil, err
}
nn, err := makeMap(
"moduleID", c.res.ID,
"handle", c.res.Handle,
"name", c.res.Name,
"meta", auxMeta,
"labels", c.res.Labels,
)
if c.fields != nil && len(c.fields) > 0 {
c.fields.configureEncoder(c.encoderConfig)
// Currently only mapped field representation is supported
//
// @todo Expand
nn, err = encodeResource(nn, "fields", c.fields, true, "name")
if err != nil {
return nil, err
}
}
if err != nil {
return nil, err
}
nn, err = encodeTimestamps(nn, c.ts)
if err != nil {
return nil, err
}
return nn, err
}
func (c *composeModuleField) MarshalYAML() (interface{}, error) {
var err error
auxOpt := c.res.Options
if auxOpt == nil {
auxOpt = make(types.ModuleFieldOptions)
}
switch c.res.Kind {
case "Record":
ref := c.relMod.Handle
if ref == "" {
ref = c.relMod.Name
}
auxOpt["module"] = ref
delete(auxOpt, "moduleID")
case "User":
aux := make([]string, 0, len(c.relRoles))
for _, r := range c.relRoles {
aux = append(aux, firstOkString(r.Handle, strconv.FormatUint(r.ID, 10)))
}
auxOpt["roles"] = aux
delete(auxOpt, "role")
delete(auxOpt, "roleID")
}
nopt, _ := makeMap()
for k, v := range auxOpt {
nopt, err = addMap(nopt, k, v)
if err != nil {
return nil, err
}
}
nsn, err := makeMap(
"kind", c.res.Kind,
"name", c.res.Name,
"label", c.res.Label,
"options", nopt,
"config", c.res.Config,
"required", c.res.Required,
"multi", c.res.Multi,
"defaultValue", c.res.DefaultValue,
"expressions", c.expr,
"labels", c.res.Labels,
)
if err != nil {
return nil, err
}
nsn, err = encodeTimestamps(nsn, c.ts)
if err != nil {
return nil, err
}
return nsn, err
}
func (fe composeModuleFieldExpr) MarshalYAML() (interface{}, error) {
return makeMap(
"valueExpr", fe.ValueExpr,
"sanitizers", fe.Sanitizers,
"validators", fe.Validators,
"disableDefaultValidators", fe.DisableDefaultValidators,
"formatters", fe.Formatters,
"disableDefaultFormatters", fe.DisableDefaultFormatters,
)
}

View File

@@ -0,0 +1,173 @@
package yaml
import (
"testing"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
func TestComposeModule_UnmarshalYAML(t *testing.T) {
var (
parseString = func(src string) (*composeModule, error) {
w := &composeModule{}
return w, yaml.Unmarshal([]byte(src), w)
}
)
t.Run("empty", func(t *testing.T) {
req := require.New(t)
w, err := parseString(``)
req.NoError(err)
req.NotNil(w)
req.Nil(w.res)
})
t.Run("simple name", func(t *testing.T) {
req := require.New(t)
w, err := parseString(`{ name: Test }`)
req.NoError(err)
req.NotNil(w)
req.NotNil(w.res)
req.NotEmpty(w.res.Name)
})
t.Run("field with default value", func(t *testing.T) {
req := require.New(t)
w, err := parseString(`{ fields: { one: { default: foo }, two: { default: [ foo, bar ] } } }`)
req.NoError(err)
req.NotNil(w)
req.NotNil(w.res)
req.Len(w.res.Fields, 2)
req.Len(w.res.Fields[0].DefaultValue, 1)
req.Equal("foo", w.res.Fields[0].DefaultValue[0].Value)
req.Len(w.res.Fields[1].DefaultValue, 2)
req.Equal("foo", w.res.Fields[1].DefaultValue[0].Value)
req.Equal("bar", w.res.Fields[1].DefaultValue[1].Value)
})
t.Run("doc 1", func(t *testing.T) {
req := require.New(t)
doc, err := parseDocument("compose_module_1")
req.NoError(err)
req.NotNil(doc)
req.NotNil(doc.compose)
req.Len(doc.compose.Modules, 3)
req.Equal(30, len(doc.compose.Modules[0].res.Fields))
req.NotNil(doc.compose.Modules[0].res.Fields.FindByName("AccountSource"))
req.Equal(21, len(doc.compose.Modules[1].res.Fields))
req.NotNil(doc.compose.Modules[1].res.Fields.FindByName("NumberOfConvertedLeads"))
req.Equal(23, len(doc.compose.Modules[2].res.Fields))
req.NotNil(doc.compose.Modules[0].rbac)
req.NotEmpty(doc.compose.Modules[0].rbac)
})
t.Run("doc 2", func(t *testing.T) {
req := require.New(t)
doc, err := parseDocument("compose_module_2")
req.NoError(err)
req.NotNil(doc)
req.NotNil(doc.compose)
req.Len(doc.compose.Modules, 1)
req.Len(doc.compose.Modules[0].res.Fields, 2)
full := doc.compose.Modules[0].res.Fields.FindByName("full")
vaKV := doc.compose.Modules[0].res.Fields.FindByName("validatorKV")
req.Equal("a > b", full.Expressions.ValueExpr)
req.Len(full.Expressions.Sanitizers, 1)
req.Contains(full.Expressions.Sanitizers, "trim(value)")
req.Len(full.Expressions.Validators, 1)
req.Equal(`a == ""`, full.Expressions.Validators[0].Test)
req.Equal("Value should not be empty", full.Expressions.Validators[0].Error)
req.True(full.Expressions.DisableDefaultValidators)
req.Len(vaKV.Expressions.Validators, 1)
req.Equal(`value == ""`, vaKV.Expressions.Validators[0].Test)
req.Equal("Value should be filled", vaKV.Expressions.Validators[0].Error)
})
t.Run("doc rbac", func(t *testing.T) {
req := require.New(t)
doc, err := parseDocument("compose_module_rbac")
req.NoError(err)
req.NotNil(doc)
req.NotNil(doc.compose)
mod := doc.compose.Modules[0]
req.Len(mod.rbac, 2)
a := mod.rbac[0]
b := mod.rbac[1]
req.Equal(a.res.Operation, "read")
req.Equal(a.res.Access, rbac.Allow)
req.Equal(b.res.Operation, "delete")
req.Equal(b.res.Access, rbac.Deny)
})
}
func TestComposeModule_Shaping(t *testing.T) {
t.Run("1 key; list mapping", func(t *testing.T) {
req := require.New(t)
doc, err := parseDocument("compose_module_s_1")
req.NoError(err)
req.NotNil(doc)
req.NotNil(doc.compose)
req.Len(doc.compose.Modules, 1)
m1 := doc.compose.Modules[0]
tp := m1.recTpl
req.NotNil(tp)
req.Equal("mod1.csv", tp.Source)
req.Equal([]string{"id"}, tp.Key)
req.Len(tp.Mapping, 2)
req.Equal(uint(0), tp.Mapping[0].Index)
req.Equal("f1", tp.Mapping[0].Field)
req.Equal(uint(3), tp.Mapping[1].Index)
req.Equal("f2", tp.Mapping[1].Field)
})
t.Run("2 keys", func(t *testing.T) {
req := require.New(t)
doc, err := parseDocument("compose_module_s_2")
req.NoError(err)
req.NotNil(doc)
req.NotNil(doc.compose)
req.Len(doc.compose.Modules, 1)
m1 := doc.compose.Modules[0]
tp := m1.recTpl
req.NotNil(tp)
req.Equal([]string{"f1", "f2"}, tp.Key)
})
t.Run("complex mapping", func(t *testing.T) {
req := require.New(t)
doc, err := parseDocument("compose_module_s_3")
req.NoError(err)
req.NotNil(doc)
req.NotNil(doc.compose)
req.Len(doc.compose.Modules, 1)
m1 := doc.compose.Modules[0]
tp := m1.recTpl
req.Len(tp.Mapping, 2)
req.Equal(uint(0), tp.Mapping[0].Index)
req.Equal("c1", tp.Mapping[0].Cell)
req.Equal("f1", tp.Mapping[0].Field)
req.Equal(uint(0), tp.Mapping[1].Index)
req.Equal("c2", tp.Mapping[1].Cell)
req.Equal("f2", tp.Mapping[1].Field)
})
}

View File

@@ -0,0 +1,408 @@
package yaml
import (
"fmt"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/pkg/handle"
"github.com/cortezaproject/corteza/server/pkg/y7s"
"gopkg.in/yaml.v3"
)
func (wset *composeModuleSet) UnmarshalYAML(n *yaml.Node) error {
return y7s.Each(n, func(k, v *yaml.Node) (err error) {
var (
wrap = &composeModule{}
)
if v == nil {
return y7s.NodeErr(n, "malformed module definition")
}
if err = v.Decode(&wrap); err != nil {
return
}
if err = decodeRef(k, "module", &wrap.res.Handle); err != nil {
return y7s.NodeErr(n, "Chart reference must be a valid handle")
}
if wrap.res.Name == "" {
// if name is not set, use handle
wrap.res.Name = wrap.res.Handle
}
*wset = append(*wset, wrap)
return
})
}
func (wrap *composeModule) UnmarshalYAML(n *yaml.Node) (err error) {
if wrap.res == nil {
wrap.rbac = make(rbacRuleSet, 0, 10)
wrap.res = &types.Module{}
}
if wrap.rbac, err = decodeRbac(n); err != nil {
return
}
if wrap.locale, err = decodeLocale(n); err != nil {
return
}
if wrap.envoyConfig, err = decodeEnvoyConfig(n); err != nil {
return
}
if wrap.ts, err = decodeTimestamps(n); err != nil {
return
}
return y7s.EachMap(n, func(k, v *yaml.Node) (err error) {
switch k.Value {
case "moduleID":
return y7s.DecodeScalar(v, "module ID", &wrap.res.ID)
case "name":
return y7s.DecodeScalar(v, "module name", &wrap.res.Name)
case "handle":
return y7s.DecodeScalar(v, "module handle", &wrap.res.Handle)
case "fields":
if !y7s.IsKind(v, yaml.MappingNode) {
return y7s.NodeErr(n, "field definition must be a map")
}
if err = v.Decode(&wrap.fields); err != nil {
return err
}
wrap.res.Fields = wrap.fields.set()
return nil
case "records":
if y7s.IsKind(v, yaml.MappingNode) {
return v.Decode(&wrap.recTpl)
} else {
return y7s.NodeErr(n, "records definition must be a map")
}
}
return nil
})
}
func (wset composeModuleSet) setNamespaceRef(ref string) error {
for _, res := range wset {
if res.refNamespace != "" && ref != res.refNamespace {
return fmt.Errorf("cannot override namespace reference %s with %s", res.refNamespace, ref)
}
res.refNamespace = ref
}
return nil
}
func (wset composeModuleSet) MarshalEnvoy() ([]resource.Interface, error) {
// namespace usually have bunch of sub-resources defined
nn := make([]resource.Interface, 0, len(wset)*10)
for _, res := range wset {
if tmp, err := res.MarshalEnvoy(); err != nil {
return nil, err
} else {
nn = append(nn, tmp...)
}
}
return nn, nil
}
func (wrap composeModule) MarshalEnvoy() ([]resource.Interface, error) {
nsRef := resource.MakeNamespaceRef(0, wrap.refNamespace, "")
rs := resource.NewComposeModule(wrap.res, nsRef)
rs.SetTimestamps(wrap.ts)
rs.SetConfig(wrap.envoyConfig)
var crs *resource.ComposeRecordTemplate
if wrap.recTpl != nil {
s := wrap.recTpl
mtt := make(resource.MappingTplSet, 0, len(s.Mapping))
for _, m := range s.Mapping {
mtt = append(mtt, &m.MappingTpl)
}
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, nsRef, rs.Ref())
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,
)
}
func (rt *composeRecordTpl) UnmarshalYAML(n *yaml.Node) error {
return y7s.EachMap(n, func(k, v *yaml.Node) (err error) {
switch k.Value {
case "source", "origin", "from":
rt.Source = v.Value
case "key", "index", "pk":
if !y7s.IsKind(v, yaml.SequenceNode) {
rt.Key = []string{v.Value}
} else {
rt.Key = make([]string, 0, 3)
y7s.EachSeq(v, func(v *yaml.Node) error {
rt.Key = append(rt.Key, v.Value)
return nil
})
}
case "defaultable":
return y7s.DecodeScalar(v, "defaultable field mapping", &rt.Defaultable)
case "mapping", "map":
rt.Mapping = make(mappingTplSet, 0, 20)
// When provided as a sequence node, map the fields based on the index.
// first cell is mapped to the first sequence value, second cell to the second, and so on.
// Omit cells with empty, /, or - value.
if y7s.IsKind(v, yaml.SequenceNode) {
i := uint(0)
y7s.EachSeq(v, func(v *yaml.Node) error {
defer func() { i++ }()
if v.Value == "" || v.Value == "/" || v.Value == "-" {
return nil
}
rt.Mapping = append(rt.Mapping, &mappingTpl{
MappingTpl: resource.MappingTpl{
Index: i,
Field: v.Value,
},
})
return nil
})
} else if y7s.IsKind(v, yaml.MappingNode) {
// When provided as a mapping node, it can be a simple cell: field map
// or a more complex underlying structure.
y7s.EachMap(v, func(k, v *yaml.Node) error {
m := &mappingTpl{}
if y7s.IsKind(v, yaml.MappingNode) {
v.Decode(m)
} else {
m.Field = v.Value
}
m.Cell = k.Value
rt.Mapping = append(rt.Mapping, m)
return nil
})
}
}
return nil
})
}
func (set *composeModuleFieldSet) UnmarshalYAML(n *yaml.Node) error {
return y7s.Each(n, func(k, v *yaml.Node) (err error) {
var (
wrap = &composeModuleField{}
)
if v == nil {
return y7s.NodeErr(n, "malformed module field definition")
}
if err = v.Decode(&wrap); err != nil {
return err
}
if k != nil {
if !handle.IsValid(k.Value) {
return y7s.NodeErr(n, "field name must be a valid handle")
}
wrap.res.Name = k.Value
if wrap.res.Label == "" {
wrap.res.Label = k.Value
}
}
*set = append(*set, wrap)
return
})
}
func (set composeModuleFieldSet) set() (out types.ModuleFieldSet) {
for _, i := range set {
out = append(out, i.res)
}
return out
}
func (wrap *composeModuleField) UnmarshalYAML(n *yaml.Node) (err error) {
if wrap.res == nil {
wrap.rbac = make(rbacRuleSet, 0, 10)
wrap.res = &types.ModuleField{}
}
if wrap.rbac, err = decodeRbac(n); err != nil {
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":
return fmt.Errorf("name should be encoded as field definition key")
case "place":
return y7s.DecodeScalar(v, "module place", &wrap.res.Place)
case "kind":
return y7s.DecodeScalar(v, "module kind", &wrap.res.Kind)
case "label":
return y7s.DecodeScalar(v, "module label", &wrap.res.Label)
case "required":
return y7s.DecodeScalar(v, "module required", &wrap.res.Required)
case "multi":
return y7s.DecodeScalar(v, "module multi", &wrap.res.Multi)
case "options":
if err = v.Decode(&wrap.res.Options); err != nil {
return err
}
case "config":
if err = v.Decode(&wrap.res.Config); err != nil {
return err
}
case "expressions":
ea := composeModuleFieldExpr{}
if err = v.Decode(&ea); err != nil {
return err
}
wrap.res.Expressions = types.ModuleFieldExpr(ea)
case "default":
var rvs = types.RecordValueSet{}
switch v.Kind {
case yaml.ScalarNode:
rvs = rvs.Set(&types.RecordValue{Value: v.Value})
case yaml.SequenceNode:
_ = y7s.EachSeq(v, func(v *yaml.Node) error {
rvs = rvs.Set(&types.RecordValue{Value: v.Value, Place: uint(len(rvs))})
return nil
})
}
wrap.res.DefaultValue = rvs
}
return nil
})
}
func (aux *composeModuleFieldExpr) UnmarshalYAML(n *yaml.Node) (err error) {
return y7s.EachMap(n, func(k *yaml.Node, v *yaml.Node) error {
switch k.Value {
case "valueExpr", "value":
aux.ValueExpr = v.Value
return nil
case "sanitizer":
aux.Sanitizers = []string{v.Value}
return nil
case "sanitizers":
return y7s.EachSeq(v, func(san *yaml.Node) error {
aux.Sanitizers = append(aux.Sanitizers, san.Value)
return nil
})
case "validator", "validators":
return y7s.Each(v, func(k *yaml.Node, v *yaml.Node) error {
vld := &types.ModuleFieldValidator{}
if y7s.IsKind(v, yaml.MappingNode) {
if err := v.Decode(vld); err != nil {
return err
}
} else {
vld.Test = k.Value
vld.Error = v.Value
}
aux.Validators = append(aux.Validators, *vld)
return nil
})
case "disableDefaultValidators":
return v.Decode(&aux.DisableDefaultValidators)
case "formatter":
aux.Formatters = []string{v.Value}
return nil
case "formatters":
return y7s.EachSeq(v, func(fmt *yaml.Node) error {
aux.Formatters = append(aux.Formatters, fmt.Value)
return nil
})
case "disableDefaultFormatters":
return v.Decode(&aux.DisableDefaultFormatters)
}
return nil
})
}

View File

@@ -0,0 +1,158 @@
package yaml
import (
"fmt"
"strconv"
atypes "github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
)
type (
composeNamespace struct {
res *types.Namespace `yaml:",inline"`
ts *resource.Timestamps
meta composeNamespaceMeta
envoyConfig *resource.EnvoyConfig
encoderConfig *EncoderConfig
// all known modules on a namespace
modules composeModuleSet
// all known charts on a namespace
charts composeChartSet
// all known records on a namespace
records composeRecordSet
// all known pages on a namespace
pages composePageSet
// module's RBAC rules
rbac rbacRuleSet
locale resourceTranslationSet
}
composeNamespaceSet []*composeNamespace
composeNamespaceMeta types.NamespaceMeta
)
func composeNamespaceFromResource(r *resource.ComposeNamespace, cfg *EncoderConfig) *composeNamespace {
return &composeNamespace{
res: r.Res,
encoderConfig: cfg,
}
}
// configureEncoder configures the composeNamespace encoding
func (nn composeNamespaceSet) configureEncoder(cfg *EncoderConfig) {
for _, n := range nn {
n.encoderConfig = cfg
}
}
func (nn composeNamespaceSet) findComposeNamespace(ns string) *composeNamespace {
for _, n := range nn {
if resource.Check(ns, n.res.ID, n.res.Slug, n.res.Name) {
return n
}
}
return nil
}
func (nn composeNamespaceSet) addComposeModule(ref string, m *composeModule) error {
ns := nn.findComposeNamespace(ref)
if ns == nil {
return composeNamespaceErrNotFound(ref)
}
if ns.modules == nil {
ns.modules = make(composeModuleSet, 0, 1)
}
ns.modules = append(ns.modules, m)
return nil
}
func (nn composeNamespaceSet) addComposePage(ref string, p *composePage) error {
ns := nn.findComposeNamespace(ref)
if ns == nil {
return composeNamespaceErrNotFound(ref)
}
if ns.pages == nil {
ns.pages = make(composePageSet, 0, 1)
}
ns.pages = append(ns.pages, p)
return nil
}
func (nn composeNamespaceSet) addComposeChart(ref string, c *composeChart) error {
ns := nn.findComposeNamespace(ref)
if ns == nil {
return composeNamespaceErrNotFound(ref)
}
if ns.charts == nil {
ns.charts = make(composeChartSet, 0, 1)
}
ns.charts = append(ns.charts, c)
return nil
}
func (nn composeNamespaceSet) addComposeRecord(ref string, r *composeRecord) error {
ns := nn.findComposeNamespace(ref)
if ns == nil {
return composeNamespaceErrNotFound(ref)
}
if ns.records == nil {
ns.records = make(composeRecordSet, 0, 1)
}
ns.records = append(ns.records, r)
return nil
}
func (nn composeNamespaceSet) addRbacRule(ref string, r *rbacRule) error {
ns := nn.findComposeNamespace(ref)
if ns == nil {
return composeNamespaceErrNotFound(ref)
}
if ns.rbac == nil {
ns.rbac = make(rbacRuleSet, 0, 20)
}
ns.rbac = append(ns.rbac, r)
return nil
}
func (c composeNamespaceMeta) empty() bool {
if c == (composeNamespaceMeta{}) {
return true
}
return false
}
func relNsToRef(ns *types.Namespace) string {
return firstOkString(ns.Slug, strconv.FormatUint(ns.ID, 10))
}
func relModToRef(mod *types.Module) string {
return firstOkString(mod.Handle, strconv.FormatUint(mod.ID, 10))
}
func relWfToRef(mod *atypes.Workflow) string {
return firstOkString(mod.Handle, strconv.FormatUint(mod.ID, 10))
}
func composeNamespaceErrNotFound(i string) error {
return fmt.Errorf("namespace not found: %v", i)
}

View File

@@ -0,0 +1,102 @@
package yaml
import (
"context"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
)
func (n *composeNamespace) Prepare(ctx context.Context, state *envoy.ResourceState) (err error) {
ns, ok := state.Res.(*resource.ComposeNamespace)
if !ok {
return encoderErrInvalidResource(types.NamespaceResourceType, state.Res.ResourceType())
}
n.meta = composeNamespaceMeta(ns.Res.Meta)
return nil
}
func (n *composeNamespace) Encode(ctx context.Context, doc *Document, state *envoy.ResourceState) (err error) {
if n.res.ID <= 0 {
n.res.ID = nextID()
}
n.ts, err = resource.MakeTimestampsCUDA(&n.res.CreatedAt, n.res.UpdatedAt, n.res.DeletedAt, nil).
Model(n.encoderConfig.TimeLayout, n.encoderConfig.Timezone)
if err != nil {
return err
}
// @todo skip eval?
doc.addComposeNamespace(n)
return nil
}
func (c *composeNamespace) MarshalYAML() (interface{}, error) {
var err error
nn, err := makeMap(
"namespaceID", c.res.ID,
"name", c.res.Name,
"slug", c.res.Slug,
"enabled", c.res.Enabled,
"labels", c.res.Labels,
)
if err != nil {
return nil, err
}
if !c.meta.empty() {
nn, err = addMap(nn,
"meta", c.res.Meta,
)
}
nn, err = encodeTimestamps(nn, c.ts)
if err != nil {
return nil, err
}
if len(c.modules) > 0 {
c.modules.configureEncoder(c.encoderConfig)
nn, err = encodeResource(nn, "modules", c.modules, c.encoderConfig.MappedOutput, "handle")
if err != nil {
return nil, err
}
}
if len(c.records) > 0 {
c.records.configureEncoder(c.encoderConfig)
// Records don't have this
nn, err = encodeResource(nn, "records", c.records, false, "")
if err != nil {
return nil, err
}
}
if len(c.pages) > 0 {
c.pages.configureEncoder(c.encoderConfig)
nn, err = encodeResource(nn, "pages", c.pages, c.encoderConfig.MappedOutput, "handle")
if err != nil {
return nil, err
}
}
if len(c.charts) > 0 {
c.charts.configureEncoder(c.encoderConfig)
nn, err = encodeResource(nn, "charts", c.charts, c.encoderConfig.MappedOutput, "handle")
if err != nil {
return nil, err
}
}
return nn, nil
}

View File

@@ -0,0 +1,78 @@
package yaml
import (
"testing"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
func TestComposeNamespace_UnmarshalYAML(t *testing.T) {
var (
parseString = func(src string) (*composeNamespace, error) {
w := &composeNamespace{}
return w, yaml.Unmarshal([]byte(src), w)
}
)
t.Run("empty", func(t *testing.T) {
req := require.New(t)
w, err := parseString(``)
req.NoError(err)
req.NotNil(w)
req.Nil(w.res)
})
t.Run("simple name", func(t *testing.T) {
req := require.New(t)
w, err := parseString(`{ name: Test }`)
req.NoError(err)
req.NotNil(w)
req.NotNil(w.res)
req.NotEmpty(w.res.Name)
req.True(w.res.Enabled)
})
t.Run("disabled", func(t *testing.T) {
req := require.New(t)
w, err := parseString(`{ enabled: false }`)
req.NoError(err)
req.NotNil(w)
req.NotNil(w.res)
req.False(w.res.Enabled)
})
t.Run("compose namespace 1", func(t *testing.T) {
// req := require.New(t)
// doc, err := parseDocument("compose_namespace_1")
// req.NoError(err)
// req.NotNil(doc)
// req.NotNil(doc.compose)
// req.Len(doc.compose.Namespaces, 1)
// req.NotNil(doc.compose.Namespaces.set[0])
// req.Equal("CRM", doc.compose.Namespaces.set[0].res.Name)
// req.Equal("crm", doc.compose.Namespaces.set[0].res.Slug)
// req.NotNil(doc.compose.Namespaces.set[0].rbac)
// req.NotEmpty(doc.compose.Namespaces.set[0].rbac)
})
}
func TestComposeNamespace_MarshalEnvoy(t *testing.T) {
var (
req = require.New(t)
wrap = composeNamespace{res: &types.Namespace{ID: 42}}
nn, err = wrap.MarshalEnvoy()
)
req.NoError(err)
req.Len(nn, 2) // namespace + resource translation
// req.IsType(&node.ComposeNamespace{}, nn[0])
// req.Equal(uint64(42), nn[0].(*node.ComposeNamespace).Res.ID)
}

View File

@@ -0,0 +1,143 @@
package yaml
import (
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/pkg/handle"
"github.com/cortezaproject/corteza/server/pkg/y7s"
"gopkg.in/yaml.v3"
)
func (wset *composeNamespaceSet) UnmarshalYAML(n *yaml.Node) error {
return y7s.Each(n, func(k, v *yaml.Node) (err error) {
var (
wrap = &composeNamespace{}
)
if v == nil {
return y7s.NodeErr(n, "malformed namespace definition")
}
if err = v.Decode(&wrap); err != nil {
return
}
if k != nil {
if wrap.res.Slug != "" {
return y7s.NodeErr(k, "cannot define slug in mapped namespace definition")
}
if !handle.IsValid(k.Value) {
return y7s.NodeErr(n, "namespace reference must be a valid handle")
}
// set namespace slug from map key value
wrap.res.Slug = k.Value
}
*wset = append(*wset, wrap)
return
})
}
func (wrap *composeNamespace) UnmarshalYAML(n *yaml.Node) (err error) {
if !y7s.IsKind(n, yaml.MappingNode) {
return y7s.NodeErr(n, "namespace definition must be a map or scalar")
}
if wrap.res == nil {
wrap.res = &types.Namespace{
// namespaces are enabled by default
Enabled: true,
}
}
if err = n.Decode(&wrap.res); err != nil {
return
}
if wrap.rbac, err = decodeRbac(n); err != nil {
return
}
if wrap.locale, err = decodeLocale(n); err != nil {
return
}
if wrap.envoyConfig, err = decodeEnvoyConfig(n); err != nil {
return
}
if wrap.ts, err = decodeTimestamps(n); err != nil {
return
}
return y7s.Each(n, func(k, v *yaml.Node) (err error) {
switch k.Value {
case "namespaceID":
return y7s.DecodeScalar(v, "namespace ID", &wrap.res.ID)
case "modules":
return v.Decode(&wrap.modules)
case "records":
return v.Decode(&wrap.records)
case "charts":
return v.Decode(&wrap.charts)
case "pages":
return v.Decode(&wrap.pages)
}
return nil
})
}
func (wset composeNamespaceSet) MarshalEnvoy() ([]resource.Interface, error) {
if wset == nil {
return []resource.Interface{}, nil
}
// namespace usually have bunch of sub-resources defined
nn := make([]resource.Interface, 0, len(wset)*10)
for _, res := range wset {
if tmp, err := res.MarshalEnvoy(); err != nil {
return nil, err
} else {
nn = append(nn, tmp...)
}
}
return nn, nil
}
func (wrap composeNamespace) MarshalEnvoy() ([]resource.Interface, error) {
nsr := resource.NewComposeNamespace(wrap.res)
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

@@ -0,0 +1,97 @@
package yaml
import (
"fmt"
"strconv"
atypes "github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
)
type (
composePage struct {
res *types.Page
ts *resource.Timestamps
envoyConfig *resource.EnvoyConfig
encoderConfig *EncoderConfig
children composePageSet
blocks composePageBlockSet
refNamespace string
relNs *types.Namespace
refModule string
relMod *types.Module
refParent string
relParent *types.Page
rbac rbacRuleSet
}
composePageSet []*composePage
composePageBlock struct {
res *types.PageBlock
relWf []*atypes.Workflow
refWf []string
relMod []*types.Module
refMod []string
relChart []*types.Chart
refChart []string
cfg *EncoderConfig
}
composePageBlockSet []*composePageBlock
composePageBlockStyle = types.PageBlockStyle
)
func (pg *composePage) matches(i string) bool {
if pg.res.ID > 0 && strconv.FormatUint(pg.res.ID, 10) == i {
return true
} else if pg.res.Handle == i {
return true
} else if pg.res.Title == i {
return true
}
return false
}
func (c composePageSet) addComposePage(ii string, p *composePage) error {
var cpg *composePage
for _, s := range c {
if s.matches(ii) {
cpg = s
break
}
}
if cpg == nil {
return composePageErrNotFound(ii)
}
if cpg.children == nil {
cpg.children = make(composePageSet, 0, 1)
}
cpg.children = append(cpg.children, p)
return nil
}
func (nn composePageSet) configureEncoder(cfg *EncoderConfig) {
for _, n := range nn {
n.encoderConfig = cfg
}
}
func (bb composePageBlockSet) configureEncoder(cfg *EncoderConfig) {
for _, b := range bb {
b.cfg = cfg
}
}
func composePageErrNotFound(i string) error {
return fmt.Errorf("page not found: %v", i)
}

View File

@@ -0,0 +1,300 @@
package yaml
import (
"context"
"strconv"
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"
)
func composePageFromResource(r *resource.ComposePage, cfg *EncoderConfig) *composePage {
return &composePage{
res: r.Res,
encoderConfig: cfg,
}
}
func (n *composePage) Prepare(ctx context.Context, state *envoy.ResourceState) (err error) {
pg, ok := state.Res.(*resource.ComposePage)
if !ok {
return encoderErrInvalidResource(composeTypes.PageResourceType, state.Res.ResourceType())
}
// Get the related namespace
n.relNs = resource.FindComposeNamespace(state.ParentResources, pg.RefNs.Identifiers)
if n.relNs == nil {
return resource.ComposeNamespaceErrUnresolved(pg.RefNs.Identifiers)
}
n.refNamespace = relNsToRef(n.relNs)
// Get the related parent; if any
if pg.RefParent != nil {
n.relParent = resource.FindComposePage(state.ParentResources, pg.RefParent.Identifiers)
if n.relParent == nil {
return resource.ComposePageErrUnresolved(pg.RefParent.Identifiers)
}
}
// Get the related module; if any
if pg.RefMod != nil {
n.relMod = resource.FindComposeModule(state.ParentResources, pg.RefMod.Identifiers)
if n.relMod == nil {
return resource.ComposePageErrUnresolved(pg.RefMod.Identifiers)
}
}
n.blocks = make(composePageBlockSet, 0, len(pg.Res.Blocks))
for i, b := range pg.Res.Blocks {
auxb := b
cpb := &composePageBlock{
res: &auxb,
}
// Check for refs
if refs, has := pg.BlockRefs[i]; has {
for _, ref := range refs {
switch ref.ResourceType {
case composeTypes.ModuleResourceType:
relMod := resource.FindComposeModule(state.ParentResources, ref.Identifiers)
if relMod == nil {
return resource.ComposeModuleErrUnresolved(ref.Identifiers)
}
cpb.relMod = append(cpb.relMod, relMod)
cpb.refMod = append(cpb.refMod, relModToRef(relMod))
case automationTypes.WorkflowResourceType:
relWf := resource.FindAutomationWorkflow(state.ParentResources, ref.Identifiers)
if relWf == nil {
return resource.AutomationWorkflowErrUnresolved(ref.Identifiers)
}
cpb.relWf = append(cpb.relWf, relWf)
cpb.refWf = append(cpb.refWf, relWfToRef(relWf))
case composeTypes.ChartResourceType:
relChart := resource.FindComposeChart(state.ParentResources, ref.Identifiers)
if relChart == nil {
return resource.ComposeChartErrUnresolved(ref.Identifiers)
}
cpb.relChart = append(cpb.relChart, relChart)
cpb.refChart = append(cpb.refChart, relChartToRef(relChart))
default:
return ErrUnknownResource
}
}
}
n.blocks = append(n.blocks, cpb)
}
return nil
}
func (n *composePage) Encode(ctx context.Context, doc *Document, state *envoy.ResourceState) (err error) {
if n.res.ID <= 0 {
n.res.ID = nextID()
}
if state.Conflicting {
return nil
}
// Timestaps
n.ts, err = resource.MakeTimestampsCUDA(&n.res.CreatedAt, n.res.UpdatedAt, n.res.DeletedAt, nil).
Model(n.encoderConfig.TimeLayout, n.encoderConfig.Timezone)
if err != nil {
return err
}
// @todo skip eval?
// if n.encoderConfig.CompactOutput {
// if n.refParent != "" {
// err = doc.nestComposePageChild(n.refParent, n)
// } else {
// err = doc.nestComposePage(n.refNamespace, n)
// }
// } else {
// doc.addComposePage(n)
// }
doc.addComposePage(n)
return err
}
func (p *composePage) MarshalYAML() (interface{}, error) {
var err error
if p.blocks != nil && len(p.blocks) > 0 {
p.blocks.configureEncoder(p.encoderConfig)
}
nn, _ := makeMap()
if p.relMod != nil {
nn, err = addMap(nn, "module", firstOkString(p.relMod.Handle, strconv.FormatUint(p.relMod.ID, 10)))
if err != nil {
return nil, err
}
}
if p.relParent != nil {
nn, err = addMap(nn, "parent", firstOkString(p.relParent.Handle, strconv.FormatUint(p.relParent.ID, 10)))
if err != nil {
return nil, err
}
}
nn, err = addMap(nn,
"pageID", p.res.ID,
"handle", p.res.Handle,
"title", p.res.Title,
"description", p.res.Description,
"blocks", p.blocks,
"labels", p.res.Labels,
"visible", p.res.Visible,
"weight", p.res.Weight,
)
if err != nil {
return nil, err
}
if p.children != nil && len(p.children) > 0 {
p.children.configureEncoder(p.encoderConfig)
nn, err = encodeResource(nn, "pages", p.children, p.encoderConfig.MappedOutput, "handle")
if err != nil {
return nil, err
}
}
nn, err = encodeTimestamps(nn, p.ts)
if err != nil {
return nil, err
}
return nn, nil
}
func (c *composePageBlock) MarshalYAML() (interface{}, error) {
if c.res.Kind == "RecordList" {
c.cleanupModuleFields(c.res.Options)
}
opt := c.res.Options
switch c.res.Kind {
case "RecordList":
delete(opt, "moduleID")
if len(c.refMod) > 0 {
opt["moduleID"] = c.refMod[0]
}
delete(opt, "module")
break
case "RecordOrganizer":
delete(opt, "moduleID")
if len(c.refMod) > 0 {
opt["moduleID"] = c.refMod[0]
}
delete(opt, "module")
break
case "Chart":
delete(opt, "chartID")
if len(c.refChart) > 0 {
opt["chartID"] = c.refChart[0]
}
delete(opt, "chart")
break
case "Calendar":
ff, _ := opt["feeds"].([]interface{})
for i, f := range ff {
feed, _ := f.(map[string]interface{})
fOpts, _ := (feed["options"]).(map[string]interface{})
fOpts["module"] = c.refMod[i]
delete(fOpts, "moduleID")
}
break
case "Automation":
bb, _ := opt["buttons"].([]interface{})
i := 0
for _, b := range bb {
button, _ := b.(map[string]interface{})
if _, has := button["workflowID"]; !has {
continue
}
button["workflow"] = c.refWf[i]
delete(button, "workflowID")
i++
}
break
case "Metric":
mm, _ := opt["metrics"].([]interface{})
for i, m := range mm {
mops, _ := m.(map[string]interface{})
mops["module"] = c.refMod[i]
delete(mops, "moduleID")
}
break
case "Comment":
delete(opt, "moduleID")
if len(c.refMod) > 0 {
opt["moduleID"] = c.refMod[0]
}
delete(opt, "module")
break
}
return makeMap(
"blockID", c.res.BlockID,
"title", c.res.Title,
"description", c.res.Description,
"options", c.res.Options,
"style", c.res.Style,
"kind", c.res.Kind,
// @todo figure out a way to have [x, y, w, h]; stringifying doesn't work
"xywh", c.res.XYWH,
)
}
// cleanupModuleFields handles cases where module fields are provided as full blown fields
// instead of just a name reference.
func (c *composePageBlock) cleanupModuleFields(opt map[string]interface{}) {
rawFF, has := opt["fields"]
if !has {
return
}
ff, ok := rawFF.([]interface{})
if !ok {
return
}
retFF := make([]interface{}, 0, len(ff))
for _, rawF := range ff {
switch c := rawF.(type) {
case string:
retFF = append(retFF, map[string]interface{}{"name": c})
case map[string]interface{}, map[string]string:
retFF = append(retFF, c)
default:
retFF = append(retFF, rawF)
}
}
opt["fields"] = retFF
}

View File

@@ -0,0 +1,50 @@
package yaml
import (
"testing"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
func TestComposePage_UnmarshalYAML(t *testing.T) {
var (
parseString = func(src string) (*composePage, error) {
w := &composePage{}
return w, yaml.Unmarshal([]byte(src), w)
}
)
t.Run("empty", func(t *testing.T) {
req := require.New(t)
w, err := parseString(``)
req.NoError(err)
req.NotNil(w)
req.Nil(w.res)
})
t.Run("simple name", func(t *testing.T) {
req := require.New(t)
w, err := parseString(`{ title: Test }`)
req.NoError(err)
req.NotNil(w)
req.NotNil(w.res)
req.NotEmpty(w.res.Title)
})
t.Run("doc 1", func(t *testing.T) {
req := require.New(t)
doc, err := parseDocument("compose_page_1")
req.NoError(err)
req.NotNil(doc)
req.NotNil(doc.compose)
req.Len(doc.compose.Pages, 1)
req.Len(doc.compose.Pages[0].children, 1)
req.Equal(3, len(doc.compose.Pages[0].res.Blocks))
req.NotNil(doc.compose.Pages[0].rbac)
req.NotEmpty(doc.compose.Pages[0].rbac)
})
}

View File

@@ -0,0 +1,191 @@
package yaml
import (
"fmt"
"strings"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/pkg/y7s"
"gopkg.in/yaml.v3"
)
func (wset *composePageSet) UnmarshalYAML(n *yaml.Node) error {
wx := make(map[string]int)
return y7s.Each(n, func(k, v *yaml.Node) (err error) {
var (
wrap = &composePage{
// Set this to something negative so we have an easier time determining
// if we should fix the pages weight
res: &types.Page{Weight: -1},
}
)
if v == nil {
return y7s.NodeErr(n, "malformed page definition")
}
if err = v.Decode(&wrap); err != nil {
return
}
if err = decodeRef(k, "page", &wrap.res.Handle); err != nil {
return
}
if wrap.res.Title == "" {
// if name is not set, use handle
wrap.res.Title = wrap.res.Handle
}
if wrap.res.Weight < 0 {
wrap.res.Weight = wx[wrap.refParent]
}
wx[wrap.refParent]++
*wset = append(*wset, wrap)
return
})
}
func (wrap *composePage) UnmarshalYAML(n *yaml.Node) (err error) {
if wrap.res == nil {
wrap.rbac = make(rbacRuleSet, 0, 10)
wrap.res = &types.Page{
// Pages are visible by default
Visible: true,
}
}
if wrap.rbac, err = decodeRbac(n); err != nil {
return
}
if wrap.envoyConfig, err = decodeEnvoyConfig(n); err != nil {
return
}
if wrap.ts, err = decodeTimestamps(n); err != nil {
return
}
return y7s.EachMap(n, func(k, v *yaml.Node) (err error) {
switch strings.ToLower(k.Value) {
case "id", "pageid", "pageID":
return y7s.DecodeScalar(v, "page ID", &wrap.res.ID)
case "title":
return y7s.DecodeScalar(v, "page title", &wrap.res.Title)
case "handle":
return y7s.DecodeScalar(v, "page handle", &wrap.res.Handle)
case "module":
return y7s.DecodeScalar(v, "page module", &wrap.refModule)
case "parent", "selfid":
return y7s.DecodeScalar(v, "page parent", &wrap.refParent)
case "weight", "order":
return y7s.DecodeScalar(v, "page weight", &wrap.res.Weight)
case "visible":
return y7s.DecodeScalar(v, "page visible", &wrap.res.Visible)
case "description":
return y7s.DecodeScalar(v, "page description", &wrap.res.Description)
case "blocks":
var cpb = make(composePageBlockSet, 0)
if err = v.Decode(&cpb); err != nil {
return err
}
wrap.res.Blocks = make([]types.PageBlock, len(cpb))
for i, b := range cpb {
wrap.res.Blocks[i] = *b.res
}
case "children",
"pages":
return v.Decode(&wrap.children)
}
return nil
})
}
func (wrap *composePageBlock) UnmarshalYAML(n *yaml.Node) (err error) {
var b types.PageBlock
err = n.Decode(&b)
if err != nil {
return err
}
wrap.res = &b
return nil
}
// @todo also do this for the pages property
func (wset composePageSet) setNamespaceRef(ref string) error {
for _, res := range wset {
if res.refNamespace != "" && ref != res.refNamespace {
return fmt.Errorf("cannot override namespace reference %s with %s", res.refNamespace, ref)
}
res.refNamespace = ref
if res.children != nil {
res.children.setNamespaceRef(ref)
}
}
return nil
}
func (wset composePageSet) MarshalEnvoy() ([]resource.Interface, error) {
// namespace usually have bunch of sub-resources defined
nn := make([]resource.Interface, 0, len(wset)*10)
for _, res := range wset {
if tmp, err := res.MarshalEnvoy(); err != nil {
return nil, err
} else {
nn = append(nn, tmp...)
}
}
return nn, nil
}
func (wrap composePage) MarshalEnvoy() ([]resource.Interface, error) {
rs := resource.NewComposePage(
wrap.res,
resource.MakeNamespaceRef(0, wrap.refNamespace, ""),
resource.MakeModuleRef(0, wrap.refModule, ""),
resource.MakePageRef(0, wrap.refParent, ""),
)
rs.SetTimestamps(wrap.ts)
rs.SetConfig(wrap.envoyConfig)
return envoy.CollectNodes(
rs,
wrap.children.bindParent(rs),
wrap.rbac.bindResource(rs),
)
}
func (rr composePageSet) bindParent(res resource.Interface) composePageSet {
rtr := make(composePageSet, 0, len(rr))
for _, r := range rr {
idd := res.Identifiers().StringSlice()
if len(idd) > 0 {
r.refParent = idd[0]
}
rtr = append(rtr, r)
}
return rtr
}

View File

@@ -0,0 +1,34 @@
package yaml
import (
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
)
type (
composeRecord struct {
id string
values map[string]string
ts *resource.Timestamps
us *resource.Userstamps
config *resource.EnvoyConfig
cfg *EncoderConfig
refModule string
refNamespace string
rbac rbacRuleSet
}
composeRecordSet []*composeRecord
composeRecordValues struct {
rvs types.RecordValueSet
}
)
func (nn composeRecordSet) configureEncoder(cfg *EncoderConfig) {
for _, n := range nn {
n.cfg = cfg
}
}

View File

@@ -0,0 +1,164 @@
package yaml
import (
"context"
"fmt"
"strconv"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
)
type (
composeRecordEncoder struct {
encoderConfig *EncoderConfig
res *resource.ComposeRecord
refNamespace string
relNs *types.Namespace
refModule string
relMod *types.Module
}
)
func composeRecordSetFromResource(rec *resource.ComposeRecord, cfg *EncoderConfig) *composeRecordEncoder {
return &composeRecordEncoder{
res: rec,
encoderConfig: cfg,
}
}
func (n *composeRecordEncoder) Prepare(ctx context.Context, state *envoy.ResourceState) (err error) {
rec, ok := state.Res.(*resource.ComposeRecord)
if !ok {
return encoderErrInvalidResource(types.RecordResourceType, state.Res.ResourceType())
}
// Get the related namespace
n.relNs = resource.FindComposeNamespace(state.ParentResources, rec.RefNs.Identifiers)
if n.relNs == nil {
return resource.ComposeNamespaceErrUnresolved(rec.RefNs.Identifiers)
}
// Get the related module
n.relMod = resource.FindComposeModule(state.ParentResources, rec.RefMod.Identifiers)
if n.relNs == nil {
return resource.ComposeNamespaceErrUnresolved(rec.RefMod.Identifiers)
}
n.refNamespace = relNsToRef(n.relNs)
n.refModule = relModToRef(n.relMod)
return nil
}
func (n *composeRecordEncoder) Encode(ctx context.Context, doc *Document, state *envoy.ResourceState) (err error) {
pickLast := func(v resource.ComposeRecordRawValue) string {
if v.IsMulti {
if len(v.Values) > 0 {
return v.Values[len(v.Values)-1]
}
return ""
}
return v.Value
}
return n.res.Walker(func(r *resource.ComposeRecordRaw) (err error) {
n.res.IDMap[r.ID], err = strconv.ParseUint(r.ID, 10, 64)
if err != nil {
return err
}
cr := &composeRecord{
id: r.ID,
us: r.Us,
config: r.Config,
refModule: n.refModule,
refNamespace: n.refNamespace,
}
cr.us, err = resolveUserstamps(state.ParentResources, r.Us)
if err != nil {
return err
}
cr.ts, err = r.Ts.Model(n.encoderConfig.TimeLayout, n.encoderConfig.Timezone)
if err != nil {
return err
}
vv := r.Values
cr.values = make(map[string]string)
for k, v := range vv {
f := n.res.RelMod.Fields.FindByName(k)
if f == nil {
return fmt.Errorf("field %s not found", k)
}
// Resolve user references
if f.Kind == "User" {
cr.values[k], err = n.res.UserFlakes.GetByKey(pickLast(v)).Model()
if err != nil {
return err
}
} else {
cr.values[k] = pickLast(v)
}
}
doc.addComposeRecord(cr)
return nil
})
}
func (rr composeRecordSet) MarshalYAML() (interface{}, error) {
byMod := make(map[string]composeRecordSet)
for _, r := range rr {
if _, has := byMod[r.refModule]; !has {
byMod[r.refModule] = make(composeRecordSet, 0, 100)
}
byMod[r.refModule] = append(byMod[r.refModule], r)
}
nn, _ := makeMap()
for refMod, rr := range byMod {
ss, err := makeSeq()
for _, r := range rr {
ss, err = addSeq(ss, r)
if err != nil {
return nil, err
}
}
nn, err = addMap(nn,
refMod, ss,
)
}
return nn, nil
}
func (c *composeRecord) MarshalYAML() (interface{}, error) {
nn, err := makeMap(
"recordID", c.id,
"values", c.values,
)
if err != nil {
return nil, err
}
nn, err = encodeTimestamps(nn, c.ts)
if err != nil {
return nil, err
}
nn, err = encodeUserstamps(nn, c.us)
if err != nil {
return nil, err
}
return nn, nil
}

View File

@@ -0,0 +1,90 @@
package yaml
import (
"fmt"
"testing"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
func TestComposeRecord_UnmarshalYAML(t *testing.T) {
var (
parseString = func(src string) (*composeRecord, error) {
w := &composeRecord{
values: make(map[string]string),
}
return w, yaml.Unmarshal([]byte(src), w)
}
)
t.Run("empty", func(t *testing.T) {
req := require.New(t)
w, err := parseString(``)
req.NoError(err)
req.NotNil(w)
req.Empty(w.values)
})
t.Run("empty", func(t *testing.T) {
req := require.New(t)
w, err := parseString(`{ values: { foo: bar }, createdBy: foo, updatedAt: 2020-10-10T10:10:00Z }`)
req.NoError(err)
req.NotNil(w)
req.NotEmpty(w.values)
req.NotNil(w.ts)
req.NotNil(w.us)
req.Equal("bar", w.values["foo"])
})
t.Run("compose record file 1", func(t *testing.T) {
req := require.New(t)
doc, err := parseDocument("compose_record_1")
req.NoError(err)
req.NotNil(doc)
req.NotNil(doc.compose)
req.Len(doc.compose.Records, 3)
req.NotEmpty(doc.compose.Records[0].values)
req.Equal("Department", doc.compose.Records[0].refModule)
v := doc.compose.Records[0].values
req.Equal("Service", v["Name"])
req.Equal("50", v["HourCost"])
req.NotEmpty(doc.compose.Records[1].values)
req.Equal("EmailTemplate", doc.compose.Records[1].refModule)
req.NotEmpty(doc.compose.Records[2].values)
req.Equal("Settings", doc.compose.Records[2].refModule)
//req.NotNil(doc.compose.records[0].rbac)
//req.NotEmpty(doc.compose.records[0].rbac.rules)
})
}
func TestComposeRecord_MarshalEnvoy(t *testing.T) {
t.Run("compose record file 2", func(t *testing.T) {
req := require.New(t)
doc, err := parseDocument("compose_record_2")
rr, err := doc.compose.MarshalEnvoy()
req.NoError(err)
req.Len(rr, 3)
for ri, r := range rr {
rec, ok := r.(*resource.ComposeRecord)
req.True(ok)
req.NotNil(rec)
rec.Walker(func(r *resource.ComposeRecordRaw) error {
req.Equal(fmt.Sprintf("mod%d f1 v1", ri+1), r.Values["f1"].Value)
req.Equal(fmt.Sprintf("mod%d f2 v1", ri+1), r.Values["f2"].Value)
return nil
})
}
})
}

View File

@@ -0,0 +1,204 @@
package yaml
import (
"fmt"
"strings"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/pkg/y7s"
"gopkg.in/yaml.v3"
)
// UnmarshalYAML resolves set of record definitions, either sequence or map
//
// When resolving map, key is used as module handle
func (wset *composeRecordSet) UnmarshalYAML(n *yaml.Node) error {
return y7s.Each(n, func(k, v *yaml.Node) (err error) {
var (
moduleRef string
)
if v == nil {
return y7s.NodeErr(n, "malformed record definition")
}
if err = y7s.DecodeScalar(k, "module", &moduleRef); err != nil {
return
}
if y7s.IsKind(v, yaml.SequenceNode) {
// multiple records defined
return y7s.EachSeq(v, func(r *yaml.Node) error {
var wrap = &composeRecord{refModule: moduleRef}
if err = r.Decode(&wrap); err != nil {
return err
}
*wset = append(*wset, wrap)
return nil
})
}
if y7s.IsKind(v, yaml.MappingNode) {
// one record defined
var wrap = &composeRecord{refModule: moduleRef}
if err = v.Decode(&wrap); err != nil {
return
}
*wset = append(*wset, wrap)
}
return nil
})
}
func (wrap *composeRecord) UnmarshalYAML(n *yaml.Node) (err error) {
if wrap.values == nil {
wrap.values = make(map[string]string)
}
if wrap.config, err = decodeEnvoyConfig(n); err != nil {
return
}
if wrap.ts, err = decodeTimestamps(n); err != nil {
return
}
if wrap.us, err = decodeUserstamps(n); err != nil {
return
}
return y7s.EachMap(n, func(k, v *yaml.Node) error {
switch strings.ToLower(k.Value) {
case "id",
"recordid":
return y7s.DecodeScalar(v, "recordID", &wrap.id)
case "module":
return y7s.DecodeScalar(v, "module", &wrap.refModule)
case "allow", "deny":
return fmt.Errorf("compose record RBAC rules not supported on resource level")
case "values":
// Use aux structure to decode record values into RVS
if err := v.Decode(&wrap.values); err != nil {
return err
}
return nil
}
return nil
})
}
// MarshalEnvoy works a bit differenlty
func (wset composeRecordSet) MarshalEnvoy() ([]resource.Interface, error) {
nn := make([]resource.Interface, 0, len(wset))
type (
auxRecord struct {
rr resource.ComposeRecordRawSet
nsRef string
modRef string
}
)
// We'll do a list of wrappers & a map of wrappers to preserve order and keep
// optimal lookups
rww := make([]*auxRecord, 0, len(wset))
rrx := make(map[string]*auxRecord)
for _, res := range wset {
// A bit stronger index just in case
ix := res.refNamespace + "/" + res.refModule
if _, ok := rrx[ix]; !ok {
rrx[ix] = &auxRecord{
rr: make(resource.ComposeRecordRawSet, 0, 10),
nsRef: res.refNamespace,
modRef: res.refModule,
}
rww = append(rww, rrx[ix])
}
auxValues := make(map[string]resource.ComposeRecordRawValue)
for k, v := range res.values {
auxValues[k] = resource.ComposeRecordRawValue{
Name: k,
Value: v,
}
}
r := &resource.ComposeRecordRaw{
ID: res.getID(),
Config: res.config,
Values: auxValues,
Ts: res.ts,
Us: res.us,
}
rrx[ix].rr = append(rrx[ix].rr, r)
}
for _, w := range rww {
cw := *w
walker := func(f func(r *resource.ComposeRecordRaw) error) error {
for _, r := range cw.rr {
err := f(r)
if err != nil {
return err
}
}
return nil
}
n := resource.NewComposeRecordSet(walker, w.nsRef, w.modRef)
// Empty userstamp index so the referencing will work with wildcards
n.SetUserFlakes(make(resource.UserstampIndex))
// Take note of the record ID's that are provided.
// This will later let us find existing records.
for _, r := range w.rr {
if r.ID != "" {
n.IDMap[r.ID] = 0
}
}
nn = append(nn, n)
}
return nn, nil
}
func (wset composeRecordSet) setNamespaceRef(ref string) error {
for _, res := range wset {
if res.refNamespace != "" && ref != res.refNamespace {
return fmt.Errorf("cannot override namespace reference %s with %s", res.refNamespace, ref)
}
res.refNamespace = ref
}
return nil
}
// Utilities
func (wrap *composeRecord) getID() string {
if wrap.id != "" {
return wrap.id
}
if wrap.values["id"] != "" {
return wrap.values["id"]
} else if wrap.values["ID"] != "" {
return wrap.values["ID"]
} else if wrap.values["recordID"] != "" {
return wrap.values["recordID"]
}
return ""
}

View File

@@ -0,0 +1,55 @@
package yaml
import (
"testing"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
func TestCompose_UnmarshalYAML(t *testing.T) {
var (
parse = func(c *compose, src string) (*compose, error) {
return c, yaml.Unmarshal([]byte(src), c)
}
)
t.Run("empty", func(t *testing.T) {
req := require.New(t)
c, err := parse(&compose{}, ``)
req.NoError(err)
req.NotNil(c)
req.Empty(c.Namespaces)
})
t.Run("sequence of namespaces", func(t *testing.T) {
req := require.New(t)
c, err := parse(&compose{}, `namespaces: [ { } ]`)
req.NoError(err)
req.NotNil(c)
req.Len(c.Namespaces, 1)
})
t.Run("map of namespaces", func(t *testing.T) {
req := require.New(t)
c, err := parse(&compose{}, `namespaces: { test: {} }`)
req.NoError(err)
req.NotNil(c)
req.Len(c.Namespaces, 1)
})
t.Run("malformed namespace ref", func(t *testing.T) {
req := require.New(t)
_, err := parse(&compose{}, `namespace: { }`)
req.Error(err)
})
t.Run("namespace ref", func(t *testing.T) {
req := require.New(t)
c, err := parse(&compose{}, `namespace: foo`)
req.NoError(err)
req.NotNil(c)
req.Empty(c.Namespaces, "namespace ref should not result in namespace definition")
})
}

View File

@@ -0,0 +1,85 @@
package yaml
import (
"context"
"io"
"regexp"
"strings"
"github.com/cortezaproject/corteza/server/pkg/y7s"
. "github.com/cortezaproject/corteza/server/pkg/y7s"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"gopkg.in/yaml.v3"
)
type (
// decoder is a wrapper struct for yaml related methods
decoder struct{}
EnvoyMarshler interface {
MarshalEnvoy() ([]resource.Interface, error)
}
)
var (
validReference = regexp.MustCompile(`^[A-Za-z1-9][0-9A-Za-z_\-.]*[A-Za-z0-9]$`)
)
func Decoder() *decoder {
return &decoder{}
}
// CanDecodeFile checks if the file can be handled by this decoder
//
// @todo Add support for this; current library is unable to detect this.
func (y *decoder) CanDecodeFile(f io.Reader) bool {
// relying on can-decode-ext for now
return true
}
func (y *decoder) CanDecodeExt(name string) bool {
var (
pt = strings.Split(name, ".")
ext = strings.TrimSpace(pt[len(pt)-1])
)
return ext == "yaml" || ext == "yml"
}
func (y *decoder) Decode(ctx context.Context, r io.Reader, dctx *envoy.DecoderOpts) ([]resource.Interface, error) {
var (
doc = &Document{}
)
if err := yaml.NewDecoder(r).Decode(doc); err != nil {
return nil, err
}
aaa, err := doc.Decode(ctx)
return aaa, err
}
// Checks validity of ref node and sets the value to given arg ptr
func decodeRef(n *yaml.Node, refType string, ref *string) error {
if n == nil {
return nil
}
if !IsKind(n, yaml.ScalarNode) {
return y7s.NodeErr(n, "%s reference must be scalar", refType)
}
if !IsValidRef(n.Value) {
return y7s.NodeErr(n, "%s reference must be a valid ID or Handle", refType)
}
*ref = n.Value
return nil
}
func IsValidRef(s string) bool {
return len(s) >= 2 && validReference.MatchString(s)
}

View File

@@ -0,0 +1,422 @@
package yaml
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/pkg/y7s"
"gopkg.in/yaml.v3"
)
type (
// Document defines the supported yaml structure
Document struct {
compose *compose
automation *automation
roles roleSet
users userSet
templates templateSet
applications applicationSet
apiGateway apiGatewaySet
reports reportSet
settings settingSet
rbac rbacRuleSet
locale resourceTranslationSet
cfg *EncoderConfig
}
)
func (doc *Document) UnmarshalYAML(n *yaml.Node) (err error) {
if err = n.Decode(&doc.compose); err != nil {
return
}
if err = n.Decode(&doc.automation); err != nil {
return
}
if doc.rbac, err = decodeRbac(n); err != nil {
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":
return v.Decode(&doc.roles)
case "users":
return v.Decode(&doc.users)
case "templates":
return v.Decode(&doc.templates)
case "apigateway", "apigw":
return v.Decode(&doc.apiGateway)
case "reports":
return v.Decode(&doc.reports)
case "applications":
return v.Decode(&doc.applications)
case "settings":
return v.Decode(&doc.settings)
}
return nil
})
}
func (doc *Document) MarshalYAML() (interface{}, error) {
var err error
dn, _ := makeMap()
if doc.compose != nil {
doc.compose.EncoderConfig = doc.cfg
cn, err := encodeNode(doc.compose)
if err != nil {
return nil, err
}
dn, _ = inlineContent(dn, cn)
}
if doc.automation != nil {
doc.automation.EncoderConfig = doc.cfg
cn, err := encodeNode(doc.automation)
if err != nil {
return nil, err
}
dn, _ = inlineContent(dn, cn)
}
if doc.roles != nil {
doc.roles.configureEncoder(doc.cfg)
dn, err = encodeResource(dn, "roles", doc.roles, doc.cfg.MappedOutput, "handle")
if err != nil {
return nil, err
}
}
if doc.users != nil && len(doc.users) > 0 {
doc.users.configureEncoder(doc.cfg)
dn, err = encodeResource(dn, "users", doc.users, doc.cfg.MappedOutput, "handle")
if err != nil {
return nil, err
}
}
if doc.templates != nil && len(doc.templates) > 0 {
doc.templates.configureEncoder(doc.cfg)
dn, err = encodeResource(dn, "templates", doc.templates, doc.cfg.MappedOutput, "handle")
if err != nil {
return nil, err
}
}
if doc.reports != nil && len(doc.reports) > 0 {
doc.reports.configureEncoder(doc.cfg)
dn, err = encodeResource(dn, "reports", doc.reports, doc.cfg.MappedOutput, "handle")
if err != nil {
return nil, err
}
}
if doc.apiGateway != nil && len(doc.apiGateway) > 0 {
doc.apiGateway.configureEncoder(doc.cfg)
// API GW don't support map representation
// @todo use path+proto?
dn, err = addMap(dn,
"apigateway", doc.apiGateway,
)
if err != nil {
return nil, err
}
}
if doc.applications != nil && len(doc.applications) > 0 {
doc.applications.configureEncoder(doc.cfg)
// Applications don't support map representation
dn, err = addMap(dn,
"applications", doc.applications,
)
if err != nil {
return nil, err
}
}
if doc.settings != nil {
dn, err = encodeResource(dn, "settings", doc.settings, doc.cfg.MappedOutput, "name")
if err != nil {
return nil, err
}
}
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)
if err != nil {
return nil, err
}
dn, err = inlineContent(dn, m)
if err != nil {
return nil, err
}
}
return dn, nil
}
func (doc *Document) Decode(ctx context.Context) ([]resource.Interface, error) {
nn := make([]resource.Interface, 0, 100)
mm := make([]envoy.Marshaller, 0, 20)
if doc.compose != nil {
mm = append(mm, doc.compose)
}
if doc.roles != nil {
mm = append(mm, doc.roles)
}
if doc.users != nil {
mm = append(mm, doc.users)
}
if doc.templates != nil {
mm = append(mm, doc.templates)
}
if doc.automation != nil {
mm = append(mm, doc.automation)
}
if doc.applications != nil {
mm = append(mm, doc.applications)
}
if doc.reports != nil {
mm = append(mm, doc.reports)
}
if doc.apiGateway != nil {
mm = append(mm, doc.apiGateway)
}
if doc.settings != nil {
for _, s := range doc.settings {
mm = append(mm, s)
}
}
if doc.locale != nil {
mm = append(mm, doc.locale)
}
if doc.rbac != nil {
mm = append(mm, doc.rbac)
}
for _, m := range mm {
if tmp, err := m.MarshalEnvoy(); err != nil {
return nil, err
} else {
nn = append(nn, tmp...)
}
}
return nn, nil
}
func (doc *Document) addComposeNamespace(n *composeNamespace) {
if doc.compose == nil {
doc.compose = &compose{}
}
if doc.compose.Namespaces == nil {
doc.compose.Namespaces = make(composeNamespaceSet, 0, 3)
}
doc.compose.Namespaces = append(doc.compose.Namespaces, n)
}
func (doc *Document) addComposeModule(m *composeModule) {
if doc.compose == nil {
doc.compose = &compose{}
}
if doc.compose.Modules == nil {
doc.compose.Modules = make(composeModuleSet, 0)
}
doc.compose.Modules = append(doc.compose.Modules, m)
}
func (doc *Document) addComposeRecord(r *composeRecord) {
if doc.compose == nil {
doc.compose = &compose{}
}
if doc.compose.Records == nil {
doc.compose.Records = make(composeRecordSet, 0)
}
doc.compose.Records = append(doc.compose.Records, r)
}
func (doc *Document) addComposePage(p *composePage) {
if doc.compose == nil {
doc.compose = &compose{}
}
if doc.compose.Pages == nil {
doc.compose.Pages = make(composePageSet, 0)
}
doc.compose.Pages = append(doc.compose.Pages, p)
}
func (doc *Document) addComposeChart(c *composeChart) {
if doc.compose == nil {
doc.compose = &compose{}
}
if doc.compose.Charts == nil {
doc.compose.Charts = make(composeChartSet, 0)
}
doc.compose.Charts = append(doc.compose.Charts, c)
}
func (doc *Document) addAutomationWorkflow(m *automationWorkflow) {
if doc.automation == nil {
doc.automation = &automation{}
}
if doc.automation.Workflows == nil {
doc.automation.Workflows = make(automationWorkflowSet, 0)
}
doc.automation.Workflows = append(doc.automation.Workflows, m)
}
func (doc *Document) addApiGateway(a *apiGateway) {
doc.apiGateway = append(doc.apiGateway, a)
}
func (doc *Document) addReport(a *report) {
doc.reports = append(doc.reports, a)
}
func (doc *Document) addRole(r *role) {
if doc.roles == nil {
doc.roles = make(roleSet, 0, 20)
}
doc.roles = append(doc.roles, r)
}
func (doc *Document) addUser(u *user) {
if doc.users == nil {
doc.users = make(userSet, 0, 20)
}
doc.users = append(doc.users, u)
}
func (doc *Document) addTemplate(u *template) {
if doc.templates == nil {
doc.templates = make(templateSet, 0, 20)
}
doc.templates = append(doc.templates, u)
}
func (doc *Document) addApplication(a *application) {
if doc.applications == nil {
doc.applications = make(applicationSet, 0, 20)
}
doc.applications = append(doc.applications, a)
}
func (doc *Document) addSetting(s *setting) {
if doc.settings == nil {
doc.settings = make([]*setting, 0, 100)
}
doc.settings = append(doc.settings, s)
}
func (doc *Document) addRbacRule(r *rbacRule) {
if doc.rbac == nil {
doc.rbac = make(rbacRuleSet, 0, 100)
}
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{}
}
if doc.compose.Namespaces == nil {
return composeNamespaceErrNotFound(ns)
}
return doc.compose.Namespaces.addComposeModule(ns, m)
}
func (doc *Document) nestComposePage(ns string, p *composePage) error {
if doc.compose == nil {
doc.compose = &compose{}
}
if doc.compose.Namespaces == nil {
return composeNamespaceErrNotFound(ns)
}
return doc.compose.Namespaces.addComposePage(ns, p)
}
func (doc *Document) nestComposePageChild(parent string, p *composePage) error {
if doc.compose == nil {
doc.compose = &compose{}
}
if doc.compose.Namespaces == nil {
return composeNamespaceErrNotFound(parent)
}
return doc.compose.Pages.addComposePage(parent, p)
}
func (doc *Document) nestComposeChart(ns string, c *composeChart) error {
if doc.compose == nil {
doc.compose = &compose{}
}
if doc.compose.Namespaces == nil {
return composeNamespaceErrNotFound(ns)
}
return doc.compose.Namespaces.addComposeChart(ns, c)
}

View File

@@ -0,0 +1,18 @@
package yaml
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestDocument_UnmarshalYAML(t *testing.T) {
t.Run("doc 1", func(t *testing.T) {
req := require.New(t)
doc, err := parseDocument("global_rbac_1")
req.NoError(err)
req.NotNil(doc)
req.NotNil(doc.rbac)
req.NotEmpty(doc.rbac)
})
}

View File

@@ -0,0 +1,302 @@
package yaml
import (
"bytes"
"context"
"fmt"
"io"
"strings"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"gopkg.in/yaml.v3"
)
type (
yamlEncoder struct {
cfg *EncoderConfig
resState map[resource.Interface]resourceState
Documents encoderState
}
// encodedDocument holds the yaml encoded document sources.
// Use the reader for further processing (file creation, HTTP, ...)
encodedDocument struct {
doc *Document
Source io.Reader
ResourceType string
Scope string
Identifier string
}
encodedDocumentSet []*encodedDocument
// encoderState holds all of the generated, encoded documents indexed by resource type
encoderState map[string]encodedDocumentSet
// EncoderConfig allows us to configure the resource encoding process
EncoderConfig struct {
// Skip if defines a pkg/expr expression when to skip the resource
SkipIf string
// Defer is called after the resource is encoded, regardles of the result
Defer func()
// DeferOk is called after the resource is encoded, only when successful
DeferOk func()
// DeferNok is called after the resource is encoded, only when failed
// If you return an error, the encoding will terminate.
// If you return nil (ignore the error), the encoding will continue.
DeferNok func(error) error
// Timezone defines what timezone should be used when encoding timestamps
//
// If not defined, UTC is used
Timezone string
// TimeLayout defines how to format the encoded timestamp
//
// If not defined, RFC3339 is used (this one - 2006-01-02T15:04:05Z07:00)
TimeLayout string
// // CompactOutput forces the output to be as compact as possible
// CompactOutput bool
// MappedOutput forces the sequences to encode as maps (where possible)
MappedOutput bool
// @todo different output structuring
}
// resourceState holds some intermedia values to help with encoding
resourceState interface {
Prepare(ctx context.Context, state *envoy.ResourceState) (err error)
Encode(ctx context.Context, ds *Document, state *envoy.ResourceState) (err error)
}
)
var (
ErrUnknownResource = fmt.Errorf("unknown resource")
ErrResourceStateUndefined = fmt.Errorf("undefined resource state")
ErrInvalidResourceType = fmt.Errorf("invalid resource state")
)
// NewYamlEncoder initializes a fresh yaml encoder
func NewYamlEncoder(cfg *EncoderConfig) envoy.PrepareEncodeStreamer {
if cfg == nil {
cfg = &EncoderConfig{}
}
return &yamlEncoder{
cfg: cfg,
Documents: make(encoderState),
resState: make(map[resource.Interface]resourceState),
}
}
// Prepare prepares the encoder for the given set of resources
//
// It initializes and prepares the resource state for each provided resource
func (ye *yamlEncoder) Prepare(ctx context.Context, ee ...*envoy.ResourceState) (err error) {
f := func(rs resourceState, es *envoy.ResourceState) error {
if rs == nil {
return nil
}
err = rs.Prepare(ctx, es)
if err != nil {
return err
}
ye.resState[es.Res] = rs
return nil
}
for _, e := range ee {
// Skip placeholders
if e.Res.Placeholder() {
continue
}
switch res := e.Res.(type) {
// Compose resources
case *resource.ComposeNamespace:
err = f(composeNamespaceFromResource(res, ye.cfg), e)
case *resource.ComposeModule:
err = f(composeModuleFromResource(res, ye.cfg), e)
case *resource.ComposeRecord:
err = f(composeRecordSetFromResource(res, ye.cfg), e)
case *resource.ComposePage:
err = f(composePageFromResource(res, ye.cfg), e)
case *resource.ComposeChart:
err = f(composeChartFromResource(res, ye.cfg), e)
// System resources
case *resource.Role:
err = f(roleFromResource(res, ye.cfg), e)
case *resource.User:
err = f(userFromResource(res, ye.cfg), e)
case *resource.Template:
err = f(templateFromResource(res, ye.cfg), e)
case *resource.Application:
err = f(applicationFromResource(res, ye.cfg), e)
case *resource.APIGateway:
err = f(apiGatewayFromResource(res, ye.cfg), e)
case *resource.Report:
err = f(reportFromResource(res, ye.cfg), e)
case *resource.Setting:
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:
err = f(automationWorkflowFromResource(res, ye.cfg), e)
default:
err = ErrUnknownResource
}
if err != nil {
return ye.WrapError("prepare", e.Res, err)
}
}
return nil
}
// Encode encodes the resources into a series of yaml documents
//
// The document structure follows the provided configuration as much as possible,
// but some modifications may be permitted (such as splitting compose resources that belong
// to different namespaces).
//
// @todo improve document structuring; the base encodes each resource type into it's own document.
//
// This is good enough for now but should be expanded in the near future.
func (ye *yamlEncoder) Encode(ctx context.Context, p envoy.Provider) error {
var e *envoy.ResourceState
var err error
// Encode the resources into document structs
for {
e, err = p.NextInverted(ctx)
if err != nil {
return err
}
if e == nil {
break
}
// Skip placeholders
if e.Res.Placeholder() {
continue
}
state := ye.resState[e.Res]
if state == nil {
err = ErrResourceStateUndefined
} else {
// Determine the document that we should encode into
//
// @todo improve flexibility
c := ye.Documents.byResourceType(e.Res.ResourceType())
if c.doc.cfg == nil {
c.doc.cfg = ye.cfg
}
err = state.Encode(ctx, c.doc, e)
}
if err != nil {
return ye.WrapError("encode: build doc", e.Res, err)
}
}
// Encode documents into yaml document streams
for _, d := range ye.Documents.all() {
if err := d.encode(); err != nil {
return err
}
// The source Document doesn't need to exist after this step
d.doc = nil
}
return nil
}
func (se *yamlEncoder) Stream() []*envoy.Stream {
ss := make([]*envoy.Stream, 0, 20)
for _, dd := range se.Documents {
for _, d := range dd {
ss = append(ss, &envoy.Stream{
Resource: d.ResourceType,
Identifier: d.Identifier,
Source: d.Source,
})
}
}
return ss
}
// WrapError wraps errors related to yaml encoding
//
// Always wrap your errors.
func (se *yamlEncoder) WrapError(act string, res resource.Interface, err error) error {
rt := strings.Join(strings.Split(strings.TrimSpace(strings.TrimRight(res.ResourceType(), ":")), ":"), " ")
return fmt.Errorf("yaml encoder %s %s %v: %s", act, rt, res.Identifiers().StringSlice(), err)
}
func encoderErrInvalidResource(exp, got string) error {
return fmt.Errorf("invalid resource type: expecting %s, got %s", exp, got)
}
// byResourceType returns the first document used for a specific resource type
func (ec encoderState) byResourceType(rt string) *encodedDocument {
if _, has := ec[rt]; !has {
ec[rt] = make(encodedDocumentSet, 0, 2)
}
if ec[rt] == nil || len(ec[rt]) == 0 {
e := &encodedDocument{
doc: &Document{},
ResourceType: rt,
}
ec[rt] = append(ec[rt], e)
return e
}
return (ec[rt])[0]
}
// all returns all of the encodedDocument structs stored in the state.
//
// Use this when encoding everything.
// You can use specific state fields when encoding specific resources only.
func (ec encoderState) all() encodedDocumentSet {
dd := make(encodedDocumentSet, 0, 100)
for _, ss := range ec {
dd = append(dd, ss...)
}
return dd
}
// Little helper to encode the raw Document into a io.Reader
func (d *encodedDocument) encode() error {
b := bytes.Buffer{}
err := yaml.NewEncoder(&b).Encode(d.doc)
if err != nil {
return err
}
d.Source = &b
return nil
}

View File

@@ -0,0 +1,19 @@
package yaml
import (
"fmt"
"os"
"gopkg.in/yaml.v3"
)
func parseDocument(name string) (*Document, error) {
doc := &Document{}
f, err := os.Open(fmt.Sprintf("testdata/%s.yaml", name))
if err != nil {
return nil, err
}
defer f.Close()
return doc, yaml.NewDecoder(f).Decode(doc)
}

View File

@@ -0,0 +1,113 @@
package yaml
import (
"strconv"
"strings"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
rbacRule struct {
res *rbac.Rule
// The resource in question
refRbacResource string
refRbacRes *resource.Ref
// The path items
refPathRes []*resource.Ref
// The role
refRole *resource.Ref
role *types.Role
}
rbacRuleSet []*rbacRule
)
func rbacRuleFromResource(r *resource.RbacRule, cfg *EncoderConfig) *rbacRule {
rr := r.Res.Resource
rr = strings.Trim(rr, ":")
if !strings.Contains(rr, ":") {
rr = "corteza::" + rr
}
return &rbacRule{
res: r.Res,
refRbacResource: rr,
refRbacRes: r.RefRes,
refPathRes: r.RefPath,
refRole: r.RefRole,
}
}
func (rr rbacRuleSet) groupByAccess() []rbacRuleSet {
rtr := make([]rbacRuleSet, 2)
for _, r := range rr {
if r.res.Access == rbac.Allow {
rtr[0] = append(rtr[0], r)
} else if r.res.Access == rbac.Deny {
rtr[1] = append(rtr[1], r)
}
}
return rtr
}
func (rr rbacRuleSet) groupByRole() []rbacRuleSet {
rolx := make(map[string]rbacRuleSet)
for _, r := range rr {
roleKey := r.getRoleKey()
if _, has := rolx[roleKey]; !has {
rolx[roleKey] = make(rbacRuleSet, 0, 100)
}
rolx[roleKey] = append(rolx[roleKey], r)
}
rtr := make([]rbacRuleSet, 0, len(rolx))
for _, rr := range rolx {
rtr = append(rtr, rr)
}
return rtr
}
func (rr rbacRuleSet) groupByResource() []rbacRuleSet {
rolx := make(map[string]rbacRuleSet)
for _, r := range rr {
k := r.res.Resource
if _, has := rolx[k]; !has {
rolx[k] = make(rbacRuleSet, 0, 100)
}
rolx[k] = append(rolx[k], r)
}
rtr := make([]rbacRuleSet, 0, len(rolx))
for _, rr := range rolx {
rtr = append(rtr, rr)
}
return rtr
}
func (r *rbacRule) getRoleKey() string {
if r.role == nil {
return ""
}
if r.role.Handle != "" {
return r.role.Handle
}
return strconv.FormatUint(r.res.RoleID, 10)
}

View File

@@ -0,0 +1,387 @@
package yaml
import (
"context"
"fmt"
"strconv"
"strings"
automationTypes "github.com/cortezaproject/corteza/server/automation/types"
composeTypes "github.com/cortezaproject/corteza/server/compose/types"
federationTypes "github.com/cortezaproject/corteza/server/federation/types"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
systemTypes "github.com/cortezaproject/corteza/server/system/types"
)
func (n *rbacRule) Prepare(ctx context.Context, state *envoy.ResourceState) (err error) {
rl, ok := state.Res.(*resource.RbacRule)
if !ok {
return encoderErrInvalidResource(resource.RbacResourceType, state.Res.ResourceType())
}
// Get the related role
n.role = resource.FindRole(state.ParentResources, rl.RefRole.Identifiers)
if n.role == nil {
return resource.RoleErrUnresolved(rl.RefRole.Identifiers)
}
refRes := n.refRbacRes
if refRes == nil {
return
}
// For now we will only allow resource specific RBAC rules if that resource is
// also present.
// Here we check if we can find it in case we're handling a resource specific rule.
found := false
switch refRes.ResourceType {
case composeTypes.RecordResourceType:
if found = n.handleComposeRecord(state.ParentResources); !found {
return resource.RbacResourceErrNotFound(refRes.Identifiers)
}
case composeTypes.ModuleFieldResourceType:
if found = n.handleComposeModuleField(state.ParentResources); !found {
return resource.RbacResourceErrNotFound(refRes.Identifiers)
}
default:
for _, r := range state.ParentResources {
if refRes.ResourceType == r.ResourceType() && r.Identifiers().HasAny(refRes.Identifiers) {
return
}
}
// We couldn't find it...
return resource.RbacResourceErrNotFound(refRes.Identifiers)
}
return nil
}
func (r *rbacRule) Encode(ctx context.Context, doc *Document, state *envoy.ResourceState) (err error) {
// @todo Improve RBAC rule 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.makeRBACResource(state)
if err != nil {
return err
}
r.refRbacResource = refResource
r.res.Resource = refResource
doc.addRbacRule(r)
return nil
}
func (r *rbacRule) makeRBACResource(state *envoy.ResourceState) (string, error) {
res := state.Res.(*resource.RbacRule)
p0ID := "*"
p1ID := "*"
p2ID := "*"
rt := strings.Split(r.refRbacResource, "/")[0]
switch rt {
case composeTypes.ComponentResourceType,
systemTypes.ComponentResourceType,
automationTypes.ComponentResourceType,
federationTypes.ComponentResourceType:
return rt, nil
}
switch rt {
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.NamespaceRbacResourceTpl(), composeTypes.NamespaceResourceType, 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.ModuleRbacResourceTpl(), composeTypes.ModuleResourceType, p0ID, p1ID), 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.ChartRbacResourceTpl(), composeTypes.ChartResourceType, 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)
}
p1ID = p1.Handle
}
return fmt.Sprintf(composeTypes.PageRbacResourceTpl(), composeTypes.PageResourceType, p0ID, p1ID), nil
case composeTypes.RecordResourceType:
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 len(res.RefPath) > 1 {
p1 := resource.FindComposeModule(state.ParentResources, res.RefPath[1].Identifiers)
if p1 == nil {
return "", resource.ComposeModuleErrUnresolved(res.RefPath[1].Identifiers)
}
p1ID = p1.Handle
}
if res.RefRes != nil {
ref := res.RefPath[1]
p2 := resource.FindComposeRecordResource(state.ParentResources, ref.Identifiers)
if p2 == nil {
return "", resource.ComposeRecordErrUnresolved(res.RefRes.Identifiers)
}
for _, i := range res.RefRes.Identifiers {
if _, ok := p2.IDMap[i]; ok {
p2ID = res.RefRes.Identifiers.First()
break
}
}
if p2ID == "" {
return "", resource.ComposeRecordErrUnresolved(res.RefRes.Identifiers)
}
}
return fmt.Sprintf(composeTypes.RecordRbacResourceTpl(), composeTypes.RecordResourceType, p0ID, p1ID, p2ID), nil
case composeTypes.ModuleFieldResourceType:
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 len(res.RefPath) > 1 {
p1 := resource.FindComposeModule(state.ParentResources, res.RefPath[1].Identifiers)
if p1 == nil {
return "", resource.ComposeModuleErrUnresolved(res.RefPath[1].Identifiers)
}
p1ID = p1.Handle
}
if r.refRbacRes != nil {
modRef := r.refPathRes[1]
p2 := resource.FindComposeModuleField(state.ParentResources, modRef.Identifiers, r.refRbacRes.Identifiers)
if p2 == nil {
return "", resource.ComposeModuleFieldErrUnresolved(r.refRbacRes.Identifiers)
}
p2ID = p2.Name
}
// @todo specific module field RBAC
return fmt.Sprintf(composeTypes.ModuleFieldRbacResourceTpl(), composeTypes.ModuleFieldResourceType, p0ID, p1ID, p2ID), nil
case systemTypes.UserResourceType:
if res.RefRes != nil {
p1 := resource.FindUser(state.ParentResources, res.RefRes.Identifiers)
if p1 == nil {
return "", resource.UserErrUnresolved(res.RefRes.Identifiers)
}
p1ID = p1.Handle
}
return fmt.Sprintf(systemTypes.UserRbacResourceTpl(), systemTypes.UserResourceType, p1ID), nil
case systemTypes.RoleResourceType:
if res.RefRes != nil {
p1 := resource.FindRole(state.ParentResources, res.RefRes.Identifiers)
if p1 == nil {
return "", resource.RoleErrUnresolved(res.RefRes.Identifiers)
}
p1ID = p1.Handle
}
return fmt.Sprintf(systemTypes.RoleRbacResourceTpl(), systemTypes.RoleResourceType, p1ID), nil
case systemTypes.ApplicationResourceType:
if res.RefRes != nil {
p1 := resource.FindApplication(state.ParentResources, res.RefRes.Identifiers)
if p1 == nil {
return "", resource.ApplicationErrUnresolved(res.RefRes.Identifiers)
}
p1ID = p1.Name
}
return fmt.Sprintf(systemTypes.ApplicationRbacResourceTpl(), systemTypes.ApplicationResourceType, p1ID), nil
case systemTypes.ApigwRouteResourceType:
if res.RefRes != nil {
p1 := resource.FindAPIGateway(state.ParentResources, res.RefRes.Identifiers)
if p1 == nil {
return "", resource.APIGatewayErrUnresolved(res.RefRes.Identifiers)
}
p1ID = strconv.FormatUint(p1.ID, 10)
}
return fmt.Sprintf(systemTypes.ApigwRouteRbacResourceTpl(), systemTypes.ApigwRouteResourceType, p1ID), nil
case systemTypes.TemplateResourceType:
if res.RefRes != nil {
p1 := resource.FindTemplate(state.ParentResources, res.RefRes.Identifiers)
if p1 == nil {
return "", resource.TemplateErrUnresolved(res.RefRes.Identifiers)
}
p1ID = p1.Handle
}
return fmt.Sprintf(systemTypes.TemplateRbacResourceTpl(), systemTypes.TemplateResourceType, p1ID), nil
case systemTypes.ReportResourceType:
if res.RefRes != nil {
p1 := resource.FindReport(state.ParentResources, res.RefRes.Identifiers)
if p1 == nil {
return "", resource.ReportErrUnresolved(res.RefRes.Identifiers)
}
p1ID = p1.Handle
}
return fmt.Sprintf(systemTypes.ReportRbacResourceTpl(), systemTypes.ReportResourceType, p1ID), nil
case federationTypes.NodeResourceType:
return fmt.Sprintf(federationTypes.NodeRbacResourceTpl(), federationTypes.NodeResourceType, p1ID), nil
case federationTypes.SharedModuleResourceType:
return fmt.Sprintf(federationTypes.SharedModuleRbacResourceTpl(), federationTypes.SharedModuleResourceType, p1ID), nil
case federationTypes.ExposedModuleResourceType:
return fmt.Sprintf(federationTypes.ExposedModuleRbacResourceTpl(), federationTypes.ExposedModuleResourceType, p1ID), nil
}
return "", fmt.Errorf("unsupported resource type '%s' for RBAC YAML encode", r.res.Resource)
}
func (n *rbacRule) handleComposeRecord(pp []resource.Interface) bool {
for _, p := range pp {
if p.ResourceType() == composeTypes.RecordResourceType && p.Identifiers().HasAny(n.refPathRes[1].Identifiers) {
return true
}
}
return false
}
func (n *rbacRule) handleComposeModuleField(pp []resource.Interface) bool {
for _, p := range pp {
if p.ResourceType() == composeTypes.ModuleResourceType && p.Identifiers().HasAny(n.refPathRes[1].Identifiers) {
return true
}
}
return false
}
func (rr rbacRuleSet) MarshalYAML() (interface{}, error) {
if rr == nil || len(rr) == 0 {
return nil, nil
}
var err error
accNode, _ := makeMap()
for i, accRules := range rr.groupByAccess() {
roleNode, _ := makeMap()
for _, roleRules := range accRules.groupByRole() {
resNode, _ := makeMap()
for _, resRules := range roleRules.groupByResource() {
opNode, _ := makeSeq()
for _, rule := range resRules {
opNode, err = addSeq(opNode, rule.res.Operation)
if err != nil {
return nil, err
}
}
resNode, err = addMap(resNode,
resRules[0].res.Resource, opNode,
)
if err != nil {
return nil, err
}
}
roleNode, err = addMap(roleNode,
roleRules[0].getRoleKey(), resNode,
)
if err != nil {
return nil, err
}
}
if i == 0 {
accNode, err = addMap(accNode,
"allow", roleNode,
)
} else {
accNode, err = addMap(accNode,
"deny", roleNode,
)
}
if err != nil {
return nil, err
}
}
return accNode, nil
}
func (r *rbacRule) MarshalYAML() (interface{}, error) {
return r.res.Operation, nil
}

View File

@@ -0,0 +1,55 @@
package yaml
import (
"testing"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
func TestRBACRules_UnmarshalYAML(t *testing.T) {
var (
parseString = func(src string) (*composeNamespace, error) {
w := &composeNamespace{}
return w, yaml.Unmarshal([]byte(src), w)
}
)
t.Run("empty", func(t *testing.T) {
req := require.New(t)
w, err := parseString(``)
req.NoError(err)
req.NotNil(w)
req.Nil(w.res)
})
t.Run("compose rbac 1", func(t *testing.T) {
req := require.New(t)
doc, err := parseDocument("compose_rbac_1")
req.NoError(err)
req.NotNil(doc)
req.NotNil(doc.compose)
req.Len(doc.compose.Modules, 1)
req.NotNil(doc.compose.Modules[0].rbac)
req.NotEmpty(doc.compose.Modules[0].rbac)
})
}
func TestRBACRules_MarshalEnvoy(t *testing.T) {
var (
req = require.New(t)
wrap = composeNamespace{res: &types.Namespace{ID: 42}}
nn, err = wrap.MarshalEnvoy()
)
req.NoError(err)
req.Len(nn, 2) // namespace + resource translation
// req.IsType(&node.ComposeNamespace{}, nn[0])
// req.Equal(uint64(42), nn[0].(*node.ComposeNamespace).Res.ID)
}

View File

@@ -0,0 +1,112 @@
package yaml
import (
"fmt"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/cortezaproject/corteza/server/pkg/y7s"
"github.com/cortezaproject/corteza/server/system/types"
"gopkg.in/yaml.v3"
)
func decodeRbac(n *yaml.Node) (rbacRuleSet, error) {
var (
rr = make(rbacRuleSet, 0, 20)
)
return rr, y7s.EachMap(n, func(k, v *yaml.Node) (err error) {
switch k.Value {
case "allow":
rr, err = rr.decodeRbac(rbac.Allow, v)
if err != nil {
return err
}
case "deny":
rr, err = rr.decodeRbac(rbac.Deny, v)
if err != nil {
return err
}
}
return nil
})
}
func (rr rbacRuleSet) decodeRbac(a rbac.Access, rules *yaml.Node) (oo rbacRuleSet, err error) {
if rr == nil {
oo = make(rbacRuleSet, 0, 10)
} else {
oo = rr
}
parseOps := func(ops *yaml.Node, roleRef, res string) error {
return y7s.EachSeq(ops, func(op *yaml.Node) error {
rule := &rbacRule{
res: &rbac.Rule{
Access: a,
Operation: op.Value,
Resource: res,
},
refRole: resource.MakeRef(types.RoleResourceType, resource.MakeIdentifiers(roleRef)),
}
if res != "" {
if err = rule.SetResource(res); err != nil {
return fmt.Errorf("failed to decode RBAC rule for role '%s': %w", roleRef, err)
}
}
oo = append(oo, rule)
return nil
})
}
return oo, y7s.EachMap(rules, func(role, ops *yaml.Node) error {
// If its a mapping node, keys represent resources
if ops.Kind == yaml.MappingNode {
err = y7s.EachMap(ops, func(res, ops *yaml.Node) error {
return parseOps(ops, role.Value, res.Value)
})
} else {
return parseOps(ops, role.Value, "")
}
return nil
})
}
func (rr rbacRuleSet) bindResource(resI resource.Interface) rbacRuleSet {
res, ref, pp := resI.(resource.RBACInterface).RBACParts()
rtr := make(rbacRuleSet, 0, len(rr))
for _, r := range rr {
r.refRbacResource = res
r.refRbacRes = ref
r.refPathRes = pp
rtr = append(rtr, r)
}
return rtr
}
func (rr rbacRuleSet) MarshalEnvoy() ([]resource.Interface, error) {
var nn = make([]resource.Interface, 0, len(rr))
for _, r := range rr {
nn = append(nn, resource.NewRbacRule(r.res, r.refRole, r.refRbacRes, r.refRbacResource, r.refPathRes...))
}
return nn, nil
}
func (r *rbacRule) SetResource(res string) error {
_, ref, pp, err := resource.ParseRule(res)
if err != nil {
return err
}
r.refRbacResource = res
r.refRbacRes = ref
r.refPathRes = pp
return nil
}

View File

@@ -0,0 +1,48 @@
package yaml
import (
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
report struct {
res *types.Report
sources reportSourceSet
blocks reportBlockSet
ts *resource.Timestamps
us *resource.Userstamps
envoyConfig *resource.EnvoyConfig
encoderConfig *EncoderConfig
rbac rbacRuleSet
locale resourceTranslationSet
}
reportSet []*report
reportSource struct {
res *types.ReportDataSource
envoyConfig *resource.EnvoyConfig
encoderConfig *EncoderConfig
}
reportSourceSet []*reportSource
reportBlock struct {
res *types.ReportBlock
envoyConfig *resource.EnvoyConfig
encoderConfig *EncoderConfig
locale resourceTranslationSet
}
reportBlockSet []*reportBlock
)
func (nn reportSet) configureEncoder(cfg *EncoderConfig) {
for _, n := range nn {
n.encoderConfig = cfg
}
}

View File

@@ -0,0 +1,131 @@
package yaml
import (
"context"
automationTypes "github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
)
func reportFromResource(r *resource.Report, cfg *EncoderConfig) *report {
ss := make(reportSourceSet, len(r.Sources))
for i, s := range r.Sources {
ss[i] = &reportSource{
res: s.Res,
encoderConfig: cfg,
}
}
pp := make(reportBlockSet, len(r.Blocks))
for i, p := range r.Blocks {
pp[i] = &reportBlock{
res: p.Res,
encoderConfig: cfg,
}
}
return &report{
res: r.Res,
sources: ss,
blocks: pp,
encoderConfig: cfg,
}
}
func (n *report) Prepare(ctx context.Context, state *envoy.ResourceState) (err error) {
wf, ok := state.Res.(*resource.Report)
if !ok {
return encoderErrInvalidResource(automationTypes.WorkflowResourceType, state.Res.ResourceType())
}
n.res = wf.Res
n.us = wf.Userstamps()
return nil
}
func (n *report) Encode(ctx context.Context, doc *Document, state *envoy.ResourceState) (err error) {
if n.res.ID <= 0 {
n.res.ID = nextID()
}
n.ts, err = resource.MakeTimestampsCUDA(&n.res.CreatedAt, n.res.UpdatedAt, n.res.DeletedAt, nil).
Model(n.encoderConfig.TimeLayout, n.encoderConfig.Timezone)
if err != nil {
return err
}
n.us, err = resolveUserstamps(state.ParentResources, n.us)
if err != nil {
return err
}
// @todo skip eval?
doc.addReport(n)
return err
}
func (wf *report) MarshalYAML() (interface{}, error) {
var err error
nn, err := makeMap(
"handle", wf.res.Handle,
"meta", wf.res.Meta,
"sources", wf.sources,
"blocks", wf.blocks,
)
if err != nil {
return nil, err
}
nn, err = encodeTimestamps(nn, wf.ts)
if err != nil {
return nil, err
}
nn, err = encodeUserstamps(nn, wf.us)
if err != nil {
return nil, err
}
return nn, nil
}
func (t *reportSource) MarshalYAML() (interface{}, error) {
var err error
nn, err := makeMap(
"meta", t.res.Meta,
"step", t.res.Step,
)
if err != nil {
return nil, err
}
return nn, nil
}
func (t *reportBlock) MarshalYAML() (interface{}, error) {
var err error
nn, err := makeMap(
"title", t.res.Title,
"description", t.res.Description,
"key", t.res.Key,
"kind", t.res.Kind,
"options", t.res.Options,
"elements", t.res.Elements,
"sources", t.res.Sources,
"xywh", t.res.XYWH,
"layout", t.res.Layout,
)
if err != nil {
return nil, err
}
return nn, nil
}

View File

@@ -0,0 +1,204 @@
package yaml
import (
"strings"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/pkg/y7s"
"github.com/cortezaproject/corteza/server/system/types"
"gopkg.in/yaml.v3"
)
func (wset *reportSet) UnmarshalYAML(n *yaml.Node) error {
return y7s.Each(n, func(k, v *yaml.Node) (err error) {
var (
wrap = &report{}
)
if v == nil {
return y7s.NodeErr(n, "malformed report definition")
}
if err = v.Decode(&wrap); err != nil {
return
}
if err = decodeRef(k, "report handle", &wrap.res.Handle); err != nil {
return y7s.NodeErr(n, "Report reference must be a valid handle")
}
if wrap.res.Meta == nil {
wrap.res.Meta = &types.ReportMeta{}
}
if wrap.res.Meta.Name == "" {
// if name is not set, use handle
wrap.res.Meta.Name = wrap.res.Handle
}
*wset = append(*wset, wrap)
return
})
}
func (wrap *report) UnmarshalYAML(n *yaml.Node) (err error) {
if wrap.res == nil {
wrap.rbac = make(rbacRuleSet, 0, 10)
wrap.res = &types.Report{}
}
if wrap.rbac, err = decodeRbac(n); err != nil {
return
}
if wrap.locale, err = decodeLocale(n); err != nil {
return
}
if wrap.envoyConfig, err = decodeEnvoyConfig(n); err != nil {
return
}
if wrap.ts, err = decodeTimestamps(n); err != nil {
return
}
if wrap.us, err = decodeUserstamps(n); err != nil {
return
}
return y7s.EachMap(n, func(k, v *yaml.Node) (err error) {
switch strings.ToLower(k.Value) {
case "handle":
return y7s.DecodeScalar(v, "report handle", &wrap.res.Handle)
case "meta":
return v.Decode(&wrap.res.Meta)
case "sources":
wrap.sources = make(reportSourceSet, 0, 10)
err = v.Decode(&wrap.sources)
if err != nil {
return err
}
case "blocks":
wrap.blocks = make(reportBlockSet, 0, 10)
err = v.Decode(&wrap.blocks)
if err != nil {
return err
}
}
return nil
})
}
func (wrap *reportSource) UnmarshalYAML(n *yaml.Node) (err error) {
if wrap.res == nil {
wrap.res = &types.ReportDataSource{}
}
if wrap.envoyConfig, err = decodeEnvoyConfig(n); err != nil {
return
}
return y7s.EachMap(n, func(k, v *yaml.Node) (err error) {
switch k.Value {
case "meta":
return v.Decode(&wrap.res.Meta)
case "step":
return v.Decode(&wrap.res.Step)
}
return nil
})
}
func (wrap *reportBlock) UnmarshalYAML(n *yaml.Node) (err error) {
if wrap.res == nil {
wrap.res = &types.ReportBlock{}
}
if wrap.envoyConfig, err = decodeEnvoyConfig(n); err != nil {
return
}
if wrap.locale, err = decodeLocale(n); err != nil {
return
}
return y7s.EachMap(n, func(k, v *yaml.Node) (err error) {
switch strings.ToLower(k.Value) {
case "title":
return y7s.DecodeScalar(v, "title", &wrap.res.Title)
case "description":
return y7s.DecodeScalar(v, "description", &wrap.res.Description)
case "key":
return y7s.DecodeScalar(v, "key", &wrap.res.Key)
case "kind":
return y7s.DecodeScalar(v, "kind", &wrap.res.Kind)
case "options":
return v.Decode(&wrap.res.Options)
case "elements":
return v.Decode(&wrap.res.Elements)
case "sources":
return v.Decode(&wrap.res.Sources)
case "xywh":
return v.Decode(&wrap.res.XYWH)
case "layout":
return y7s.DecodeScalar(v, "layout", &wrap.res.Layout)
}
return nil
})
}
func (wset reportSet) MarshalEnvoy() ([]resource.Interface, error) {
nn := make([]resource.Interface, 0, len(wset)*2)
for _, res := range wset {
if tmp, err := res.MarshalEnvoy(); err != nil {
return nil, err
} else {
nn = append(nn, tmp...)
}
}
return nn, nil
}
func (wrap report) MarshalEnvoy() ([]resource.Interface, error) {
rs := resource.NewReport(wrap.res)
rs.SetTimestamps(wrap.ts)
rs.SetUserstamps(wrap.us)
rs.SetConfig(wrap.envoyConfig)
// default report translations
// includes translations for nested resources also
// var defaultReportTranslations []resource.Interface
// dft, err := rs.EncodeTranslations()
// if err != nil {
// return nil, err
// }
// for _, d := range dft {
// d.MarkDefault()
// defaultReportTranslations = append(defaultReportTranslations, d)
// }
for _, s := range wrap.sources {
rs.AddReportSource(s.res)
}
for _, p := range wrap.blocks {
rs.AddReportBlock(p.res)
}
return envoy.CollectNodes(
rs,
// defaultReportTranslations,
wrap.rbac.bindResource(rs),
)
}

View File

@@ -0,0 +1,69 @@
package yaml
import (
"strings"
"github.com/cortezaproject/corteza/server/pkg/y7s"
. "github.com/cortezaproject/corteza/server/pkg/y7s"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"gopkg.in/yaml.v3"
)
func decodeEnvoyConfig(n *yaml.Node) (*resource.EnvoyConfig, error) {
ec := &resource.EnvoyConfig{}
var ecNode *yaml.Node
for i, k := range n.Content {
if k.Value == "(envoy)" {
ecNode = n.Content[i+1]
break
}
}
if ecNode == nil {
return nil, nil
}
return ec, y7s.EachMap(ecNode, func(k, v *yaml.Node) (err error) {
switch k.Value {
case "skipIf", "skip":
return y7s.DecodeScalar(v, "decode skip if", &ec.SkipIf)
case "onExisting", "mergeAlg":
return decodeMergeAlg(v, "decode merge alg", &ec.OnExisting)
}
return nil
})
}
func decodeMergeAlg(n *yaml.Node, refType string, val *resource.MergeAlg) error {
if n == nil {
return nil
}
if !IsKind(n, yaml.ScalarNode) {
return y7s.NodeErr(n, "%s reference must be scalar", refType)
}
switch strings.ToLower(n.Value) {
case "skip",
"s":
*val = resource.Skip
case "replace",
"r":
*val = resource.Replace
case "mergeleft",
"left",
"ml":
*val = resource.MergeLeft
case "mergeright",
"right",
"mr":
*val = resource.MergeRight
default:
return y7s.NodeErr(n, "%s unknown algorithm", refType)
}
return nil
}

View File

@@ -0,0 +1,57 @@
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 {
if len(r.Res) == 0 {
return nil
}
return &resourceTranslation{
locales: r.Res,
refResourceTranslation: r.Res[0].Resource,
refLocaleRes: r.RefRes,
refPathRes: r.RefPath,
}
}

View File

@@ -0,0 +1,420 @@
package yaml
import (
"context"
"fmt"
"sort"
"strings"
composeTypes "github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/spf13/cast"
"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.
// @todo move out of the encoding logic
if r == nil {
return
}
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)
}
if p1.Slug == "" {
p1ID = cast.ToString(p1.ID)
} else {
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)
}
if p0.Slug == "" {
p0ID = cast.ToString(p0.ID)
} else {
p0ID = p0.Slug
}
}
if res.RefRes != nil {
p1 := resource.FindComposeModule(state.ParentResources, res.RefRes.Identifiers)
if p1 == nil {
return "", resource.ComposeModuleErrUnresolved(res.RefRes.Identifiers)
}
if p1.Handle == "" {
p1ID = cast.ToString(p1.ID)
} else {
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)
}
if p0.Slug == "" {
p0ID = cast.ToString(p0.ID)
} else {
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 = cast.ToString(p1.ID)
} 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)
}
if p0.Slug == "" {
p0ID = cast.ToString(p0.ID)
} else {
p0ID = p0.Slug
}
p1 := resource.FindComposeModule(state.ParentResources, res.RefPath[1].Identifiers)
if p1 == nil {
return "", resource.ComposeModuleErrUnresolved(res.RefPath[1].Identifiers)
}
if p1.Handle == "" {
p1ID = cast.ToString(p1.ID)
} else {
p1ID = p1.Handle
}
// field
f := resource.FindComposeModuleField(state.ParentResources, res.RefPath[1].Identifiers, 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)
}
if p0.Slug == "" {
p0ID = cast.ToString(p0.ID)
} else {
p0ID = p0.Slug
}
}
if res.RefRes != nil {
p1 := resource.FindComposeChart(state.ParentResources, res.RefRes.Identifiers)
if p1 == nil {
return "", resource.ComposeChartErrUnresolved(res.RefRes.Identifiers)
}
if p1.Handle == "" {
p1ID = cast.ToString(p1.ID)
} else {
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
// case systemTypes.ReportResourceType:
// if res.RefRes != nil {
// p0 := resource.FindReport(state.ParentResources, res.RefRes.Identifiers)
// if p0 == nil {
// return "", resource.ReportErrUnresolved(res.RefRes.Identifiers)
// }
// p0ID = p0.Handle
// }
// return fmt.Sprintf(systemTypes.ReportResourceTranslationTpl(), systemTypes.ReportResourceTranslationType, 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,119 @@
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
}

View File

@@ -0,0 +1,26 @@
package yaml
import (
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
role struct {
res *types.Role
ts *resource.Timestamps
envoyConfig *resource.EnvoyConfig
encoderConfig *EncoderConfig
// module's RBAC rules
rbac rbacRuleSet
}
roleSet []*role
)
func (nn roleSet) configureEncoder(cfg *EncoderConfig) {
for _, n := range nn {
n.encoderConfig = cfg
}
}

View File

@@ -0,0 +1,64 @@
package yaml
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/system/types"
)
func roleFromResource(r *resource.Role, cfg *EncoderConfig) *role {
return &role{
res: r.Res,
encoderConfig: cfg,
}
}
func (r *role) Prepare(ctx context.Context, state *envoy.ResourceState) (err error) {
rl, ok := state.Res.(*resource.Role)
if !ok {
return encoderErrInvalidResource(types.RoleResourceType, state.Res.ResourceType())
}
r.res = rl.Res
return nil
}
func (r *role) Encode(ctx context.Context, doc *Document, state *envoy.ResourceState) (err error) {
if r.res.ID <= 0 {
r.res.ID = nextID()
}
r.ts, err = resource.MakeTimestampsCUDA(&r.res.CreatedAt, r.res.UpdatedAt, r.res.DeletedAt, r.res.ArchivedAt).
Model(r.encoderConfig.TimeLayout, r.encoderConfig.Timezone)
if err != nil {
return err
}
// @todo skip eval?
doc.addRole(r)
return err
}
func (c *role) MarshalYAML() (interface{}, error) {
var err error
nsn, err := makeMap(
"name", c.res.Name,
"handle", c.res.Handle,
"labels", c.res.Labels,
)
if err != nil {
return nil, err
}
nsn, err = encodeTimestamps(nsn, c.ts)
if err != nil {
return nil, err
}
return nsn, nil
}

View File

@@ -0,0 +1,48 @@
package yaml
import (
"testing"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
func TestRole_UnmarshalYAML(t *testing.T) {
var (
parseString = func(src string) (*role, error) {
w := &role{}
return w, yaml.Unmarshal([]byte(src), w)
}
)
t.Run("empty", func(t *testing.T) {
req := require.New(t)
w, err := parseString(``)
req.NoError(err)
req.NotNil(w)
req.Nil(w.res)
})
t.Run("simple name", func(t *testing.T) {
req := require.New(t)
w, err := parseString(`{ name: Test }`)
req.NoError(err)
req.NotNil(w)
req.NotNil(w.res)
req.NotEmpty(w.res.Name)
})
t.Run("role 1", func(t *testing.T) {
req := require.New(t)
doc, err := parseDocument("role_1")
req.NoError(err)
req.NotNil(doc)
req.Len(doc.roles, 2)
req.NotNil(doc.roles[1])
req.Equal("Role2", doc.roles[1].res.Name)
req.Equal("r2", doc.roles[1].res.Handle)
})
}

View File

@@ -0,0 +1,98 @@
package yaml
import (
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/pkg/y7s"
"github.com/cortezaproject/corteza/server/system/types"
"gopkg.in/yaml.v3"
)
func (wset *roleSet) UnmarshalYAML(n *yaml.Node) error {
return y7s.Each(n, func(k, v *yaml.Node) (err error) {
var (
wrap = &role{}
)
if v == nil {
return y7s.NodeErr(n, "malformed role definition")
}
wrap.res = &types.Role{
// no special defaults
}
switch v.Kind {
case yaml.ScalarNode:
if err = y7s.DecodeScalar(v, "role name", &wrap.res.Name); err != nil {
return
}
case yaml.MappingNode:
if err = v.Decode(&wrap); err != nil {
return
}
}
if err = decodeRef(k, "role", &wrap.res.Handle); err != nil {
return err
}
*wset = append(*wset, wrap)
return
})
}
func (wrap *role) UnmarshalYAML(n *yaml.Node) (err error) {
if !y7s.IsKind(n, yaml.MappingNode) {
return y7s.NodeErr(n, "role definition must be a map")
}
if wrap.res == nil {
wrap.res = &types.Role{}
}
if err = n.Decode(&wrap.res); err != nil {
return
}
if wrap.rbac, err = decodeRbac(n); err != nil {
return
}
if wrap.envoyConfig, err = decodeEnvoyConfig(n); err != nil {
return
}
if wrap.ts, err = decodeTimestamps(n); err != nil {
return
}
return nil
}
func (wset roleSet) MarshalEnvoy() ([]resource.Interface, error) {
nn := make([]resource.Interface, 0, len(wset))
for _, res := range wset {
if tmp, err := res.MarshalEnvoy(); err != nil {
return nil, err
} else {
nn = append(nn, tmp...)
}
}
return nn, nil
}
func (wrap role) MarshalEnvoy() ([]resource.Interface, error) {
rs := resource.NewRole(wrap.res)
rs.SetTimestamps(wrap.ts)
rs.SetConfig(wrap.envoyConfig)
return envoy.CollectNodes(
rs,
wrap.rbac.bindResource(rs),
)
}

View File

@@ -0,0 +1,19 @@
package yaml
import (
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
setting struct {
res *types.SettingValue
ts *resource.Timestamps
us *resource.Userstamps
envoyConfig *resource.EnvoyConfig
encoderConfig *EncoderConfig
}
settingSet []*setting
)

View File

@@ -0,0 +1,73 @@
package yaml
import (
"context"
"encoding/json"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
)
func settingFromResource(r *resource.Setting, cfg *EncoderConfig) *setting {
return &setting{
res: &r.Res,
encoderConfig: cfg,
}
}
func (n *setting) Prepare(ctx context.Context, state *envoy.ResourceState) (err error) {
st, ok := state.Res.(*resource.Setting)
if !ok {
return encoderErrInvalidResource(resource.SettingsResourceType, state.Res.ResourceType())
}
n.res = &st.Res
n.us = st.Userstamps()
return nil
}
func (n *setting) Encode(ctx context.Context, doc *Document, state *envoy.ResourceState) (err error) {
n.ts, err = resource.MakeTimestampsCUDA(nil, &n.res.UpdatedAt, nil, nil).
Model(n.encoderConfig.TimeLayout, n.encoderConfig.Timezone)
if err != nil {
return err
}
n.us, err = resolveUserstamps(state.ParentResources, n.us)
if err != nil {
return err
}
// @todo skip eval?
doc.addSetting(n)
return nil
}
func (c *setting) MarshalYAML() (interface{}, error) {
var aux interface{}
err := json.Unmarshal(c.res.Value, &aux)
if err != nil {
return nil, err
}
nn, err := makeMap(
"name", c.res.Name,
"value", aux,
)
if err != nil {
return nil, err
}
nn, err = encodeTimestamps(nn, c.ts)
if err != nil {
return nil, err
}
nn, err = encodeUserstamps(nn, c.us)
if err != nil {
return nil, err
}
return nn, nil
}

View File

@@ -0,0 +1,20 @@
package yaml
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestSettings_UnmarshalYAML(t *testing.T) {
t.Run("settings 1", func(t *testing.T) {
req := require.New(t)
doc, err := parseDocument("settings_1")
req.NoError(err)
req.NotNil(doc)
req.Len(doc.settings, 11)
// req.Contains(doc.settings, "privacy.mask.email")
// req.Equal(true, doc.settings["privacy.mask.email"])
})
}

View File

@@ -0,0 +1,167 @@
package yaml
import (
"encoding/json"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/pkg/y7s"
"github.com/cortezaproject/corteza/server/system/types"
sqlt "github.com/jmoiron/sqlx/types"
"gopkg.in/yaml.v3"
)
func (wset *settingSet) UnmarshalYAML(n *yaml.Node) error {
return y7s.Each(n, func(k, v *yaml.Node) (err error) {
var (
wrap = &setting{}
)
if v == nil {
return y7s.NodeErr(n, "malformed setting definition")
}
wrap.res = &types.SettingValue{}
switch v.Kind {
case yaml.MappingNode:
if err = v.Decode(&wrap); err != nil {
return
}
default:
if err = y7s.DecodeScalar(k, "setting name", &wrap.res.Name); err != nil {
return err
}
if y7s.IsKind(v, yaml.SequenceNode) {
aux := make([]interface{}, 0, 10)
y7s.EachSeq(v, func(n *yaml.Node) error {
var vx interface{}
err := y7s.DecodeScalar(n, "setting value", &vx)
if err != nil {
return err
}
aux = append(aux, vx)
return nil
})
m, err := json.Marshal(aux)
if err != nil {
return err
}
wrap.res.Value = sqlt.JSONText(m)
} else if y7s.IsKind(v, yaml.MappingNode) {
aux := make(map[string]interface{})
y7s.EachMap(v, func(k, v *yaml.Node) error {
var vx interface{}
err := y7s.DecodeScalar(v, "setting value", &vx)
if err != nil {
return err
}
aux[k.Value] = vx
return nil
})
m, err := json.Marshal(aux)
if err != nil {
return err
}
wrap.res.Value = sqlt.JSONText(m)
} else {
var aux interface{}
err = y7s.DecodeScalar(v, "setting value", &aux)
if err != nil {
return err
}
m, err := json.Marshal(aux)
if err != nil {
return err
}
wrap.res.Value = sqlt.JSONText(m)
}
}
*wset = append(*wset, wrap)
return
})
}
func (wrap *setting) UnmarshalYAML(n *yaml.Node) (err error) {
if !y7s.IsKind(n, yaml.MappingNode) {
return y7s.NodeErr(n, "setting definition must be a map")
}
if wrap.res == nil {
wrap.res = &types.SettingValue{}
}
err = y7s.EachMap(n, func(k, v *yaml.Node) (err error) {
switch k.Value {
case "name":
return y7s.DecodeScalar(v, "setting name", &wrap.res.Name)
case "value":
var aux interface{}
err = y7s.DecodeScalar(v, "setting value", &aux)
if err != nil {
return err
}
m, err := json.Marshal(aux)
if err != nil {
return err
}
wrap.res.Value = sqlt.JSONText(m)
}
return nil
})
if err != nil {
return err
}
if wrap.envoyConfig, err = decodeEnvoyConfig(n); err != nil {
return
}
if wrap.ts, err = decodeTimestamps(n); err != nil {
return
}
if wrap.us, err = decodeUserstamps(n); err != nil {
return
}
return nil
}
func (wset settingSet) MarshalEnvoy() ([]resource.Interface, error) {
nn := make([]resource.Interface, 0, len(wset))
for _, res := range wset {
if tmp, err := res.MarshalEnvoy(); err != nil {
return nil, err
} else {
nn = append(nn, tmp...)
}
}
return nn, nil
}
func (wrap *setting) MarshalEnvoy() ([]resource.Interface, error) {
rs := resource.NewSetting(wrap.res)
rs.SetTimestamps(wrap.ts)
rs.SetUserstamps(wrap.us)
rs.SetConfig(wrap.envoyConfig)
return envoy.CollectNodes(
rs,
)
}

View File

@@ -0,0 +1,109 @@
package yaml
import (
"strings"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/pkg/y7s"
"gopkg.in/yaml.v3"
)
func decodeTimestamps(n *yaml.Node) (*resource.Timestamps, error) {
var (
st = &resource.Timestamps{}
)
// Little helper to make timestamps
f := func(v *yaml.Node) (*resource.Timestamp, error) {
aux := ""
err := y7s.DecodeScalar(v, "decode "+v.Value, &aux)
if err != nil {
return nil, err
}
return resource.MakeTimestamp(aux), nil
}
return st, y7s.EachMap(n, func(k, v *yaml.Node) (err error) {
switch strings.ToLower(k.Value) {
case "createdat":
st.CreatedAt, err = f(v)
case "updatedat":
st.UpdatedAt, err = f(v)
case "deletedat":
st.DeletedAt, err = f(v)
case "suspendedat":
st.SuspendedAt, err = f(v)
case "archivedat":
st.ArchivedAt, err = f(v)
}
return err
})
}
func decodeUserstamps(n *yaml.Node) (*resource.Userstamps, error) {
var (
us = &resource.Userstamps{}
)
// Little helper to make userstamps
f := func(v *yaml.Node) (*resource.Userstamp, error) {
aux := ""
err := y7s.DecodeScalar(v, "decode "+v.Value, &aux)
if err != nil {
return nil, err
}
return resource.MakeUserstampFromRef(aux), nil
}
return us, y7s.EachMap(n, func(k, v *yaml.Node) (err error) {
switch strings.ToLower(k.Value) {
case "createdby",
"creatorid",
"creator":
us.CreatedBy, err = f(v)
case "updatedby":
us.UpdatedBy, err = f(v)
case "deletedby":
us.DeletedBy, err = f(v)
case "ownedby",
"ownerid",
"owner":
us.OwnedBy, err = f(v)
case "runas",
"runasid",
"runner":
us.RunAs, err = f(v)
}
return err
})
}
// mapTimestamps helper encodes Timestamps into the mapping node
func encodeTimestamps(n *yaml.Node, ts *resource.Timestamps) (*yaml.Node, error) {
if ts == nil {
return n, nil
}
return addMap(n,
"createdAt", ts.CreatedAt,
"updatedAt", ts.UpdatedAt,
"deletedAt", ts.DeletedAt,
"archivedAt", ts.ArchivedAt,
"suspendedAt", ts.SuspendedAt,
)
}
// mapUserstamps helper encodes Userstamps into the mapping node
func encodeUserstamps(n *yaml.Node, us *resource.Userstamps) (*yaml.Node, error) {
if us == nil {
return n, nil
}
return addMap(n,
"createdBy", us.CreatedBy,
"updatedBy", us.UpdatedBy,
"deletedBy", us.DeletedBy,
"ownedBy", us.OwnedBy,
"runAs", us.RunAs,
)
}

View File

@@ -0,0 +1,26 @@
package yaml
import (
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
template struct {
res *types.Template
ts *resource.Timestamps
us *resource.Userstamps
envoyConfig *resource.EnvoyConfig
encoderConfig *EncoderConfig
rbac rbacRuleSet
}
templateSet []*template
)
func (nn templateSet) configureEncoder(cfg *EncoderConfig) {
for _, n := range nn {
n.encoderConfig = cfg
}
}

View File

@@ -0,0 +1,86 @@
package yaml
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/system/types"
)
func templateFromResource(r *resource.Template, cfg *EncoderConfig) *template {
return &template{
res: r.Res,
encoderConfig: cfg,
}
}
func (t *template) Prepare(ctx context.Context, state *envoy.ResourceState) (err error) {
us, ok := state.Res.(*resource.Template)
if !ok {
return encoderErrInvalidResource(types.TemplateResourceType, state.Res.ResourceType())
}
t.res = us.Res
return nil
}
func (t *template) Encode(ctx context.Context, doc *Document, state *envoy.ResourceState) (err error) {
if t.res.ID <= 0 {
t.res.ID = nextID()
}
t.ts, err = resource.MakeTimestampsCUDAS(&t.res.CreatedAt, t.res.UpdatedAt, t.res.DeletedAt, nil, nil).
Model(t.encoderConfig.TimeLayout, t.encoderConfig.Timezone)
if err != nil {
return err
}
t.us, err = resolveUserstamps(state.ParentResources, t.us)
if err != nil {
return err
}
// @todo implement resource skipping?
doc.addTemplate(t)
return
}
func (c *template) MarshalYAML() (interface{}, error) {
var err error
meta, err := makeMap(
"short", c.res.Meta.Short,
"description", c.res.Meta.Description,
)
if err != nil {
return nil, err
}
nsn, err := makeMap(
"handle", c.res.Handle,
"language", c.res.Language,
"type", c.res.Type,
"partial", c.res.Partial,
"template", c.res.Template,
"meta", meta,
"Meta", c.res.Labels,
)
if err != nil {
return nil, err
}
nsn, err = encodeTimestamps(nsn, c.ts)
if err != nil {
return nil, err
}
nsn, err = encodeUserstamps(nsn, c.us)
if err != nil {
return nil, err
}
return nsn, nil
}

View File

@@ -0,0 +1,95 @@
package yaml
import (
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/pkg/y7s"
"github.com/cortezaproject/corteza/server/system/types"
"gopkg.in/yaml.v3"
)
func (wset *templateSet) UnmarshalYAML(n *yaml.Node) error {
return y7s.Each(n, func(k, v *yaml.Node) (err error) {
var (
wrap = &template{}
)
if v == nil {
return y7s.NodeErr(n, "malformed template definition")
}
wrap.res = &types.Template{}
switch v.Kind {
case yaml.MappingNode:
if err = v.Decode(&wrap); err != nil {
return
}
default:
return y7s.NodeErr(n, "expecting scalar or map with template definitions")
}
if err = decodeRef(k, "template handle", &wrap.res.Handle); err != nil {
return err
}
*wset = append(*wset, wrap)
return
})
}
func (wrap *template) UnmarshalYAML(n *yaml.Node) (err error) {
if !y7s.IsKind(n, yaml.MappingNode) {
return y7s.NodeErr(n, "template definition must be a map")
}
if wrap.res == nil {
wrap.res = &types.Template{}
}
if err = n.Decode(&wrap.res); err != nil {
return
}
if wrap.rbac, err = decodeRbac(n); err != nil {
return
}
if wrap.envoyConfig, err = decodeEnvoyConfig(n); err != nil {
return
}
if wrap.ts, err = decodeTimestamps(n); err != nil {
return
}
return nil
}
func (wset templateSet) MarshalEnvoy() ([]resource.Interface, error) {
nn := make([]resource.Interface, 0, len(wset))
for _, res := range wset {
if tmp, err := res.MarshalEnvoy(); err != nil {
return nil, err
} else {
nn = append(nn, tmp...)
}
}
return nn, nil
}
func (wrap template) MarshalEnvoy() ([]resource.Interface, error) {
rs := resource.NewTemplate(wrap.res)
rs.SetTimestamps(wrap.ts)
rs.SetConfig(wrap.envoyConfig)
return envoy.CollectNodes(
rs,
wrap.rbac.bindResource(rs),
)
}

View File

@@ -0,0 +1,3 @@
applications:
- one
- name: two

View File

@@ -0,0 +1,48 @@
namespace: crm
charts:
AccountsByRating:
name: Accounts by rating
config:
reports:
- dimensions:
- conditions: {}
field: Rating
meta: {}
modifier: (no grouping / buckets)
skipMissing: true
filter: ""
metrics:
- aggregate: AVG
field: count
fill: true
fixTooltips: false
label: Number of Accounts
lineTension: 0.4
type: line
module: Account
renderer: {}
yAxis:
label: Accounts
colorScheme: tableau.Tableau10
SalesYTD:
name: Sales YTD
config:
reports:
- dimensions:
- conditions: {}
field: LeadSource
meta:
steps:
- value: "150000"
- value: "500000"
- value: "1000000"
modifier: (no grouping / buckets)
filter: YEAR(created_at) = YEAR(NOW()) AND Amount IS NOT NULL
metrics:
- aggregate: SUM
field: Amount
type: gauge
module: Opportunity
renderer: {}
colorScheme: tableau.Traffic9
allow: { somerole: [ read ] }

View File

@@ -0,0 +1,9 @@
namespace: crm
charts:
ChartHandle:
allow:
authenticated:
- read
deny:
authenticated:
- delete

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,17 @@
namespace: crm
modules:
Account:
name: Expressions
fields:
full:
expressions:
valueExpr: "a > b"
sanitizers: [ "trim(value)" ]
validators:
- test: 'a == ""'
error: Value should not be empty
disableDefaultValidators: true
validatorKV:
expressions:
validators:
'value == ""': Value should be filled

View File

@@ -0,0 +1,10 @@
namespace: crm
modules:
Account:
name: Account
allow:
authenticated:
- read
deny:
authenticated:
- delete

View File

@@ -0,0 +1,11 @@
namespace: ns1
modules:
mod1:
records:
source: mod1.csv
key: id
mapping:
- f1
-
- /
- f2

View File

@@ -0,0 +1,7 @@
namespace: ns1
modules:
mod1:
records:
key:
- f1
- f2

View File

@@ -0,0 +1,9 @@
namespace: ns1
modules:
mod1:
records:
mapping:
c1:
field: f1
c2:
field: f2

View File

@@ -0,0 +1,10 @@
namespaces:
crm:
name: CRM
allow:
authenticated:
- module.create
- read
deny:
authenticated:
- delete

View File

@@ -0,0 +1,206 @@
namespace: crm
pages:
- handle: Leads
title: Leads
allow: { somerole: [ read ] }
blocks:
- title: Leads by type
options:
chart: LeadsByType
style:
variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}
kind: Chart
xywh: [9, 0, 3, 8]
- title: New Leads Per Month
options:
chart: LeadsPerMonth
style:
variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}
kind: Chart
xywh: [9, 8, 3, 8]
- title: Leads
options:
allowExport: true
draggable: false
editFields: []
editable: false
fields:
- name: FirstName
- name: LastName
- name: Email
- name: Company
- name: Rating
- name: createdAt
hideRecordCloneButton: true
hideRecordEditButton: false
hideRecordReminderButton: true
hideRecordViewButton: true
module: Lead
perPage: 20
prefilter: ""
presort: createdAt DESC
selectMode: multi
selectable: true
selectionButtons: []
style:
variants: {border: primary, headerText: primary}
kind: RecordList
xywh: [0, 0, 9, 16]
children:
- handle: RecordPageForModuleLead
module: Lead
title: Record page for module "Lead"
blocks:
- title: Personal Data
options:
fields:
- name: FirstName
- name: LastName
- name: Salutation
- name: Title
- name: Phone
- name: MobilePhone
- name: Email
- name: Website
- name: Twitter
- name: LinkedIn
- name: Facebook
style:
variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}
kind: Record
xywh: [0, 0, 3, 15]
- title: Twitter feed
options:
profileSourceField: Twitter
profileUrl: ""
style:
variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}
kind: SocialFeed
xywh: [9, 0, 3, 31]
- title: Company Data
options:
fields:
- name: Company
- name: Industry
- name: AnnualRevenue
- name: NumberOfEmployees
- name: NumberOfEmployeesRange
- name: Country
- name: City
- name: Street
- name: State
- name: PostalCode
style:
variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}
kind: Record
xywh: [3, 0, 3, 15]
- title: Other Information
options:
fields:
- name: Rating
- name: CampaignId
- name: Status
- name: LeadSource
- name: OwnerId
- name: Description
- name: ConvertedAccountId
- name: ConvertedContactId
- name: ConvertedOpportunityId
- name: ConvertedDate
style:
variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}
kind: Record
xywh: [6, 0, 3, 15]
- title: Notes
options:
allowExport: true
draggable: false
editFields: []
editable: false
fields:
- name: Title
- name: Body
- name: createdAt
hideRecordCloneButton: true
hideRecordEditButton: false
hideRecordReminderButton: true
hideRecordViewButton: true
module: Note
perPage: 20
prefilter: LeadId = ${recordID}
presort: createdAt DESC
selectMode: multi
selectable: true
selectionButtons: []
style:
variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}
kind: RecordList
xywh: [0, 15, 6, 8]
- title: Events
options:
allowExport: true
draggable: false
editFields: []
editable: false
fields:
- name: Type
- name: Subject
- name: ActivityDate
hideRecordCloneButton: true
hideRecordEditButton: false
hideRecordReminderButton: true
hideRecordViewButton: true
module: Event
perPage: 20
prefilter: LeadId = ${recordID}
presort: ActivityDate DESC
selectMode: multi
selectable: true
selectionButtons: []
style:
variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}
kind: RecordList
xywh: [0, 23, 4, 8]
- title: Tasks
options:
allowExport: true
draggable: false
editFields: []
editable: false
fields:
- name: Type
- name: Subject
- name: Status
- name: ActivityDate
hideRecordCloneButton: true
hideRecordEditButton: false
hideRecordReminderButton: true
hideRecordViewButton: true
module: Task
perPage: 20
prefilter: LeadId = ${recordID}
presort: ActivityDate DESC
selectMode: multi
selectable: true
selectionButtons: []
style:
variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}
kind: RecordList
xywh: [4, 23, 5, 8]
- title: Lead Actions
options:
buttons:
- enabled: true
label: Convert this Lead into an Account
resourceType: compose:record
script: /client-scripts/compose/crm/Lead/ToAccount.js:default
- enabled: true
label: Convert this Lead into an Account and Opportunity
resourceType: compose:record
script: /client-scripts/compose/crm/Lead/ToAccountAndOpportunity.js:default
sealed: false
style:
variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}
kind: Automation
xywh: [6, 15, 3, 8]
visible: true

View File

@@ -0,0 +1,10 @@
modules:
mod1:
name: mod1 name
allow:
authenticated:
- op1
deny:
authenticated:
- op2

View File

@@ -0,0 +1,18 @@
namespace: service-solution
records:
Department:
- values:
Name: "Service"
HourCost: "50"
EmailTemplate:
- values:
Name: "Case Update"
Description: "This is the default case update template"
Subject: "Case: [{{Case.Number}}] {{Case.Subject}}"
Body: |-
<p><strong>Dear {{Contact.FirstName}}</strong></p>
Settings:
- values:
KBNextNumber: "1"
DefaultCostPerHour: "50"
DefaultTimeUpdate: "0.25"

View File

@@ -0,0 +1,14 @@
namespace: service-solution
records:
mod1:
- values:
f1: "mod1 f1 v1"
f2: "mod1 f2 v1"
mod2:
- values:
f1: "mod2 f1 v1"
f2: "mod2 f2 v1"
mod3:
- values:
f1: "mod3 f1 v1"
f2: "mod3 f2 v1"

View File

@@ -0,0 +1,52 @@
allow:
authenticated:
corteza::compose/:
- access
corteza::compose:namespace/*:
- read
corteza::compose:module/*/*:
- read
corteza::compose:page/*/*:
- read
corteza::compose:chart/*/*:
- read
admins:
corteza::compose/:
- access
- grant
- settings.read
- settings.manage
- namespace.create
corteza::compose:namespace/*:
- read
- update
- delete
- manage
- page.create
- module.create
- chart.create
corteza::compose:module/*/*:
- read
- update
- delete
- record.create
- record.read
- record.update
- record.delete
corteza::compose:chart/*/*:
- read
- update
- delete
corteza::compose:page/*/*:
- read
- update
- delete

View File

@@ -0,0 +1,8 @@
channels:
- name: General
topic: Talk about anything
type: public
- name: Random
type: public
- name: Secret
type: private

View File

@@ -0,0 +1,4 @@
roles:
r1: Role1
r2:
name: Role2

View File

@@ -0,0 +1,28 @@
settings:
privacy.mask.email: true
privacy.mask.name: true
auth.mail.email-confirmation.subject.en: Confirm your email address
auth.mail.email-confirmation.body.en: |-
{{.EmailHeaderEn}}
<h2 style="color: #FF9661;text-align: center;">Confirm your email address</h2>
<p>Hello,</p>
<p>Follow <a href="{{ .URL }}" style="color:#FF9661;">this link</a> to confirm your email address.</p>
<p>You will be logged in after successful confirmation.</p>
{{.EmailFooterEn}}
auth.mail.password-reset.subject.en: Reset your password
auth.mail.password-reset.body.en: |-
{{.EmailHeaderEn}}
<h2 style="color: #FF9661;text-align: center;">Reset your password</h2>
<p>Hello,</p>
<p>Follow <a href="{{ .URL }}" style="color:#FF9661;">this link</a> and reset your password.</p>
<p>You will be logged in after successful reset.</p>
{{.EmailFooterEn}}
compose.ui.namespace-switcher.enabled: false
compose.record.attachments.max-size: 10
compose.record.attachments.mimetypes: []
compose.page.attachments.max-size: 10
compose.page.attachments.mimetypes: []

View File

@@ -0,0 +1,5 @@
users:
u1: u1@example.tld
u2:
name: John Doe
email: jdoe@example.tld

View File

@@ -0,0 +1,75 @@
workflows:
testko_wf:
enabled: true
trace: true
keepSessions: 2
meta:
name: name here
description: description here
triggers:
- enabled: true
stepID: 101
resourceType: "compose:module"
eventType: "beforeCreate"
constraints:
- name: name
op: op
values:
- values
meta:
description: description
visual: {}
ownedBy: "219521853847258224"
createdAt: "2021-02-23T09:53:41Z"
createdBy: "219521853847258224"
updatedAt: "2021-02-23T09:53:41Z"
updatedBy: "219521853847258224"
deletedAt: "2021-02-23T09:53:41Z"
deletedBy: "219521853847258224"
steps:
- stepID: 101
kind: "termination"
arguments:
- target: target
source: source
expr: expr
value: value
type: type
tests:
- expr: expr
error: error
results:
- target: target
source: source
expr: expr
value: value
type: type
tests:
- expr: expr
error: error
meta:
name: name
description: description
visual: {}
paths:
- expr: expr
parentID: 101
childID: 101
meta:
name: name
description: description
visual: {}
runAs: "219521853847258224"
ownedBy: "219521853847258224"
createdAt: "2021-02-23T09:53:41Z"
createdBy: "219521853847258224"
updatedAt: "2021-02-23T09:53:41Z"
updatedBy: "219521853847258224"
deletedAt: "2021-02-23T09:53:41Z"
deletedBy: "219521853847258224"

View File

@@ -0,0 +1,26 @@
package yaml
import (
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
user struct {
res *types.User
ts *resource.Timestamps
roles []string
envoyConfig *resource.EnvoyConfig
encoderConfig *EncoderConfig
rbac rbacRuleSet
}
userSet []*user
)
func (nn userSet) configureEncoder(cfg *EncoderConfig) {
for _, n := range nn {
n.encoderConfig = cfg
}
}

View File

@@ -0,0 +1,90 @@
package yaml
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/system/types"
)
func userFromResource(r *resource.User, cfg *EncoderConfig) *user {
roles := make([]string, 0, len(r.RefRoles))
for _, r := range r.RefRoles {
roles = append(roles, r.Identifiers.First())
}
return &user{
res: r.Res,
roles: roles,
encoderConfig: cfg,
}
}
func (u *user) Prepare(ctx context.Context, state *envoy.ResourceState) (err error) {
us, ok := state.Res.(*resource.User)
if !ok {
return encoderErrInvalidResource(types.UserResourceType, state.Res.ResourceType())
}
u.res = us.Res
return nil
}
func (u *user) Encode(ctx context.Context, doc *Document, state *envoy.ResourceState) (err error) {
if u.res.ID <= 0 {
u.res.ID = nextID()
}
// Encode timestamps
u.ts, err = resource.MakeTimestampsCUDAS(&u.res.CreatedAt, u.res.UpdatedAt, u.res.DeletedAt, nil, u.res.SuspendedAt).
Model(u.encoderConfig.TimeLayout, u.encoderConfig.Timezone)
if err != nil {
return err
}
// @todo implement resource skipping?
doc.addUser(u)
return
}
func (u *user) MarshalYAML() (interface{}, error) {
var err error
nsn, err := makeMap(
"username", u.res.Username,
"email", u.res.Email,
"name", u.res.Name,
"handle", u.res.Handle,
"kind", u.res.Kind,
)
if err != nil {
return nil, err
}
if len(u.roles) > 0 {
nsn, err = addMap(nsn,
"roles", u.roles,
)
if err != nil {
return nil, err
}
}
nsn, err = addMap(nsn,
"meta", u.res.Meta,
"labels", u.res.Labels,
)
if err != nil {
return nil, err
}
nsn, err = encodeTimestamps(nsn, u.ts)
if err != nil {
return nil, err
}
return nsn, nil
}

View File

@@ -0,0 +1,53 @@
package yaml
import (
"testing"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
func TestUser_UnmarshalYAML(t *testing.T) {
var (
parseString = func(src string) (*user, error) {
w := &user{}
return w, yaml.Unmarshal([]byte(src), w)
}
)
t.Run("empty", func(t *testing.T) {
req := require.New(t)
w, err := parseString(``)
req.NoError(err)
req.NotNil(w)
req.Nil(w.res)
})
t.Run("simple name", func(t *testing.T) {
req := require.New(t)
w, err := parseString(`{ name: Test }`)
req.NoError(err)
req.NotNil(w)
req.NotNil(w.res)
req.NotEmpty(w.res.Name)
})
t.Run("user 1", func(t *testing.T) {
req := require.New(t)
doc, err := parseDocument("user_1")
req.NoError(err)
req.NotNil(doc)
req.Len(doc.users, 2)
req.NotNil(doc.users[0])
req.Equal("u1@example.tld", doc.users[0].res.Email)
req.Equal("u1", doc.users[0].res.Handle)
req.NotNil(doc.users[1])
req.Equal("John Doe", doc.users[1].res.Name)
req.Equal("jdoe@example.tld", doc.users[1].res.Email)
req.Equal(true, doc.users[1].res.EmailConfirmed)
req.Equal("u2", doc.users[1].res.Handle)
})
}

View File

@@ -0,0 +1,125 @@
package yaml
import (
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/pkg/y7s"
"github.com/cortezaproject/corteza/server/system/types"
"gopkg.in/yaml.v3"
)
func (wset *userSet) UnmarshalYAML(n *yaml.Node) error {
return y7s.Each(n, func(k, v *yaml.Node) (err error) {
var (
wrap = &user{}
)
if v == nil {
return y7s.NodeErr(n, "malformed user definition")
}
wrap.res = &types.User{
EmailConfirmed: true,
}
switch v.Kind {
case yaml.ScalarNode:
if err = y7s.DecodeScalar(v, "user email", &wrap.res.Email); err != nil {
return
}
case yaml.MappingNode:
if err = v.Decode(&wrap); err != nil {
return
}
default:
return y7s.NodeErr(n, "expecting scalar or map with user definitions")
}
if err = decodeRef(k, "user", &wrap.res.Handle); err != nil {
return err
}
*wset = append(*wset, wrap)
return
})
}
func (wrap *user) UnmarshalYAML(n *yaml.Node) (err error) {
if !y7s.IsKind(n, yaml.MappingNode) {
return y7s.NodeErr(n, "user definition must be a map")
}
if wrap.res == nil {
wrap.res = &types.User{}
}
if err = n.Decode(&wrap.res); err != nil {
return
}
if wrap.rbac, err = decodeRbac(n); err != nil {
return
}
if wrap.envoyConfig, err = decodeEnvoyConfig(n); err != nil {
return
}
if wrap.roles, err = decodeUserRoles(n); err != nil {
return
}
if wrap.ts, err = decodeTimestamps(n); err != nil {
return
}
return nil
}
func decodeUserRoles(n *yaml.Node) (roles []string, err error) {
var ecNode *yaml.Node
for i, k := range n.Content {
if k.Value == "roles" {
ecNode = n.Content[i+1]
break
}
}
if ecNode == nil {
return
}
return roles, y7s.EachSeq(ecNode, func(v *yaml.Node) (err error) {
roles = append(roles, v.Value)
return nil
})
}
func (wset userSet) MarshalEnvoy() ([]resource.Interface, error) {
nn := make([]resource.Interface, 0, len(wset))
for _, res := range wset {
if tmp, err := res.MarshalEnvoy(); err != nil {
return nil, err
} else {
nn = append(nn, tmp...)
}
}
return nn, nil
}
func (wrap user) MarshalEnvoy() ([]resource.Interface, error) {
rs := resource.NewUser(wrap.res, wrap.roles...)
rs.SetTimestamps(wrap.ts)
rs.SetConfig(wrap.envoyConfig)
return envoy.CollectNodes(
rs,
wrap.rbac.bindResource(rs),
)
}

View File

@@ -0,0 +1,76 @@
package yaml
import (
"strconv"
"time"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/pkg/id"
)
var (
// wrapper around nextID that will aid testing
nextID = func() uint64 {
return id.Next()
}
// wrapper around time.Now() that will aid testing
now = func() *time.Time {
c := time.Now().Round(time.Second)
return &c
}
)
func firstOkString(ss ...string) string {
for _, s := range ss {
if s != "" {
return s
}
}
return ""
}
func resolveUserstamps(rr []resource.Interface, us *resource.Userstamps) (*resource.Userstamps, error) {
if us == nil {
return nil, nil
}
fetch := func(us *resource.Userstamp) (*resource.Userstamp, error) {
if us == nil || (us.UserID == 0 && us.Ref == "") {
return nil, nil
}
// This one can be considered as valid
if us.Ref != "" && us.UserID > 0 && us.U != nil {
return us, nil
}
ii := resource.MakeIdentifiers()
if us.UserID > 0 {
ii = ii.Add(strconv.FormatUint(us.UserID, 10))
}
if us.Ref != "" {
ii = ii.Add(us.Ref)
}
u := resource.FindUser(rr, ii)
if u == nil {
return nil, resource.UserErrUnresolved(ii)
}
return resource.MakeUserstamp(u), nil
}
var err error
us.CreatedBy, err = fetch(us.CreatedBy)
us.UpdatedBy, err = fetch(us.UpdatedBy)
us.DeletedBy, err = fetch(us.DeletedBy)
us.OwnedBy, err = fetch(us.OwnedBy)
us.RunAs, err = fetch(us.RunAs)
if err != nil {
return nil, err
}
return us, nil
}

View File

@@ -0,0 +1,259 @@
package yaml
import (
"fmt"
"gopkg.in/yaml.v3"
)
// cleanMap helper removes any empty k:v nodes from the mapping node
//
// The value is empty when the tag is !!null OR when the value and the content are empty
func cleanMap(n *yaml.Node) *yaml.Node {
cc := make([]*yaml.Node, 0, len(n.Content))
for i := 0; i < len(n.Content)-1; i += 2 {
k := n.Content[i]
v := n.Content[i+1]
if v.Tag == "!!null" || v.Value == "" && (len(v.Content) == 0 || v.Content == nil) {
continue
}
cc = append(cc, k, v)
}
n.Content = cc
return n
}
// encodeResource encodes the provided struct v as the resource r
//
// The helper has built-in sequence -> map transformation
func encodeResource(n *yaml.Node, r string, v interface{}, mapped bool, k string) (*yaml.Node, error) {
var err error
if mapped {
m, err := mapResource(v, k)
if err != nil {
return nil, err
}
n, err = addMap(n,
r, m,
)
if err != nil {
return nil, err
}
} else {
n, err = addMap(n,
r, v,
)
if err != nil {
return nil, err
}
}
return n, nil
}
// mapResource encodes the resource as a map instead of a sequence
func mapResource(v interface{}, key string) (*yaml.Node, error) {
var m *yaml.Node
m, err := encodeNode(v)
if err != nil {
return nil, err
}
// It's already a mapping node; don't touch it
if m.Kind == yaml.MappingNode {
return m, nil
}
if m.Kind != yaml.SequenceNode {
return nil, fmt.Errorf("expecting mapping or sequence node (%s provided)", m.Tag)
}
m, err = seqToMap(m, key)
if err != nil {
return nil, err
}
return m, nil
}
// encodeNode shortens raw node encoding
func encodeNode(v interface{}) (*yaml.Node, error) {
n := &yaml.Node{}
err := n.Encode(v)
if err != nil {
return nil, err
}
return n, nil
}
// seqToMap converts the given sequence node into a mapping node
//
// The provided value defined by field k is used as the map key.
// The used value is removed from the map value.
// If the field is not found, an error is returned.
func seqToMap(ss *yaml.Node, k string) (*yaml.Node, error) {
if ss.Kind != yaml.SequenceNode {
return nil, fmt.Errorf("expecting sequence node (%s provided)", ss.Tag)
}
if k == "" {
return nil, fmt.Errorf("key field not defined")
}
var err error
mm, _ := makeMap()
for _, s := range ss.Content {
if s.Kind != yaml.MappingNode {
return nil, fmt.Errorf("sequence may only contain mapping nodes (%s found)", s.Tag)
}
// Find the key value; remove from map value
var kn *yaml.Node
for i := 0; i < len(s.Content)-1; i += 2 {
if s.Content[i].Value == k {
kn = s.Content[i+1]
s.Content = append(s.Content[:i], s.Content[i+2:]...)
break
}
}
if kn == nil {
return nil, fmt.Errorf("key field not defined")
}
mm, err = addMap(mm, kn.Value, s)
if err != nil {
return nil, err
}
}
return mm, nil
}
// inlineContent places mm mapping node content into the n mapping node content
func inlineContent(n *yaml.Node, mm ...*yaml.Node) (*yaml.Node, error) {
if n.Kind != yaml.MappingNode {
return nil, fmt.Errorf("expecting mapping node (destination node)")
}
for _, m := range mm {
if m.Kind != yaml.MappingNode {
return nil, fmt.Errorf("expecting mapping node (source node)")
}
n.Content = append(n.Content, m.Content...)
}
return n, nil
}
// makeMap creates a new mapping node based on the provided k, v items
//
// pp is a set of k, v items; where k's lie at i, and v's lie at i+1.
// non-string values (required by YAML nodes) are processed further.
// eg.: ["k1", "v1", "k2", "v2"]
func makeMap(pp ...interface{}) (*yaml.Node, error) {
return addMap(&yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}, pp...)
}
// addMap adds a new item to the provided mapping node
//
// pp is a set of k, v items; where k's lie at i, and v's lie at i+1.
// non-string values (required by YAML nodes) are processed further.
// eg.: ["k1", "v1", "k2", "v2"]
func addMap(n *yaml.Node, pp ...interface{}) (*yaml.Node, error) {
if len(pp) == 0 {
return n, nil
}
if len(pp)%2 == 1 {
return nil, fmt.Errorf("uneven number of elements provided (%d): %v", len(pp), pp)
}
var err error
for i := 0; i < len(pp); i += 2 {
kRaw := pp[i]
vRaw := pp[i+1]
k, ok := kRaw.(string)
if !ok {
return nil, fmt.Errorf("keys must be of type string: %v", kRaw)
}
var vn *yaml.Node
switch v := vRaw.(type) {
case bool:
if !v {
continue
}
vn, err = encodeNode(v)
case string:
if v == "" {
continue
}
vn, err = encodeNode(v)
if v == "\n" {
vn.Style = yaml.DoubleQuotedStyle
}
default:
vn, err = encodeNode(vRaw)
}
if err != nil {
return nil, err
}
// Cleanup the content
if vn.Kind == yaml.MappingNode {
vn = cleanMap(vn)
}
// Discard any null nodes and empty nodes
if vn.Tag == "!!null" || vn.Value == "" && (len(vn.Content) == 0 || vn.Content == nil) {
continue
}
n.Content = append(n.Content,
&yaml.Node{Kind: yaml.ScalarNode, Value: k},
vn,
)
}
return n, nil
}
// makeSeq creates a new sequence node based on the provided items
func makeSeq(vv ...interface{}) (*yaml.Node, error) {
return addSeq(&yaml.Node{Kind: yaml.SequenceNode}, vv...)
}
// addSeq adds new items to the sequence node
func addSeq(n *yaml.Node, vv ...interface{}) (*yaml.Node, error) {
var err error
var vn *yaml.Node
for _, vRaw := range vv {
vn, err = encodeNode(vRaw)
if err != nil {
return nil, err
}
// Discard any null nodes and empty nodes
if vn.Tag == "!!null" || vn.Value == "" && (len(vn.Content) == 0 || vn.Content == nil) {
continue
}
n.Content = append(n.Content, vn)
}
return n, nil
}