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
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:
39
server/pkg/envoy/yaml/api_gateway.go
Normal file
39
server/pkg/envoy/yaml/api_gateway.go
Normal 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
|
||||
}
|
||||
}
|
||||
116
server/pkg/envoy/yaml/api_gateway_marshal.go
Normal file
116
server/pkg/envoy/yaml/api_gateway_marshal.go
Normal 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
|
||||
}
|
||||
156
server/pkg/envoy/yaml/api_gateway_unmarshal.go
Normal file
156
server/pkg/envoy/yaml/api_gateway_unmarshal.go
Normal 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),
|
||||
)
|
||||
}
|
||||
26
server/pkg/envoy/yaml/applications.go
Normal file
26
server/pkg/envoy/yaml/applications.go
Normal 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
|
||||
}
|
||||
}
|
||||
81
server/pkg/envoy/yaml/applications_marshal.go
Normal file
81
server/pkg/envoy/yaml/applications_marshal.go
Normal 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
|
||||
}
|
||||
49
server/pkg/envoy/yaml/applications_test.go
Normal file
49
server/pkg/envoy/yaml/applications_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
98
server/pkg/envoy/yaml/applications_unmarshal.go
Normal file
98
server/pkg/envoy/yaml/applications_unmarshal.go
Normal 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),
|
||||
)
|
||||
}
|
||||
64
server/pkg/envoy/yaml/automation.go
Normal file
64
server/pkg/envoy/yaml/automation.go
Normal 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
|
||||
}
|
||||
63
server/pkg/envoy/yaml/automation_workflow.go
Normal file
63
server/pkg/envoy/yaml/automation_workflow.go
Normal 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
|
||||
}
|
||||
}
|
||||
147
server/pkg/envoy/yaml/automation_workflow_marshal.go
Normal file
147
server/pkg/envoy/yaml/automation_workflow_marshal.go
Normal 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
|
||||
}
|
||||
39
server/pkg/envoy/yaml/automation_workflow_test.go
Normal file
39
server/pkg/envoy/yaml/automation_workflow_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
297
server/pkg/envoy/yaml/automation_workflow_unmarshal.go
Normal file
297
server/pkg/envoy/yaml/automation_workflow_unmarshal.go
Normal 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),
|
||||
)
|
||||
}
|
||||
202
server/pkg/envoy/yaml/compose.go
Normal file
202
server/pkg/envoy/yaml/compose.go
Normal 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
|
||||
}
|
||||
52
server/pkg/envoy/yaml/compose_chart.go
Normal file
52
server/pkg/envoy/yaml/compose_chart.go
Normal 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))
|
||||
}
|
||||
131
server/pkg/envoy/yaml/compose_chart_marshal.go
Normal file
131
server/pkg/envoy/yaml/compose_chart_marshal.go
Normal 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
|
||||
}
|
||||
68
server/pkg/envoy/yaml/compose_chart_test.go
Normal file
68
server/pkg/envoy/yaml/compose_chart_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
181
server/pkg/envoy/yaml/compose_chart_unmarshal.go
Normal file
181
server/pkg/envoy/yaml/compose_chart_unmarshal.go
Normal 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
|
||||
})
|
||||
}
|
||||
68
server/pkg/envoy/yaml/compose_module.go
Normal file
68
server/pkg/envoy/yaml/compose_module.go
Normal 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
|
||||
}
|
||||
}
|
||||
227
server/pkg/envoy/yaml/compose_module_marshal.go
Normal file
227
server/pkg/envoy/yaml/compose_module_marshal.go
Normal 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,
|
||||
)
|
||||
}
|
||||
173
server/pkg/envoy/yaml/compose_module_test.go
Normal file
173
server/pkg/envoy/yaml/compose_module_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
408
server/pkg/envoy/yaml/compose_module_unmarshal.go
Normal file
408
server/pkg/envoy/yaml/compose_module_unmarshal.go
Normal 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
|
||||
})
|
||||
}
|
||||
158
server/pkg/envoy/yaml/compose_namespace.go
Normal file
158
server/pkg/envoy/yaml/compose_namespace.go
Normal 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)
|
||||
}
|
||||
102
server/pkg/envoy/yaml/compose_namespace_marshal.go
Normal file
102
server/pkg/envoy/yaml/compose_namespace_marshal.go
Normal 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
|
||||
}
|
||||
78
server/pkg/envoy/yaml/compose_namespace_test.go
Normal file
78
server/pkg/envoy/yaml/compose_namespace_test.go
Normal 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)
|
||||
}
|
||||
143
server/pkg/envoy/yaml/compose_namespace_unmarshal.go
Normal file
143
server/pkg/envoy/yaml/compose_namespace_unmarshal.go
Normal 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),
|
||||
)
|
||||
}
|
||||
97
server/pkg/envoy/yaml/compose_page.go
Normal file
97
server/pkg/envoy/yaml/compose_page.go
Normal 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)
|
||||
}
|
||||
300
server/pkg/envoy/yaml/compose_page_marshal.go
Normal file
300
server/pkg/envoy/yaml/compose_page_marshal.go
Normal 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
|
||||
}
|
||||
50
server/pkg/envoy/yaml/compose_page_test.go
Normal file
50
server/pkg/envoy/yaml/compose_page_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
191
server/pkg/envoy/yaml/compose_page_unmarshal.go
Normal file
191
server/pkg/envoy/yaml/compose_page_unmarshal.go
Normal 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
|
||||
}
|
||||
34
server/pkg/envoy/yaml/compose_record.go
Normal file
34
server/pkg/envoy/yaml/compose_record.go
Normal 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
|
||||
}
|
||||
}
|
||||
164
server/pkg/envoy/yaml/compose_record_marshal.go
Normal file
164
server/pkg/envoy/yaml/compose_record_marshal.go
Normal 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
|
||||
}
|
||||
90
server/pkg/envoy/yaml/compose_record_test.go
Normal file
90
server/pkg/envoy/yaml/compose_record_test.go
Normal 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
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
204
server/pkg/envoy/yaml/compose_record_unmarshal.go
Normal file
204
server/pkg/envoy/yaml/compose_record_unmarshal.go
Normal 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 ""
|
||||
}
|
||||
55
server/pkg/envoy/yaml/compose_test.go
Normal file
55
server/pkg/envoy/yaml/compose_test.go
Normal 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")
|
||||
})
|
||||
}
|
||||
85
server/pkg/envoy/yaml/decoder.go
Normal file
85
server/pkg/envoy/yaml/decoder.go
Normal 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)
|
||||
}
|
||||
422
server/pkg/envoy/yaml/document.go
Normal file
422
server/pkg/envoy/yaml/document.go
Normal 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)
|
||||
}
|
||||
18
server/pkg/envoy/yaml/document_test.go
Normal file
18
server/pkg/envoy/yaml/document_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
302
server/pkg/envoy/yaml/encoder.go
Normal file
302
server/pkg/envoy/yaml/encoder.go
Normal 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
|
||||
}
|
||||
19
server/pkg/envoy/yaml/main_test.go
Normal file
19
server/pkg/envoy/yaml/main_test.go
Normal 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)
|
||||
}
|
||||
113
server/pkg/envoy/yaml/rbac_rules.go
Normal file
113
server/pkg/envoy/yaml/rbac_rules.go
Normal 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)
|
||||
}
|
||||
387
server/pkg/envoy/yaml/rbac_rules_marshal.go
Normal file
387
server/pkg/envoy/yaml/rbac_rules_marshal.go
Normal 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
|
||||
}
|
||||
55
server/pkg/envoy/yaml/rbac_rules_test.go
Normal file
55
server/pkg/envoy/yaml/rbac_rules_test.go
Normal 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)
|
||||
}
|
||||
112
server/pkg/envoy/yaml/rbac_rules_unmarshal.go
Normal file
112
server/pkg/envoy/yaml/rbac_rules_unmarshal.go
Normal 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
|
||||
}
|
||||
48
server/pkg/envoy/yaml/report.go
Normal file
48
server/pkg/envoy/yaml/report.go
Normal 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
|
||||
}
|
||||
}
|
||||
131
server/pkg/envoy/yaml/report_marshal.go
Normal file
131
server/pkg/envoy/yaml/report_marshal.go
Normal 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
|
||||
}
|
||||
204
server/pkg/envoy/yaml/report_unmarshal.go
Normal file
204
server/pkg/envoy/yaml/report_unmarshal.go
Normal 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),
|
||||
)
|
||||
}
|
||||
69
server/pkg/envoy/yaml/resource.go
Normal file
69
server/pkg/envoy/yaml/resource.go
Normal 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
|
||||
}
|
||||
57
server/pkg/envoy/yaml/resource_translation.go
Normal file
57
server/pkg/envoy/yaml/resource_translation.go
Normal 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,
|
||||
}
|
||||
}
|
||||
420
server/pkg/envoy/yaml/resource_translation_marshal.go
Normal file
420
server/pkg/envoy/yaml/resource_translation_marshal.go
Normal 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
|
||||
}
|
||||
119
server/pkg/envoy/yaml/resource_translation_unmarshal.go
Normal file
119
server/pkg/envoy/yaml/resource_translation_unmarshal.go
Normal 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
|
||||
}
|
||||
26
server/pkg/envoy/yaml/role.go
Normal file
26
server/pkg/envoy/yaml/role.go
Normal 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
|
||||
}
|
||||
}
|
||||
64
server/pkg/envoy/yaml/role_marshal.go
Normal file
64
server/pkg/envoy/yaml/role_marshal.go
Normal 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
|
||||
}
|
||||
48
server/pkg/envoy/yaml/role_test.go
Normal file
48
server/pkg/envoy/yaml/role_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
98
server/pkg/envoy/yaml/role_unmarshal.go
Normal file
98
server/pkg/envoy/yaml/role_unmarshal.go
Normal 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),
|
||||
)
|
||||
}
|
||||
19
server/pkg/envoy/yaml/setting.go
Normal file
19
server/pkg/envoy/yaml/setting.go
Normal 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
|
||||
)
|
||||
73
server/pkg/envoy/yaml/setting_marshal.go
Normal file
73
server/pkg/envoy/yaml/setting_marshal.go
Normal 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
|
||||
}
|
||||
20
server/pkg/envoy/yaml/setting_test.go
Normal file
20
server/pkg/envoy/yaml/setting_test.go
Normal 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"])
|
||||
})
|
||||
}
|
||||
167
server/pkg/envoy/yaml/setting_unmarshal.go
Normal file
167
server/pkg/envoy/yaml/setting_unmarshal.go
Normal 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,
|
||||
)
|
||||
}
|
||||
109
server/pkg/envoy/yaml/stamps.go
Normal file
109
server/pkg/envoy/yaml/stamps.go
Normal 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,
|
||||
)
|
||||
}
|
||||
26
server/pkg/envoy/yaml/template.go
Normal file
26
server/pkg/envoy/yaml/template.go
Normal 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
|
||||
}
|
||||
}
|
||||
86
server/pkg/envoy/yaml/template_marshal.go
Normal file
86
server/pkg/envoy/yaml/template_marshal.go
Normal 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
|
||||
}
|
||||
95
server/pkg/envoy/yaml/template_unmarshal.go
Normal file
95
server/pkg/envoy/yaml/template_unmarshal.go
Normal 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),
|
||||
)
|
||||
}
|
||||
3
server/pkg/envoy/yaml/testdata/application_1.yaml
vendored
Normal file
3
server/pkg/envoy/yaml/testdata/application_1.yaml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
applications:
|
||||
- one
|
||||
- name: two
|
||||
48
server/pkg/envoy/yaml/testdata/compose_chart_1.yaml
vendored
Normal file
48
server/pkg/envoy/yaml/testdata/compose_chart_1.yaml
vendored
Normal 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 ] }
|
||||
9
server/pkg/envoy/yaml/testdata/compose_chart_rbac.yaml
vendored
Normal file
9
server/pkg/envoy/yaml/testdata/compose_chart_rbac.yaml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace: crm
|
||||
charts:
|
||||
ChartHandle:
|
||||
allow:
|
||||
authenticated:
|
||||
- read
|
||||
deny:
|
||||
authenticated:
|
||||
- delete
|
||||
1634
server/pkg/envoy/yaml/testdata/compose_module_1.yaml
vendored
Normal file
1634
server/pkg/envoy/yaml/testdata/compose_module_1.yaml
vendored
Normal file
File diff suppressed because it is too large
Load Diff
17
server/pkg/envoy/yaml/testdata/compose_module_2.yaml
vendored
Normal file
17
server/pkg/envoy/yaml/testdata/compose_module_2.yaml
vendored
Normal 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
|
||||
10
server/pkg/envoy/yaml/testdata/compose_module_rbac.yaml
vendored
Normal file
10
server/pkg/envoy/yaml/testdata/compose_module_rbac.yaml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace: crm
|
||||
modules:
|
||||
Account:
|
||||
name: Account
|
||||
allow:
|
||||
authenticated:
|
||||
- read
|
||||
deny:
|
||||
authenticated:
|
||||
- delete
|
||||
11
server/pkg/envoy/yaml/testdata/compose_module_s_1.yaml
vendored
Normal file
11
server/pkg/envoy/yaml/testdata/compose_module_s_1.yaml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace: ns1
|
||||
modules:
|
||||
mod1:
|
||||
records:
|
||||
source: mod1.csv
|
||||
key: id
|
||||
mapping:
|
||||
- f1
|
||||
-
|
||||
- /
|
||||
- f2
|
||||
7
server/pkg/envoy/yaml/testdata/compose_module_s_2.yaml
vendored
Normal file
7
server/pkg/envoy/yaml/testdata/compose_module_s_2.yaml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace: ns1
|
||||
modules:
|
||||
mod1:
|
||||
records:
|
||||
key:
|
||||
- f1
|
||||
- f2
|
||||
9
server/pkg/envoy/yaml/testdata/compose_module_s_3.yaml
vendored
Normal file
9
server/pkg/envoy/yaml/testdata/compose_module_s_3.yaml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace: ns1
|
||||
modules:
|
||||
mod1:
|
||||
records:
|
||||
mapping:
|
||||
c1:
|
||||
field: f1
|
||||
c2:
|
||||
field: f2
|
||||
10
server/pkg/envoy/yaml/testdata/compose_namespace_1.yaml
vendored
Normal file
10
server/pkg/envoy/yaml/testdata/compose_namespace_1.yaml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
namespaces:
|
||||
crm:
|
||||
name: CRM
|
||||
allow:
|
||||
authenticated:
|
||||
- module.create
|
||||
- read
|
||||
deny:
|
||||
authenticated:
|
||||
- delete
|
||||
206
server/pkg/envoy/yaml/testdata/compose_page_1.yaml
vendored
Normal file
206
server/pkg/envoy/yaml/testdata/compose_page_1.yaml
vendored
Normal 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
|
||||
10
server/pkg/envoy/yaml/testdata/compose_rbac_1.yaml
vendored
Normal file
10
server/pkg/envoy/yaml/testdata/compose_rbac_1.yaml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
modules:
|
||||
mod1:
|
||||
name: mod1 name
|
||||
|
||||
allow:
|
||||
authenticated:
|
||||
- op1
|
||||
deny:
|
||||
authenticated:
|
||||
- op2
|
||||
18
server/pkg/envoy/yaml/testdata/compose_record_1.yaml
vendored
Normal file
18
server/pkg/envoy/yaml/testdata/compose_record_1.yaml
vendored
Normal 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"
|
||||
14
server/pkg/envoy/yaml/testdata/compose_record_2.yaml
vendored
Normal file
14
server/pkg/envoy/yaml/testdata/compose_record_2.yaml
vendored
Normal 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"
|
||||
52
server/pkg/envoy/yaml/testdata/global_rbac_1.yaml
vendored
Normal file
52
server/pkg/envoy/yaml/testdata/global_rbac_1.yaml
vendored
Normal 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
|
||||
8
server/pkg/envoy/yaml/testdata/messaging_channels_1.yaml
vendored
Normal file
8
server/pkg/envoy/yaml/testdata/messaging_channels_1.yaml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
channels:
|
||||
- name: General
|
||||
topic: Talk about anything
|
||||
type: public
|
||||
- name: Random
|
||||
type: public
|
||||
- name: Secret
|
||||
type: private
|
||||
4
server/pkg/envoy/yaml/testdata/role_1.yaml
vendored
Normal file
4
server/pkg/envoy/yaml/testdata/role_1.yaml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
roles:
|
||||
r1: Role1
|
||||
r2:
|
||||
name: Role2
|
||||
28
server/pkg/envoy/yaml/testdata/settings_1.yaml
vendored
Normal file
28
server/pkg/envoy/yaml/testdata/settings_1.yaml
vendored
Normal 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: []
|
||||
5
server/pkg/envoy/yaml/testdata/user_1.yaml
vendored
Normal file
5
server/pkg/envoy/yaml/testdata/user_1.yaml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
users:
|
||||
u1: u1@example.tld
|
||||
u2:
|
||||
name: John Doe
|
||||
email: jdoe@example.tld
|
||||
75
server/pkg/envoy/yaml/testdata/workflow_1.yaml
vendored
Normal file
75
server/pkg/envoy/yaml/testdata/workflow_1.yaml
vendored
Normal 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"
|
||||
26
server/pkg/envoy/yaml/user.go
Normal file
26
server/pkg/envoy/yaml/user.go
Normal 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
|
||||
}
|
||||
}
|
||||
90
server/pkg/envoy/yaml/user_marshal.go
Normal file
90
server/pkg/envoy/yaml/user_marshal.go
Normal 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
|
||||
}
|
||||
53
server/pkg/envoy/yaml/user_test.go
Normal file
53
server/pkg/envoy/yaml/user_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
125
server/pkg/envoy/yaml/user_unmarshal.go
Normal file
125
server/pkg/envoy/yaml/user_unmarshal.go
Normal 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),
|
||||
)
|
||||
}
|
||||
76
server/pkg/envoy/yaml/util.go
Normal file
76
server/pkg/envoy/yaml/util.go
Normal 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
|
||||
}
|
||||
259
server/pkg/envoy/yaml/yaml.go
Normal file
259
server/pkg/envoy/yaml/yaml.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user