3
0

Add base resource decoders

* yaml,
* csv.
This commit is contained in:
Tomaž Jerman 2020-10-19 17:00:33 +02:00
parent d076dbd70f
commit 93bfa80eb0
5 changed files with 329 additions and 9 deletions

View File

@ -1,27 +1,28 @@
package types
import (
"time"
"github.com/cortezaproject/corteza-server/pkg/filter"
"github.com/cortezaproject/corteza-server/pkg/rbac"
"github.com/jmoiron/sqlx/types"
"time"
)
type (
Module struct {
ID uint64 `json:"moduleID,string"`
ID uint64 `json:"moduleID,string" yaml:"-"`
Handle string `json:"handle"`
Name string `json:"name"`
Meta types.JSONText `json:"meta"`
Fields ModuleFieldSet `json:"fields"`
Meta types.JSONText `json:"meta" yaml:",omitempty"`
Fields ModuleFieldSet `json:"fields" yaml:"-"`
Labels map[string]string `json:"labels,omitempty"`
NamespaceID uint64 `json:"namespaceID,string"`
CreatedAt time.Time `json:"createdAt,omitempty"`
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
DeletedAt *time.Time `json:"deletedAt,omitempty"`
CreatedAt time.Time `json:"createdAt,omitempty" yaml:",omitempty"`
UpdatedAt *time.Time `json:"updatedAt,omitempty" yaml:",omitempty"`
DeletedAt *time.Time `json:"deletedAt,omitempty" yaml:",omitempty"`
}
ModuleFilter struct {

View File

@ -3,10 +3,11 @@ package types
import (
"encoding/json"
"fmt"
"github.com/cortezaproject/corteza-server/pkg/filter"
"strconv"
"time"
"github.com/cortezaproject/corteza-server/pkg/filter"
"github.com/cortezaproject/corteza-server/pkg/rbac"
)
@ -35,7 +36,7 @@ type (
ID uint64 `json:"recordID,string"`
ModuleID uint64 `json:"moduleID,string"`
Values RecordValueSet `json:"values,omitempty"`
Values RecordValueSet `json:"values,omitempty" yaml:"-"`
Labels map[string]string `json:"labels,omitempty"`

112
pkg/envoy/decoder/csv.go Normal file
View File

@ -0,0 +1,112 @@
package decoder
import (
"context"
"encoding/csv"
"errors"
"io"
"regexp"
"github.com/cortezaproject/corteza-server/pkg/envoy/types"
)
type (
CsvDecoder struct{}
)
var (
ErrorNoCsvHeader = errors.New("csv decoder: no header")
ErrorCsvHeaderMalformed = errors.New("csv decoder: header malformed")
// This strict regexp for field names will do for now.
// Later we can add support for matching over field labels as well.
headerRegexp, _ = regexp.Compile("^[A-Za-z][0-9A-Za-z_]*[A-Za-z0-9]$")
)
func NewCsvDecoder() *CsvDecoder {
return &CsvDecoder{}
}
// A quick header field validator
//
// @note should we complicate it any further?
func (c *CsvDecoder) validateHeader(header []string) error {
for _, h := range header {
if !headerRegexp.MatchString(h) {
return ErrorCsvHeaderMalformed
}
}
return nil
}
func (c *CsvDecoder) Decode(ctx context.Context, r io.Reader, filename string) ([]types.Node, error) {
n := &types.ComposeRecordNode{}
// Determine base module for dependency resolution
// -4 is to remove .csv ext
//
// @todo tweak this a bit
modRes := filename[0 : len(filename)-4]
mod := &types.ComposeModule{}
mod.Handle = modRes
mod.Name = modRes
n.Mod = mod
// Prepare reader
//
// For optimization we reuse allocated memory; keep this in mind!
cr := csv.NewReader(r)
cr.ReuseRecord = true
// Get header
hh, err := cr.Read()
if err == io.EOF {
return nil, ErrorNoCsvHeader
} else if err != nil {
return nil, err
}
header := make([]string, 0, len(hh))
for _, h := range hh {
header = append(header, h)
}
err = c.validateHeader(header)
if err != nil {
return nil, err
}
// Iterator function for providing records to be imported.
// This doesn't do any validation; that should be handled by other layers.
n.Walk = func(f func(*types.ComposeRecord) error) error {
for {
record, err := cr.Read()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
rvs := make(types.ComposeRecordValueSet, 0)
for i, h := range header {
v := &types.ComposeRecordValue{}
v.Name = h
v.Value = record[i]
rvs = append(rvs, v)
}
rec := &types.ComposeRecord{}
rec.Values = rvs
err = f(rec)
if err != nil {
return err
}
}
}
return []types.Node{n}, nil
}

167
pkg/envoy/decoder/yaml.go Normal file
View File

@ -0,0 +1,167 @@
package decoder
import (
"context"
"errors"
"io"
"strings"
"github.com/cortezaproject/corteza-server/pkg/envoy/types"
"gopkg.in/yaml.v3"
)
type (
// YamlDecoder is a wrapper struct for yaml related methods
YamlDecoder struct{}
// Document defines the supported yaml structure
Document struct {
Namespace string
Namespaces types.ComposeNamespaceSet
Modules types.ComposeModuleSet
Records map[string]types.ComposeRecordSet
}
)
var (
ErrorCannotResolveNamespace = errors.New("yaml decoder: cannot resolve namespace")
)
func NewYamlDecoder() *YamlDecoder {
return &YamlDecoder{}
}
func (y *YamlDecoder) unmarshalDocument(r io.Reader) (*Document, error) {
var c *Document
buf := new(strings.Builder)
_, err := io.Copy(buf, r)
if err != nil {
return nil, err
}
err = yaml.Unmarshal([]byte(buf.String()), &c)
if err != nil {
return nil, err
}
return c, nil
}
// convert converts the decoded document into a set of envoy nodes
func (y *YamlDecoder) convert(c *Document) ([]types.Node, error) {
nn := make([]types.Node, 0, 100)
// In case of namespaces...
if c.Namespaces != nil {
nodes, err := y.convertNamespaces(c.Namespaces)
if err != nil {
return nil, err
}
nn = append(nn, nodes...)
}
ns := &types.ComposeNamespace{}
if c.Namespace != "" {
// In case of a namespace to provide dependencies
ns.Slug = c.Namespace
ns.Name = c.Namespace
} else if len(nn) > 0 {
// Try to fall back to a namespace node
ns = ((nn[0]).(*types.ComposeNamespaceNode)).Ns
} else {
// No good; we can't link with a namespace.
// @note This should be checked when converting Compose resources only.
// Some resources don't belong to a namespace.
return nil, ErrorCannotResolveNamespace
}
// In case of modules...
if c.Modules != nil {
nodes, err := y.convertModules(c.Modules, ns)
if err != nil {
return nil, err
}
nn = append(nn, nodes...)
}
if c.Records != nil {
for modRef, rr := range c.Records {
// We can define a basic module representation as it will be updated later
// during validation/runtime
mod := &types.ComposeModule{}
mod.Handle = modRef
mod.Name = modRef
nodes, err := y.convertRecords(rr, mod)
if err != nil {
return nil, err
}
nn = append(nn, nodes...)
}
}
return nn, nil
}
func (y *YamlDecoder) convertNamespaces(nss types.ComposeNamespaceSet) ([]types.Node, error) {
nn := make([]types.Node, 0, 2)
for _, ns := range nss {
nn = append(nn, &types.ComposeNamespaceNode{Ns: ns})
// Nested modules
if ns.Modules != nil {
mm, err := y.convertModules(ns.Modules, ns)
if err != nil {
return nil, err
}
nn = append(nn, mm...)
}
// @todo nested RBAC
}
return nn, nil
}
func (y *YamlDecoder) convertModules(mm types.ComposeModuleSet, ns *types.ComposeNamespace) ([]types.Node, error) {
nn := make([]types.Node, 0)
for _, m := range mm {
nn = append(nn, &types.ComposeModuleNode{
Mod: m,
Ns: ns,
})
// @todo nested resources; should there be any?
}
return nn, nil
}
func (y *YamlDecoder) convertRecords(rr types.ComposeRecordSet, m *types.ComposeModule) ([]types.Node, error) {
// Iterator function for providing records to be imported.
// This doesn't do any validation; that should be handled by other layers.
f := func(f func(*types.ComposeRecord) error) error {
for _, r := range rr {
err := f(r)
if err != nil {
return err
}
}
return nil
}
return []types.Node{&types.ComposeRecordNode{Mod: m, Walk: f}}, nil
}
func (y *YamlDecoder) Decode(ctx context.Context, r io.Reader) ([]types.Node, error) {
d, err := y.unmarshalDocument(r)
if err != nil {
return nil, err
}
return y.convert(d)
}

39
pkg/envoy/util/yaml.go Normal file
View File

@ -0,0 +1,39 @@
package util
import (
"fmt"
"gopkg.in/yaml.v3"
)
func yamlNodeErr(n *yaml.Node, format string, aa ...interface{}) error {
format += " (%d:%d)"
aa = append(aa, n.Line, n.Column)
return fmt.Errorf(format, aa...)
}
// YamlIterator helps iterate over mapping and sequence nodes fairly trivially
func YamlIterator(n *yaml.Node, fn func(*yaml.Node, *yaml.Node) error) error {
if n.Kind == yaml.MappingNode {
for i := 0; i < len(n.Content); i += 2 {
if err := fn(n.Content[i], n.Content[i+1]); err != nil {
return err
}
}
return nil
}
if n.Kind == yaml.SequenceNode {
var placeholder *yaml.Node
for i := 0; i < len(n.Content); i++ {
if err := fn(placeholder, n.Content[i]); err != nil {
return err
}
}
return nil
}
return yamlNodeErr(n, "expecting mapping or sequence node")
}