3
0
corteza/pkg/envoy/yaml/encoder.go
Tomaž Jerman 104bfb23de Update Envoy for new/extended resources
* Reports
* API GW
* Module field; user role filter
* Comment page block
2021-09-22 11:38:35 +02:00

303 lines
7.7 KiB
Go

package yaml
import (
"bytes"
"context"
"errors"
"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 = errors.New("unknown resource")
ErrResourceStateUndefined = errors.New("undefined resource state")
ErrInvalidResourceType = errors.New("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
}