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:
174
server/pkg/envoy/builder.go
Normal file
174
server/pkg/envoy/builder.go
Normal file
@@ -0,0 +1,174 @@
|
||||
package envoy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
)
|
||||
|
||||
type (
|
||||
builder struct {
|
||||
pp []Preparer
|
||||
|
||||
reportMissingRefs bool
|
||||
}
|
||||
|
||||
Stream struct {
|
||||
Source io.Reader
|
||||
Resource string
|
||||
Identifier string
|
||||
}
|
||||
|
||||
Preparer interface {
|
||||
Prepare(ctx context.Context, ee ...*ResourceState) error
|
||||
}
|
||||
|
||||
// Encoder encodes all resources provided by the Rc until nil is passed
|
||||
//
|
||||
// Encoding errors are passed via Ec.
|
||||
Encoder interface {
|
||||
Encode(ctx context.Context, p Provider) error
|
||||
}
|
||||
|
||||
// Streamer provides a set of streams of encoded documents
|
||||
Streamer interface {
|
||||
Stream() []*Stream
|
||||
}
|
||||
|
||||
PrepareEncoder interface {
|
||||
Preparer
|
||||
Encoder
|
||||
}
|
||||
PrepareEncodeStreamer interface {
|
||||
Preparer
|
||||
Encoder
|
||||
Streamer
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
BuilderErrUnresolvedReferences = fmt.Errorf("builder error: unresolved references")
|
||||
)
|
||||
|
||||
func NewBuilder(pp ...Preparer) *builder {
|
||||
return &builder{
|
||||
pp: pp,
|
||||
}
|
||||
}
|
||||
|
||||
func NewSafeBuilder(pp ...Preparer) *builder {
|
||||
return &builder{
|
||||
pp: pp,
|
||||
reportMissingRefs: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Build builds the graph that is used for structured data processing
|
||||
//
|
||||
// Outline:
|
||||
// 1. Build an initial graph so that we can do some structured preprocessing.
|
||||
// 2. Preprocess the resources based on the initial graph. The initial graph
|
||||
// should remain unchanged. Preprocessing can request additional references and
|
||||
// constraints.
|
||||
// 3. Build a final graph based on the preprocessing modifications.
|
||||
func (b *builder) Build(ctx context.Context, rr ...resource.Interface) (*graph, error) {
|
||||
var err error
|
||||
|
||||
g := b.buildGraph(rr)
|
||||
if len(g.missingRefs) > 0 {
|
||||
return g, BuilderErrUnresolvedReferences
|
||||
}
|
||||
|
||||
// Do any dep. related preprocessing
|
||||
var state *ResourceState
|
||||
err = func() error {
|
||||
for {
|
||||
state, err = g.NextInverted(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if state == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, p := range b.pp {
|
||||
err = p.Prepare(ctx, state)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
g = b.buildGraph(rr)
|
||||
if len(g.missingRefs) > 0 {
|
||||
return g, BuilderErrUnresolvedReferences
|
||||
}
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func (b *builder) buildGraph(rr []resource.Interface) *graph {
|
||||
g := newGraph()
|
||||
|
||||
// Prepare nodes for all resources
|
||||
nn := make(nodeSet, 0, len(rr))
|
||||
for _, r := range rr {
|
||||
nn = nn.add(newNode(r))
|
||||
}
|
||||
|
||||
// Index all resources for nicer lookups
|
||||
nIndex := make(nodeIndex)
|
||||
nIndex.Add(nn...)
|
||||
|
||||
// Build the graph
|
||||
for _, cNode := range nn {
|
||||
refs := cNode.res.Refs()
|
||||
|
||||
// Attempt to connect all available nodes
|
||||
for _, ref := range refs {
|
||||
// Handle wildcard references
|
||||
if ref.IsWildcard() {
|
||||
nn := nIndex.GetResourceType(ref.ResourceType)
|
||||
// Connect the nodes
|
||||
for _, n := range nn {
|
||||
g.addChild(cNode, n)
|
||||
g.addParent(n, cNode)
|
||||
}
|
||||
} else {
|
||||
// Handle regular references
|
||||
rNode := nIndex.GetRef(ref)
|
||||
|
||||
if rNode == nil {
|
||||
if !resource.IgnoreDepResolution(ref) {
|
||||
cNode.missing = append(cNode.missing, ref)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// The resource is available; we can connect the two
|
||||
g.addChild(cNode, rNode)
|
||||
g.addParent(rNode, cNode)
|
||||
}
|
||||
}
|
||||
|
||||
g.addNode(cNode)
|
||||
}
|
||||
|
||||
if b.reportMissingRefs {
|
||||
missing := make(resource.RefSet, 0, 10)
|
||||
for _, n := range g.nn {
|
||||
missing = append(missing, n.missing...)
|
||||
}
|
||||
|
||||
if len(missing) > 0 {
|
||||
g.missingRefs = missing
|
||||
}
|
||||
}
|
||||
|
||||
return g
|
||||
}
|
||||
186
server/pkg/envoy/builder_test.go
Normal file
186
server/pkg/envoy/builder_test.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package envoy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type (
|
||||
testResource struct {
|
||||
resType string
|
||||
identifiers resource.Identifiers
|
||||
refs resource.RefSet
|
||||
ph bool
|
||||
}
|
||||
)
|
||||
|
||||
func (t *testResource) Identifiers() resource.Identifiers {
|
||||
return t.identifiers
|
||||
}
|
||||
|
||||
func (t *testResource) ResourceType() string {
|
||||
return t.resType
|
||||
}
|
||||
|
||||
func (t *testResource) Refs() resource.RefSet {
|
||||
return t.refs
|
||||
}
|
||||
|
||||
func (t *testResource) MarkPlaceholder() {
|
||||
t.ph = true
|
||||
}
|
||||
|
||||
func (t *testResource) Placeholder() bool {
|
||||
return t.ph
|
||||
}
|
||||
|
||||
func (r *testResource) Resource() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *testResource) ReID(ii resource.Identifiers) {
|
||||
// no need for implementation
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (t *testResource) ReRef(old resource.RefSet, new resource.RefSet) {
|
||||
// no need for implementation
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func TestGraphBuilder_Rel(t *testing.T) {
|
||||
req := require.New(t)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("single, simple node; a", func(t *testing.T) {
|
||||
bl := NewBuilder()
|
||||
|
||||
rr := []resource.Interface{
|
||||
&testResource{
|
||||
resType: "test:resource:1:",
|
||||
identifiers: resource.Identifiers{"id1"},
|
||||
refs: nil,
|
||||
},
|
||||
}
|
||||
|
||||
g, err := bl.Build(ctx, rr...)
|
||||
g.invert()
|
||||
req.NoError(err)
|
||||
req.Len(g.nodes(), 1)
|
||||
|
||||
a := g.nodes()[0]
|
||||
req.Empty(g.childNodes(a))
|
||||
req.Empty(g.parentNodes(a))
|
||||
})
|
||||
|
||||
t.Run("simple node link; a -> b", func(t *testing.T) {
|
||||
bl := NewBuilder()
|
||||
|
||||
rr := []resource.Interface{
|
||||
&testResource{
|
||||
resType: "test:resource:1:",
|
||||
identifiers: resource.Identifiers{"id1"},
|
||||
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id2"}}},
|
||||
},
|
||||
&testResource{
|
||||
resType: "test:resource:1:",
|
||||
identifiers: resource.Identifiers{"id2"},
|
||||
refs: nil,
|
||||
},
|
||||
}
|
||||
|
||||
g, err := bl.Build(ctx, rr...)
|
||||
req.NoError(err)
|
||||
req.Len(g.nodes(), 2)
|
||||
|
||||
a := g.resIndex[rr[0]]
|
||||
b := g.resIndex[rr[1]]
|
||||
req.Len(g.childNodes(a), 1)
|
||||
req.Equal(b, g.childNodes(a)[0])
|
||||
req.Empty(g.parentNodes(a))
|
||||
|
||||
req.Len(g.parentNodes(b), 1)
|
||||
req.Equal(a, g.parentNodes(b)[0])
|
||||
})
|
||||
|
||||
t.Run("cyclic node link; a -> b -> a", func(t *testing.T) {
|
||||
bl := NewBuilder()
|
||||
|
||||
rr := []resource.Interface{
|
||||
&testResource{
|
||||
resType: "test:resource:1:",
|
||||
identifiers: resource.Identifiers{"id1"},
|
||||
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id2"}}},
|
||||
},
|
||||
&testResource{
|
||||
resType: "test:resource:1:",
|
||||
identifiers: resource.Identifiers{"id2"},
|
||||
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id1"}}},
|
||||
},
|
||||
}
|
||||
|
||||
g, err := bl.Build(ctx, rr...)
|
||||
g.invert()
|
||||
req.NoError(err)
|
||||
req.Len(g.nodes(), 2)
|
||||
|
||||
a := g.nodes()[0]
|
||||
b := g.nodes()[1]
|
||||
req.Len(g.childNodes(a), 1)
|
||||
req.Equal(b, g.childNodes(a)[0])
|
||||
req.Len(g.parentNodes(a), 1)
|
||||
req.Equal(b, g.parentNodes(a)[0])
|
||||
|
||||
req.Len(b.cc, 1)
|
||||
req.Equal(a, b.cc[0])
|
||||
req.Len(b.pp, 1)
|
||||
req.Equal(a, b.pp[0])
|
||||
})
|
||||
|
||||
t.Run("node with missing dep; a -> nill", func(t *testing.T) {
|
||||
bl := NewBuilder()
|
||||
|
||||
rr := []resource.Interface{
|
||||
&testResource{
|
||||
resType: "test:resource:1:",
|
||||
identifiers: resource.Identifiers{"id1"},
|
||||
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id2"}}},
|
||||
},
|
||||
}
|
||||
|
||||
g, err := bl.Build(ctx, rr...)
|
||||
g.invert()
|
||||
req.NoError(err)
|
||||
req.Len(g.nodes(), 1)
|
||||
|
||||
a := g.nodes()[0]
|
||||
req.Len(g.childNodes(a), 0)
|
||||
req.Len(g.parentNodes(a), 0)
|
||||
})
|
||||
|
||||
t.Run("self-cycle; a -> a", func(t *testing.T) {
|
||||
bl := NewBuilder()
|
||||
|
||||
rr := []resource.Interface{
|
||||
&testResource{
|
||||
resType: "test:resource:1:",
|
||||
identifiers: resource.Identifiers{"id1"},
|
||||
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id1"}}},
|
||||
},
|
||||
}
|
||||
|
||||
g, err := bl.Build(ctx, rr...)
|
||||
g.invert()
|
||||
req.NoError(err)
|
||||
req.Len(g.nodes(), 1)
|
||||
|
||||
a := g.nodes()[0]
|
||||
req.Len(g.childNodes(a), 1)
|
||||
req.Equal(g.childNodes(a)[0], a)
|
||||
req.Len(g.parentNodes(a), 1)
|
||||
req.Equal(g.parentNodes(a)[0], a)
|
||||
})
|
||||
}
|
||||
162
server/pkg/envoy/csv/bulk_compose_record_marshal.go
Normal file
162
server/pkg/envoy/csv/bulk_compose_record_marshal.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package csv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
)
|
||||
|
||||
type (
|
||||
bulkComposeRecordEncoder struct {
|
||||
encoderConfig *EncoderConfig
|
||||
|
||||
res *resource.ComposeRecord
|
||||
|
||||
refNs string
|
||||
|
||||
refMod string
|
||||
relMod *types.Module
|
||||
}
|
||||
)
|
||||
|
||||
func bulkComposeRecordEncoderFromResource(rec *resource.ComposeRecord, cfg *EncoderConfig) *bulkComposeRecordEncoder {
|
||||
return &bulkComposeRecordEncoder{
|
||||
encoderConfig: cfg,
|
||||
|
||||
res: rec,
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare prepares the composeRecord to be encoded
|
||||
//
|
||||
// Any validation, additional constraining should be performed here.
|
||||
func (n *bulkComposeRecordEncoder) Prepare(ctx context.Context, state *envoy.ResourceState) (err error) {
|
||||
_, ok := state.Res.(*resource.ComposeRecord)
|
||||
if !ok {
|
||||
return encoderErrInvalidResource(types.RecordResourceType, state.Res.ResourceType())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Encode encodes the composeRecord to the document
|
||||
//
|
||||
// Encode is allowed to do some data manipulation, but no resource constraints
|
||||
// should be changed.
|
||||
func (n *bulkComposeRecordEncoder) Encode(ctx context.Context, w io.Writer, state *envoy.ResourceState) (err error) {
|
||||
enc := csv.NewWriter(w)
|
||||
all := len(n.encoderConfig.Fields) == 0
|
||||
|
||||
// Generate header & cell index
|
||||
hh := make([]string, 0, 100)
|
||||
hh = append(hh, "id")
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
fx := make(map[string]int)
|
||||
// 1 sys fields
|
||||
sysFields := []string{"ownedBy", "createdAt", "createdBy", "updatedAt", "updatedBy", "deletedAt", "deletedBy"}
|
||||
for _, sf := range sysFields {
|
||||
if all || n.encoderConfig.Fields[sf] {
|
||||
fx[sf] = len(hh)
|
||||
hh = append(hh, sf)
|
||||
}
|
||||
}
|
||||
|
||||
// 2 modlue fields
|
||||
for _, f := range n.res.RelMod.Fields {
|
||||
if all || n.encoderConfig.Fields[f.Name] {
|
||||
fx[f.Name] = len(hh)
|
||||
hh = append(hh, f.Name)
|
||||
}
|
||||
}
|
||||
enc.Write(hh)
|
||||
|
||||
err = n.res.Walker(func(r *resource.ComposeRecordRaw) error {
|
||||
|
||||
row := make([]string, len(hh))
|
||||
|
||||
var err error
|
||||
row[0] = r.ID
|
||||
|
||||
if r.Us != nil {
|
||||
if r.Us.OwnedBy != nil && (all || n.encoderConfig.Fields["ownedBy"]) {
|
||||
row[fx["ownedBy"]], err = n.res.UserFlakes.GetByStamp(r.Us.OwnedBy).Model()
|
||||
}
|
||||
if r.Us.CreatedBy != nil && (all || n.encoderConfig.Fields["createdBy"]) {
|
||||
row[fx["createdBy"]], err = n.res.UserFlakes.GetByStamp(r.Us.CreatedBy).Model()
|
||||
}
|
||||
if r.Us.UpdatedBy != nil && (all || n.encoderConfig.Fields["updatedBy"]) {
|
||||
row[fx["updatedBy"]], err = n.res.UserFlakes.GetByStamp(r.Us.UpdatedBy).Model()
|
||||
}
|
||||
if r.Us.DeletedBy != nil && (all || n.encoderConfig.Fields["deletedBy"]) {
|
||||
row[fx["deletedBy"]], err = n.res.UserFlakes.GetByStamp(r.Us.DeletedBy).Model()
|
||||
}
|
||||
}
|
||||
|
||||
if r.Ts != nil {
|
||||
r.Ts, err = r.Ts.Model(n.encoderConfig.TimeLayout, n.encoderConfig.Timezone)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r.Ts.CreatedAt != nil && (all || n.encoderConfig.Fields["createdAt"]) {
|
||||
row[fx["createdAt"]] = r.Ts.CreatedAt.S
|
||||
}
|
||||
if r.Ts.UpdatedAt != nil && (all || n.encoderConfig.Fields["updatedAt"]) {
|
||||
row[fx["updatedAt"]] = r.Ts.UpdatedAt.S
|
||||
}
|
||||
if r.Ts.DeletedAt != nil && (all || n.encoderConfig.Fields["deletedAt"]) {
|
||||
row[fx["deletedAt"]] = r.Ts.DeletedAt.S
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for k, v := range r.Values {
|
||||
// Skip fields we don't want
|
||||
if !all && !n.encoderConfig.Fields[k] {
|
||||
continue
|
||||
}
|
||||
|
||||
cell, has := fx[k]
|
||||
if !has {
|
||||
return fmt.Errorf("unknown cell %s", k)
|
||||
}
|
||||
f := n.res.RelMod.Fields.FindByName(k)
|
||||
if f == nil {
|
||||
return fmt.Errorf("field %s not found", k)
|
||||
}
|
||||
|
||||
if f.Kind == "User" {
|
||||
row[cell], err = n.res.UserFlakes.GetByKey(pickLast(v)).Model()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
row[cell] = pickLast(v)
|
||||
}
|
||||
}
|
||||
return enc.Write(row)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
enc.Flush()
|
||||
return nil
|
||||
}
|
||||
123
server/pkg/envoy/csv/decoder.go
Normal file
123
server/pkg/envoy/csv/decoder.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package csv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
)
|
||||
|
||||
type (
|
||||
// wrapper struct for csv related methods
|
||||
decoder struct{}
|
||||
|
||||
// csv decoder wrapper for additional bits
|
||||
reader struct {
|
||||
header []string
|
||||
count uint64
|
||||
|
||||
cache []map[string]string
|
||||
cacheIndex int
|
||||
}
|
||||
)
|
||||
|
||||
// Decoder initializes and returns a fresh CSV decoder
|
||||
func Decoder() *decoder {
|
||||
return &decoder{}
|
||||
}
|
||||
|
||||
// CanDecodeFile determines if the file can be determined by this decoder
|
||||
func (y *decoder) CanDecodeFile(f io.Reader) bool {
|
||||
m, err := mimetype.DetectReader(f)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return y.CanDecodeExt(m.Extension())
|
||||
}
|
||||
|
||||
func (y *decoder) CanDecodeMime(m string) bool {
|
||||
return m == "text/csv"
|
||||
}
|
||||
|
||||
func (y *decoder) CanDecodeExt(ext string) bool {
|
||||
pt := strings.Split(ext, ".")
|
||||
return strings.TrimSpace(pt[len(pt)-1]) == "csv"
|
||||
}
|
||||
|
||||
// Decode decodes the given io.Reader into a generic resource dataset
|
||||
func (c *decoder) Decode(ctx context.Context, r io.Reader, do *envoy.DecoderOpts) ([]resource.Interface, error) {
|
||||
cr := &reader{
|
||||
cacheIndex: 0,
|
||||
cache: make([]map[string]string, 0, 1000),
|
||||
}
|
||||
|
||||
err := cr.prepare(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []resource.Interface{resource.NewResourceDataset(do.Name, cr)}, nil
|
||||
}
|
||||
|
||||
// The prepare step caches all of the rows into a cache array that will be used
|
||||
// to access already visited CSV rows.
|
||||
//
|
||||
// @todo implement FS cache file for larger files
|
||||
func (cr *reader) prepare(r io.Reader) (err error) {
|
||||
cReader := csv.NewReader(r)
|
||||
|
||||
// Header
|
||||
cr.header, err = cReader.Read()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
aux := make(map[string]string)
|
||||
rr, err := cReader.Read()
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Entry count
|
||||
cr.count++
|
||||
|
||||
// Cache entry
|
||||
for i, h := range cr.header {
|
||||
aux[h] = rr[i]
|
||||
}
|
||||
cr.cache = append(cr.cache, aux)
|
||||
}
|
||||
}
|
||||
|
||||
// Fields returns every available field in this dataset
|
||||
func (cr *reader) Fields() []string {
|
||||
return cr.header
|
||||
}
|
||||
|
||||
func (cr *reader) Reset() error {
|
||||
cr.cacheIndex = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
// Next returns the field: value mapping for the next row
|
||||
func (cr *reader) Next() (map[string]string, error) {
|
||||
if cr.cacheIndex >= len(cr.cache) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
mr := cr.cache[cr.cacheIndex]
|
||||
cr.cacheIndex++
|
||||
return mr, nil
|
||||
}
|
||||
|
||||
func (cr *reader) Count() uint64 {
|
||||
return cr.count
|
||||
}
|
||||
58
server/pkg/envoy/csv/decoder_test.go
Normal file
58
server/pkg/envoy/csv/decoder_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package csv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func parseDocument(ctx context.Context, name string) (*resource.ResourceDataset, error) {
|
||||
path := fmt.Sprintf("testdata/%s.csv", name)
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
cd := Decoder()
|
||||
ii, err := cd.Decode(ctx, f, &envoy.DecoderOpts{
|
||||
Name: name,
|
||||
Path: path,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rd, _ := ii[0].(*resource.ResourceDataset)
|
||||
return rd, nil
|
||||
}
|
||||
|
||||
func TestDecoder(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("doc 1", func(t *testing.T) {
|
||||
req := require.New(t)
|
||||
|
||||
ds, err := parseDocument(ctx, "records_1")
|
||||
req.NoError(err)
|
||||
req.NotNil(ds)
|
||||
req.Equal(resource.MakeIdentifiers("records_1"), ds.Identifiers())
|
||||
req.Equal([]string{"id", "c1", "c2", "c3"}, ds.P.Fields())
|
||||
req.Equal(uint64(3), ds.P.Count())
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
n, err := ds.P.Next()
|
||||
req.NoError(err)
|
||||
req.NotNil(n)
|
||||
|
||||
req.Len(n, 4)
|
||||
}
|
||||
n, err := ds.P.Next()
|
||||
req.Nil(n)
|
||||
req.Nil(err)
|
||||
})
|
||||
}
|
||||
163
server/pkg/envoy/csv/encoder.go
Normal file
163
server/pkg/envoy/csv/encoder.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package csv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
)
|
||||
|
||||
type (
|
||||
bulkRecordEncoder struct {
|
||||
cfg *EncoderConfig
|
||||
|
||||
resState map[resource.Interface]*encoderState
|
||||
}
|
||||
|
||||
encoderState struct {
|
||||
res resourceState
|
||||
source io.ReadWriter
|
||||
resourceType string
|
||||
Scope string
|
||||
identifier string
|
||||
}
|
||||
|
||||
// EncoderConfig allows us to configure the resource encoding process
|
||||
EncoderConfig struct {
|
||||
// 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
|
||||
// Fields specifies what fields we wish to include in the export
|
||||
Fields map[string]bool
|
||||
}
|
||||
|
||||
// resourceState holds some intermedia values to help with encoding
|
||||
resourceState interface {
|
||||
Prepare(ctx context.Context, state *envoy.ResourceState) (err error)
|
||||
Encode(ctx context.Context, w io.Writer, state *envoy.ResourceState) (err error)
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnknownResource = fmt.Errorf("unknown resource")
|
||||
ErrResourceStateUndefined = fmt.Errorf("undefined resource state")
|
||||
)
|
||||
|
||||
func NewBulkRecordEncoder(cfg *EncoderConfig) envoy.PrepareEncodeStreamer {
|
||||
if cfg == nil {
|
||||
cfg = &EncoderConfig{}
|
||||
}
|
||||
|
||||
return &bulkRecordEncoder{
|
||||
cfg: cfg,
|
||||
|
||||
resState: make(map[resource.Interface]*encoderState),
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare prepares the encoder for the given set of resources
|
||||
//
|
||||
// It initializes and prepares the resource state for each provided resource
|
||||
func (se *bulkRecordEncoder) Prepare(ctx context.Context, ee ...*envoy.ResourceState) (err error) {
|
||||
f := func(rs resourceState, es *envoy.ResourceState) error {
|
||||
err = rs.Prepare(ctx, es)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
se.resState[es.Res] = &encoderState{
|
||||
res: rs,
|
||||
source: &bytes.Buffer{},
|
||||
resourceType: es.Res.ResourceType(),
|
||||
identifier: es.Res.Identifiers().First(),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, e := range ee {
|
||||
switch res := e.Res.(type) {
|
||||
// @todo other resources; we'll only do records for now
|
||||
case *resource.ComposeRecord:
|
||||
err = f(bulkComposeRecordEncoderFromResource(res, se.cfg), e)
|
||||
|
||||
default:
|
||||
err = ErrUnknownResource
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return se.WrapError("prepare", e.Res, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Encode encodes the resources into a series of csv documents
|
||||
func (se *bulkRecordEncoder) 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
|
||||
}
|
||||
|
||||
state := se.resState[e.Res]
|
||||
if state == nil {
|
||||
err = ErrResourceStateUndefined
|
||||
} else {
|
||||
err = state.res.Encode(ctx, state.source, e)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return se.WrapError("encode: build doc", e.Res, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, s := range se.resState {
|
||||
s.res = nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (se *bulkRecordEncoder) Stream() []*envoy.Stream {
|
||||
ss := make([]*envoy.Stream, 0, 20)
|
||||
|
||||
for _, s := range se.resState {
|
||||
ss = append(ss, &envoy.Stream{
|
||||
Resource: s.resourceType,
|
||||
Identifier: s.identifier,
|
||||
Source: s.source,
|
||||
})
|
||||
}
|
||||
|
||||
return ss
|
||||
}
|
||||
|
||||
// WrapError wraps errors related to csv encoding
|
||||
//
|
||||
// Always wrap your errors.
|
||||
func (se *bulkRecordEncoder) WrapError(act string, res resource.Interface, err error) error {
|
||||
rt := strings.Join(strings.Split(strings.TrimSpace(strings.TrimRight(res.ResourceType(), ":")), ":"), " ")
|
||||
return fmt.Errorf("csv 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)
|
||||
}
|
||||
4
server/pkg/envoy/csv/testdata/records_1.csv
vendored
Normal file
4
server/pkg/envoy/csv/testdata/records_1.csv
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
id,c1,c2,c3
|
||||
1,c1.v1,c2.v1,c3.v1
|
||||
2,c1.v2,c2.v2,c3.v2
|
||||
3,c1.v3,c2.v3,c3.v3
|
||||
|
126
server/pkg/envoy/csv/util_encode.go
Normal file
126
server/pkg/envoy/csv/util_encode.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package csv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
)
|
||||
|
||||
type (
|
||||
seqNode []interface{}
|
||||
mapNode map[string]interface{}
|
||||
)
|
||||
|
||||
// mapTimestamps helper encodes Timestamps into the mapping node
|
||||
func mapTimestamps(n mapNode, ts *resource.Timestamps) (mapNode, 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 mapUserstamps(n mapNode, us *resource.Userstamps) (mapNode, error) {
|
||||
if us == nil {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
return addMap(n,
|
||||
"createdBy", us.CreatedBy,
|
||||
"updatedBy", us.UpdatedBy,
|
||||
"deletedBy", us.DeletedBy,
|
||||
"ownedBy", us.OwnedBy,
|
||||
)
|
||||
}
|
||||
|
||||
// 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 mapNode) mapNode {
|
||||
m := make(mapNode)
|
||||
|
||||
for k, v := range n {
|
||||
if k != "" && v != nil {
|
||||
m[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// 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{}) (mapNode, error) {
|
||||
return addMap(make(mapNode), 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 mapNode, pp ...interface{}) (mapNode, 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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
switch v := vRaw.(type) {
|
||||
case string:
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
case bool:
|
||||
if !v {
|
||||
continue
|
||||
}
|
||||
case int, uint,
|
||||
int32, uint32,
|
||||
int64, uint64:
|
||||
if v == 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
n[k] = vRaw
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// makeSeq creates a new sequence node based on the provided items
|
||||
func makeSeq(vv ...interface{}) (seqNode, error) {
|
||||
return addSeq(make(seqNode, 0, len(vv)), vv...)
|
||||
}
|
||||
|
||||
// addSeq adds new items to the sequence node
|
||||
func addSeq(n seqNode, vv ...interface{}) (seqNode, error) {
|
||||
for _, vRaw := range vv {
|
||||
if vRaw != "" && vRaw != nil {
|
||||
n = append(n, vRaw)
|
||||
}
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
9
server/pkg/envoy/decoder.go
Normal file
9
server/pkg/envoy/decoder.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package envoy
|
||||
|
||||
type (
|
||||
// DecoderOpts provides additional context for source decoders
|
||||
DecoderOpts struct {
|
||||
Name string
|
||||
Path string
|
||||
}
|
||||
)
|
||||
91
server/pkg/envoy/directory/decode.go
Normal file
91
server/pkg/envoy/directory/decode.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package directory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
)
|
||||
|
||||
type (
|
||||
Decoder interface {
|
||||
CanDecodeExt(string) bool
|
||||
CanDecodeFile(io.Reader) bool
|
||||
Decode(context.Context, io.Reader, *envoy.DecoderOpts) ([]resource.Interface, error)
|
||||
}
|
||||
)
|
||||
|
||||
// DecodeDirectory is a helper to run the decoding process over the entire directory
|
||||
func Decode(ctx context.Context, p string, decoders ...Decoder) ([]resource.Interface, error) {
|
||||
var (
|
||||
f *os.File
|
||||
|
||||
// decoded nodes
|
||||
dnn []resource.Interface
|
||||
|
||||
// agregated resources
|
||||
nn = make([]resource.Interface, 0, 100)
|
||||
)
|
||||
|
||||
if len(decoders) == 0 {
|
||||
return nil, fmt.Errorf("no decoders provided")
|
||||
}
|
||||
|
||||
return nn, filepath.Walk(p, func(p string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if f, err = os.Open(p); err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
for _, d := range decoders {
|
||||
if !d.CanDecodeFile(f) {
|
||||
// decoder cannot handle this file
|
||||
// Make sure to reset it, as the above check consumes the reader
|
||||
if _, err = f.Seek(0, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if !d.CanDecodeExt(info.Name()) {
|
||||
// this decoder cannot handle this extension
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err = f.Seek(0, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dir, fn := path.Split(p)
|
||||
do := &envoy.DecoderOpts{
|
||||
Name: fn,
|
||||
Path: dir,
|
||||
}
|
||||
|
||||
if dnn, err = d.Decode(ctx, f, do); err != nil {
|
||||
return fmt.Errorf("failed to decode %s: %w", info.Name(), err)
|
||||
}
|
||||
|
||||
nn = append(nn, dnn...)
|
||||
|
||||
// found compatible decoder
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
18
server/pkg/envoy/encoder.go
Normal file
18
server/pkg/envoy/encoder.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package envoy
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type (
|
||||
Provider interface {
|
||||
NextInverted(ctx context.Context) (*ResourceState, error)
|
||||
}
|
||||
)
|
||||
|
||||
// @todo errors!
|
||||
func Encode(ctx context.Context, p Provider, e Encoder) error {
|
||||
// @todo add support for multiple encoders at the same time.
|
||||
// The issue occurs with routines and error handling...
|
||||
return e.Encode(ctx, p)
|
||||
}
|
||||
302
server/pkg/envoy/graph.go
Normal file
302
server/pkg/envoy/graph.go
Normal file
@@ -0,0 +1,302 @@
|
||||
package envoy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
)
|
||||
|
||||
type (
|
||||
nodeMap map[*node]bool
|
||||
|
||||
graph struct {
|
||||
nn nodeSet
|
||||
resIndex map[resource.Interface]*node
|
||||
|
||||
missingRefs resource.RefSet
|
||||
|
||||
// Config flags
|
||||
inverted bool
|
||||
|
||||
processed nodeMap
|
||||
conflicting nodeMap
|
||||
}
|
||||
|
||||
ResourceState struct {
|
||||
Res resource.Interface
|
||||
Conflicting bool
|
||||
DepResources []resource.Interface
|
||||
ParentResources []resource.Interface
|
||||
}
|
||||
)
|
||||
|
||||
func newGraph() *graph {
|
||||
return &graph{
|
||||
nn: make(nodeSet, 0, 100),
|
||||
resIndex: make(map[resource.Interface]*node),
|
||||
inverted: false,
|
||||
processed: make(nodeMap),
|
||||
conflicting: make(nodeMap),
|
||||
}
|
||||
}
|
||||
|
||||
func (g *graph) MissingDeps() resource.RefSet {
|
||||
return g.missingRefs
|
||||
}
|
||||
|
||||
func (g *graph) addNode(nn ...*node) {
|
||||
for _, n := range nn {
|
||||
g.resIndex[n.res] = n
|
||||
}
|
||||
g.nn = g.nn.add(nn...)
|
||||
}
|
||||
|
||||
func (g *graph) removeNode(nn ...*node) {
|
||||
g.nn = g.nn.remove(nn...)
|
||||
for _, n := range nn {
|
||||
g.removeChild(n)
|
||||
g.removeParent(n)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *graph) invert() {
|
||||
g.inverted = !g.inverted
|
||||
}
|
||||
|
||||
func (g *graph) childNodes(n *node) nodeSet {
|
||||
if g.inverted {
|
||||
return n.pp
|
||||
}
|
||||
|
||||
return n.cc
|
||||
}
|
||||
|
||||
func (g *graph) parentNodes(n *node) nodeSet {
|
||||
if g.inverted {
|
||||
return n.cc
|
||||
}
|
||||
|
||||
return n.pp
|
||||
}
|
||||
|
||||
func (g *graph) nodes() nodeSet {
|
||||
return g.nn
|
||||
}
|
||||
|
||||
func (g *graph) markProcessed(n *node) {
|
||||
g.processed = g.processed.add(n)
|
||||
}
|
||||
func (g *graph) markConflicting(n *node) {
|
||||
g.conflicting = g.conflicting.add(n)
|
||||
}
|
||||
|
||||
func (g *graph) NextInverted(ctx context.Context) (s *ResourceState, err error) {
|
||||
g.inverted = true
|
||||
defer func() {
|
||||
g.inverted = false
|
||||
}()
|
||||
|
||||
return g.Next(ctx)
|
||||
}
|
||||
|
||||
func (g *graph) Next(ctx context.Context) (s *ResourceState, err error) {
|
||||
upNN := g.removeProcessed(g.nodes())
|
||||
|
||||
// We are done here
|
||||
if len(upNN) <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var nx *node
|
||||
|
||||
for _, upN := range upNN {
|
||||
// We should not take into account conflicted parent nodes,
|
||||
// as they already resolved the conflict.
|
||||
if len(g.removeProcessed(g.removeConflicting(g.parentNodes(upN)))) <= 0 {
|
||||
nx = upN
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if nx != nil {
|
||||
// Prepare the required context for the processing.
|
||||
// Perform some basic cleanup.
|
||||
es := g.prepExecState(nx, false)
|
||||
g.markProcessed(nx)
|
||||
|
||||
return es, nil
|
||||
}
|
||||
|
||||
// There are only conflicting nodes
|
||||
// Try to get a cycle node to resolve the conflict
|
||||
nx = g.findCycleNode(g.removeConflicting(upNN))
|
||||
|
||||
if nx == nil {
|
||||
// This is basically impossible, unless I've messed up the algorithm
|
||||
return nil, fmt.Errorf("could not determine non-conflicting node")
|
||||
}
|
||||
|
||||
// Prepare the required context for the processing.
|
||||
es := g.prepExecState(nx, true)
|
||||
g.markConflicting(nx)
|
||||
|
||||
return es, nil
|
||||
}
|
||||
|
||||
// findCycleNode returns the first graph node that caused a cycle
|
||||
//
|
||||
// General outline:
|
||||
// - DFS from a start node(s)
|
||||
// - if a child node is already in path, return that node
|
||||
// - else return nil and cleanup the path until the first node with
|
||||
// unprocessed child nodes
|
||||
//
|
||||
// @note we could complicate this further by doing cycle enumeration algorithms.
|
||||
//
|
||||
// I might do it when no one is watching :)
|
||||
func (g *graph) findCycleNode(nn nodeSet) *node {
|
||||
path := make(nodeMap)
|
||||
processed := make(nodeMap)
|
||||
|
||||
for _, n := range nn {
|
||||
if !processed.has(n) {
|
||||
m := g.traverse(n, path, processed)
|
||||
if m != nil {
|
||||
return m
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *graph) traverse(n *node, path, processed nodeMap) *node {
|
||||
processed.add(n)
|
||||
|
||||
// Found ourselves a cycle
|
||||
if path.has(n) {
|
||||
return n
|
||||
}
|
||||
cnn := g.removeProcessed(g.removeConflicting(g.childNodes(n)))
|
||||
// Nothing else to look at
|
||||
if len(cnn) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
path.add(n)
|
||||
|
||||
for _, c := range cnn {
|
||||
m := g.traverse(c, path, processed)
|
||||
if m != nil {
|
||||
return m
|
||||
}
|
||||
}
|
||||
|
||||
path.remove(n)
|
||||
return nil
|
||||
}
|
||||
|
||||
// util
|
||||
|
||||
func (g *graph) removeProcessed(nn nodeSet) nodeSet {
|
||||
mm := make(nodeSet, 0, len(nn))
|
||||
|
||||
for _, n := range nn {
|
||||
if !g.processed.has(n) {
|
||||
mm = mm.add(n)
|
||||
}
|
||||
}
|
||||
|
||||
return mm
|
||||
}
|
||||
|
||||
func (g *graph) removeConflicting(nn nodeSet) nodeSet {
|
||||
mm := make(nodeSet, 0, len(nn))
|
||||
|
||||
for _, n := range nn {
|
||||
if !g.conflicting.has(n) {
|
||||
mm = mm.add(n)
|
||||
}
|
||||
}
|
||||
|
||||
return mm
|
||||
}
|
||||
|
||||
func (g *graph) addChild(n *node, mm ...*node) {
|
||||
if g.inverted {
|
||||
n.pp = n.pp.add(mm...)
|
||||
} else {
|
||||
n.cc = n.cc.add(mm...)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *graph) removeChild(n *node, mm ...*node) {
|
||||
if len(mm) <= 0 {
|
||||
if g.inverted {
|
||||
n.pp = make(nodeSet, 0, 10)
|
||||
} else {
|
||||
n.cc = make(nodeSet, 0, 10)
|
||||
}
|
||||
} else {
|
||||
if g.inverted {
|
||||
n.pp = n.pp.remove(mm...)
|
||||
} else {
|
||||
n.cc = n.cc.remove(mm...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *graph) addParent(n *node, mm ...*node) {
|
||||
if g.inverted {
|
||||
n.cc = n.cc.add(mm...)
|
||||
} else {
|
||||
n.pp = n.pp.add(mm...)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *graph) removeParent(n *node, mm ...*node) {
|
||||
if len(mm) <= 0 {
|
||||
if g.inverted {
|
||||
n.cc = make(nodeSet, 0, 10)
|
||||
} else {
|
||||
n.pp = make(nodeSet, 0, 10)
|
||||
}
|
||||
} else {
|
||||
if g.inverted {
|
||||
n.cc = n.cc.remove(mm...)
|
||||
} else {
|
||||
n.pp = n.pp.remove(mm...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *graph) nodeResource(nn ...*node) []resource.Interface {
|
||||
rr := make([]resource.Interface, 0, len(nn))
|
||||
for _, n := range nn {
|
||||
rr = append(rr, n.res)
|
||||
}
|
||||
|
||||
return rr
|
||||
}
|
||||
|
||||
func (g *graph) prepExecState(n *node, conflicting bool) *ResourceState {
|
||||
return &ResourceState{
|
||||
Res: n.res,
|
||||
DepResources: g.nodeResource(g.childNodes(n)...),
|
||||
ParentResources: g.nodeResource(g.parentNodes(n)...),
|
||||
Conflicting: conflicting,
|
||||
}
|
||||
}
|
||||
|
||||
func (nm nodeMap) add(n *node) nodeMap {
|
||||
nm[n] = true
|
||||
return nm
|
||||
}
|
||||
func (nm nodeMap) has(n *node) bool {
|
||||
return nm[n]
|
||||
}
|
||||
func (nm nodeMap) remove(n *node) nodeMap {
|
||||
delete(nm, n)
|
||||
return nm
|
||||
}
|
||||
429
server/pkg/envoy/graph_test.go
Normal file
429
server/pkg/envoy/graph_test.go
Normal file
@@ -0,0 +1,429 @@
|
||||
package envoy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGraph_Walk(t *testing.T) {
|
||||
req := require.New(t)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("simple node link; a -> b", func(t *testing.T) {
|
||||
bl := NewBuilder()
|
||||
|
||||
a := &testResource{
|
||||
resType: "test:resource:1:",
|
||||
identifiers: resource.Identifiers{"id1"},
|
||||
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id2"}}},
|
||||
}
|
||||
|
||||
b := &testResource{
|
||||
resType: "test:resource:1:",
|
||||
identifiers: resource.Identifiers{"id2"},
|
||||
refs: nil,
|
||||
}
|
||||
|
||||
rr := []resource.Interface{
|
||||
a,
|
||||
b,
|
||||
}
|
||||
|
||||
g, err := bl.Build(ctx, rr...)
|
||||
req.NoError(err)
|
||||
g.invert()
|
||||
|
||||
s, err := g.Next(ctx)
|
||||
req.Equal(b, s.Res)
|
||||
req.False(s.Conflicting)
|
||||
|
||||
s, err = g.Next(ctx)
|
||||
req.Equal(a, s.Res)
|
||||
req.False(s.Conflicting)
|
||||
})
|
||||
|
||||
t.Run("cyclic node link; a -> b -> a", func(t *testing.T) {
|
||||
bl := NewBuilder()
|
||||
|
||||
a := &testResource{
|
||||
resType: "test:resource:1:",
|
||||
identifiers: resource.Identifiers{"id1"},
|
||||
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id2"}}},
|
||||
}
|
||||
|
||||
b := &testResource{
|
||||
resType: "test:resource:1:",
|
||||
identifiers: resource.Identifiers{"id2"},
|
||||
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id1"}}},
|
||||
}
|
||||
|
||||
rr := []resource.Interface{
|
||||
a,
|
||||
b,
|
||||
}
|
||||
|
||||
g, err := bl.Build(ctx, rr...)
|
||||
req.NoError(err)
|
||||
g.invert()
|
||||
|
||||
s, err := g.Next(ctx)
|
||||
req.Equal(a, s.Res)
|
||||
req.True(s.Conflicting)
|
||||
|
||||
s, err = g.Next(ctx)
|
||||
req.Equal(b, s.Res)
|
||||
req.False(s.Conflicting)
|
||||
|
||||
s, err = g.Next(ctx)
|
||||
req.Equal(a, s.Res)
|
||||
req.False(s.Conflicting)
|
||||
})
|
||||
|
||||
t.Run("self-cycle; a -> a", func(t *testing.T) {
|
||||
bl := NewBuilder()
|
||||
|
||||
a := &testResource{
|
||||
resType: "test:resource:1:",
|
||||
identifiers: resource.Identifiers{"id1"},
|
||||
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id1"}}},
|
||||
}
|
||||
|
||||
rr := []resource.Interface{
|
||||
a,
|
||||
}
|
||||
|
||||
g, err := bl.Build(ctx, rr...)
|
||||
req.NoError(err)
|
||||
g.invert()
|
||||
|
||||
s, err := g.Next(ctx)
|
||||
req.Equal(a, s.Res)
|
||||
req.True(s.Conflicting)
|
||||
|
||||
s, err = g.Next(ctx)
|
||||
req.Equal(a, s.Res)
|
||||
req.False(s.Conflicting)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGraph_WalkFlags(t *testing.T) {
|
||||
req := require.New(t)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("simple node link; a -> b -> c -> c", func(t *testing.T) {
|
||||
bl := NewBuilder()
|
||||
|
||||
a := &testResource{
|
||||
resType: "test:resource:1:",
|
||||
identifiers: resource.Identifiers{"id1"},
|
||||
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id2"}}},
|
||||
}
|
||||
|
||||
b := &testResource{
|
||||
resType: "test:resource:1:",
|
||||
identifiers: resource.Identifiers{"id2"},
|
||||
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id3"}}},
|
||||
}
|
||||
c := &testResource{
|
||||
resType: "test:resource:1:",
|
||||
identifiers: resource.Identifiers{"id3"},
|
||||
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id3"}}},
|
||||
}
|
||||
|
||||
rr := []resource.Interface{
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
}
|
||||
|
||||
g, err := bl.Build(ctx, rr...)
|
||||
req.NoError(err)
|
||||
|
||||
s, err := g.NextInverted(ctx)
|
||||
req.NoError(err)
|
||||
req.Equal(c, s.Res)
|
||||
req.True(s.Conflicting)
|
||||
|
||||
s, err = g.NextInverted(ctx)
|
||||
req.NoError(err)
|
||||
req.Equal(b, s.Res)
|
||||
req.False(s.Conflicting)
|
||||
|
||||
s, err = g.NextInverted(ctx)
|
||||
req.NoError(err)
|
||||
req.Equal(a, s.Res)
|
||||
req.False(s.Conflicting)
|
||||
|
||||
s, err = g.NextInverted(ctx)
|
||||
req.NoError(err)
|
||||
req.Equal(c, s.Res)
|
||||
req.False(s.Conflicting)
|
||||
|
||||
s, err = g.NextInverted(ctx)
|
||||
req.NoError(err)
|
||||
req.Nil(s)
|
||||
})
|
||||
|
||||
t.Run("simple node link; a -> b -> c -> d -> b; a -> b -> e", func(t *testing.T) {
|
||||
bl := NewBuilder()
|
||||
|
||||
a := &testResource{
|
||||
resType: "test:resource:1:",
|
||||
identifiers: resource.Identifiers{"id1"},
|
||||
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id2"}}},
|
||||
}
|
||||
|
||||
b := &testResource{
|
||||
resType: "test:resource:1:",
|
||||
identifiers: resource.Identifiers{"id2"},
|
||||
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id3"}}, &resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id5"}}},
|
||||
}
|
||||
c := &testResource{
|
||||
resType: "test:resource:1:",
|
||||
identifiers: resource.Identifiers{"id3"},
|
||||
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id4"}}},
|
||||
}
|
||||
d := &testResource{
|
||||
resType: "test:resource:1:",
|
||||
identifiers: resource.Identifiers{"id4"},
|
||||
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id2"}}},
|
||||
}
|
||||
e := &testResource{
|
||||
resType: "test:resource:1:",
|
||||
identifiers: resource.Identifiers{"id5"},
|
||||
refs: nil,
|
||||
}
|
||||
|
||||
rr := []resource.Interface{
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
d,
|
||||
e,
|
||||
}
|
||||
|
||||
g, err := bl.Build(ctx, rr...)
|
||||
req.NoError(err)
|
||||
|
||||
s, err := g.NextInverted(ctx)
|
||||
req.NoError(err)
|
||||
req.Equal(e, s.Res)
|
||||
req.False(s.Conflicting)
|
||||
|
||||
s, err = g.NextInverted(ctx)
|
||||
req.NoError(err)
|
||||
req.Equal(b, s.Res)
|
||||
req.True(s.Conflicting)
|
||||
|
||||
s, err = g.NextInverted(ctx)
|
||||
req.NoError(err)
|
||||
req.Equal(a, s.Res)
|
||||
req.False(s.Conflicting)
|
||||
|
||||
s, err = g.NextInverted(ctx)
|
||||
req.NoError(err)
|
||||
req.Equal(d, s.Res)
|
||||
req.False(s.Conflicting)
|
||||
|
||||
s, err = g.NextInverted(ctx)
|
||||
req.NoError(err)
|
||||
req.Equal(c, s.Res)
|
||||
req.False(s.Conflicting)
|
||||
|
||||
s, err = g.NextInverted(ctx)
|
||||
req.NoError(err)
|
||||
req.Equal(b, s.Res)
|
||||
req.False(s.Conflicting)
|
||||
|
||||
s, err = g.NextInverted(ctx)
|
||||
req.NoError(err)
|
||||
req.Nil(s)
|
||||
})
|
||||
|
||||
t.Run("simple node link; a -> b -> c -> d -> b; a -> b -> c -> e -> b", func(t *testing.T) {
|
||||
bl := NewBuilder()
|
||||
|
||||
a := &testResource{
|
||||
resType: "test:resource:1:",
|
||||
identifiers: resource.Identifiers{"id1"},
|
||||
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id2"}}},
|
||||
}
|
||||
|
||||
b := &testResource{
|
||||
resType: "test:resource:1:",
|
||||
identifiers: resource.Identifiers{"id2"},
|
||||
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id3"}}},
|
||||
}
|
||||
c := &testResource{
|
||||
resType: "test:resource:1:",
|
||||
identifiers: resource.Identifiers{"id3"},
|
||||
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id4"}}, &resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id5"}}},
|
||||
}
|
||||
d := &testResource{
|
||||
resType: "test:resource:1:",
|
||||
identifiers: resource.Identifiers{"id4"},
|
||||
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id2"}}},
|
||||
}
|
||||
e := &testResource{
|
||||
resType: "test:resource:1:",
|
||||
identifiers: resource.Identifiers{"id5"},
|
||||
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id2"}}},
|
||||
}
|
||||
|
||||
rr := []resource.Interface{
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
d,
|
||||
e,
|
||||
}
|
||||
|
||||
g, err := bl.Build(ctx, rr...)
|
||||
req.NoError(err)
|
||||
|
||||
s, err := g.NextInverted(ctx)
|
||||
req.NoError(err)
|
||||
req.Equal(b, s.Res)
|
||||
req.True(s.Conflicting)
|
||||
|
||||
s, err = g.NextInverted(ctx)
|
||||
req.NoError(err)
|
||||
req.Equal(a, s.Res)
|
||||
req.False(s.Conflicting)
|
||||
|
||||
s, err = g.NextInverted(ctx)
|
||||
req.NoError(err)
|
||||
req.Equal(d, s.Res)
|
||||
req.False(s.Conflicting)
|
||||
|
||||
s, err = g.NextInverted(ctx)
|
||||
req.NoError(err)
|
||||
req.Equal(e, s.Res)
|
||||
req.False(s.Conflicting)
|
||||
|
||||
s, err = g.NextInverted(ctx)
|
||||
req.NoError(err)
|
||||
req.Equal(c, s.Res)
|
||||
req.False(s.Conflicting)
|
||||
|
||||
s, err = g.NextInverted(ctx)
|
||||
req.NoError(err)
|
||||
req.Equal(b, s.Res)
|
||||
req.False(s.Conflicting)
|
||||
|
||||
s, err = g.NextInverted(ctx)
|
||||
req.NoError(err)
|
||||
req.Nil(s)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGraph_Utilities(t *testing.T) {
|
||||
req := require.New(t)
|
||||
|
||||
t.Run("adding & removing nodes", func(t *testing.T) {
|
||||
g := newGraph()
|
||||
n1 := newNode(nil)
|
||||
n2 := newNode(nil)
|
||||
|
||||
// Adding nodes
|
||||
g.addNode(n1, n2)
|
||||
req.Len(g.nn, 2)
|
||||
req.Equal(n1, g.nn[0])
|
||||
req.Equal(n2, g.nn[1])
|
||||
|
||||
// Dependencies
|
||||
g.addChild(n1, n2)
|
||||
g.addParent(n2, n1)
|
||||
req.True(g.childNodes(n1).has(n2))
|
||||
req.True(g.parentNodes(n2).has(n1))
|
||||
|
||||
// Removing nodes
|
||||
g.removeNode(n1, n2)
|
||||
req.False(g.childNodes(n1).has(n2))
|
||||
req.False(g.parentNodes(n2).has(n1))
|
||||
})
|
||||
|
||||
t.Run("inverted graph", func(t *testing.T) {
|
||||
g := newGraph()
|
||||
n1 := newNode(nil)
|
||||
n2 := newNode(nil)
|
||||
|
||||
// Adding nodes
|
||||
g.addNode(n1, n2)
|
||||
req.Len(g.nn, 2)
|
||||
req.Equal(n1, g.nn[0])
|
||||
req.Equal(n2, g.nn[1])
|
||||
|
||||
// Dependencies
|
||||
g.addChild(n1, n2)
|
||||
g.addParent(n2, n1)
|
||||
g.invert()
|
||||
req.True(g.parentNodes(n1).has(n2))
|
||||
req.True(g.childNodes(n2).has(n1))
|
||||
})
|
||||
|
||||
t.Run("inverted child dependencies", func(t *testing.T) {
|
||||
g := newGraph()
|
||||
n1 := newNode(nil)
|
||||
n2 := newNode(nil)
|
||||
n3 := newNode(nil)
|
||||
|
||||
g.addNode(n1, n2, n3)
|
||||
|
||||
g.addChild(n1, n2)
|
||||
g.invert()
|
||||
g.addChild(n1, n3)
|
||||
req.Equal(n2, n1.cc[0])
|
||||
req.Equal(n3, n1.pp[0])
|
||||
|
||||
g.removeChild(n1, n3)
|
||||
req.Len(n1.pp, 0)
|
||||
})
|
||||
|
||||
t.Run("inverted parent dependencies", func(t *testing.T) {
|
||||
g := newGraph()
|
||||
n1 := newNode(nil)
|
||||
n2 := newNode(nil)
|
||||
n3 := newNode(nil)
|
||||
|
||||
g.addNode(n1, n2, n3)
|
||||
|
||||
g.addParent(n1, n2)
|
||||
g.invert()
|
||||
g.addParent(n1, n3)
|
||||
req.Equal(n2, n1.pp[0])
|
||||
req.Equal(n3, n1.cc[0])
|
||||
|
||||
g.removeParent(n1, n3)
|
||||
req.Len(n1.cc, 0)
|
||||
})
|
||||
|
||||
// This one is just for codecoverage; the logic is nothing
|
||||
t.Run("remove all child/parent nodes", func(t *testing.T) {
|
||||
g := newGraph()
|
||||
n1 := newNode(nil)
|
||||
n2 := newNode(nil)
|
||||
|
||||
g.addNode(n1, n2)
|
||||
g.addChild(n1, n2)
|
||||
g.addParent(n1, n2)
|
||||
|
||||
g.removeChild(n1)
|
||||
g.removeParent(n1)
|
||||
req.Len(n1.cc, 0)
|
||||
req.Len(n1.pp, 0)
|
||||
|
||||
// Inverted graph
|
||||
g.addChild(n1, n2)
|
||||
g.addParent(n1, n2)
|
||||
g.invert()
|
||||
|
||||
g.removeChild(n1)
|
||||
g.removeParent(n1)
|
||||
req.Len(n1.cc, 0)
|
||||
req.Len(n1.pp, 0)
|
||||
})
|
||||
}
|
||||
135
server/pkg/envoy/json/bulk_compose_record_marshal.go
Normal file
135
server/pkg/envoy/json/bulk_compose_record_marshal.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
)
|
||||
|
||||
type (
|
||||
bulkComposeRecordEncoder struct {
|
||||
encoderConfig *EncoderConfig
|
||||
|
||||
res *resource.ComposeRecord
|
||||
|
||||
refNs string
|
||||
|
||||
refMod string
|
||||
relMod *types.Module
|
||||
}
|
||||
)
|
||||
|
||||
func bulkComposeRecordEncoderFromResource(rec *resource.ComposeRecord, cfg *EncoderConfig) *bulkComposeRecordEncoder {
|
||||
return &bulkComposeRecordEncoder{
|
||||
encoderConfig: cfg,
|
||||
|
||||
res: rec,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *bulkComposeRecordEncoder) Prepare(ctx context.Context, state *envoy.ResourceState) (err error) {
|
||||
_, ok := state.Res.(*resource.ComposeRecord)
|
||||
if !ok {
|
||||
return encoderErrInvalidResource(types.RecordResourceType, state.Res.ResourceType())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *bulkComposeRecordEncoder) Encode(ctx context.Context, w io.Writer, state *envoy.ResourceState) (err error) {
|
||||
enc := json.NewEncoder(w)
|
||||
all := len(n.encoderConfig.Fields) == 0
|
||||
|
||||
getMapValue := func(v resource.ComposeRecordRawValue, proc func(k any) *resource.Userstamp) any {
|
||||
if v.IsMulti {
|
||||
if proc == nil {
|
||||
return v.Values
|
||||
}
|
||||
|
||||
out := make([]interface{}, 0, len(v.Values))
|
||||
for _, vv := range v.Values {
|
||||
out = append(out, proc(vv))
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
if proc == nil {
|
||||
return v.Value
|
||||
}
|
||||
|
||||
return proc(v.Value)
|
||||
}
|
||||
|
||||
err = n.res.Walker(func(r *resource.ComposeRecordRaw) error {
|
||||
m, err := makeMap(
|
||||
"id", r.ID,
|
||||
)
|
||||
|
||||
r.Ts, err = r.Ts.Model(n.encoderConfig.TimeLayout, n.encoderConfig.Timezone)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Encode system values
|
||||
sysValues := make(map[string]interface{})
|
||||
sysValues["createdAt"] = r.Ts.CreatedAt
|
||||
sysValues["updatedAt"] = r.Ts.UpdatedAt
|
||||
sysValues["deletedAt"] = r.Ts.DeletedAt
|
||||
sysValues["createdBy"] = n.res.UserFlakes.GetByStamp(r.Us.CreatedBy)
|
||||
sysValues["updatedBy"] = n.res.UserFlakes.GetByStamp(r.Us.UpdatedBy)
|
||||
sysValues["deletedBy"] = n.res.UserFlakes.GetByStamp(r.Us.DeletedBy)
|
||||
sysValues["ownedBy"] = n.res.UserFlakes.GetByStamp(r.Us.OwnedBy)
|
||||
|
||||
for k, v := range sysValues {
|
||||
// Skip fields we don't want
|
||||
if !all && !n.encoderConfig.Fields[k] {
|
||||
continue
|
||||
}
|
||||
m, err = addMap(m,
|
||||
k, v,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Encode record values
|
||||
for k, v := range r.Values {
|
||||
// Skip fields we don't want
|
||||
if !all && !n.encoderConfig.Fields[k] {
|
||||
continue
|
||||
}
|
||||
|
||||
f := n.res.RelMod.Fields.FindByName(k)
|
||||
if f == nil {
|
||||
return fmt.Errorf("field %s not found", k)
|
||||
}
|
||||
if f.Kind == "User" {
|
||||
m, err = addMap(m,
|
||||
k, getMapValue(v, n.res.UserFlakes.GetByKey),
|
||||
)
|
||||
} else {
|
||||
m, err = addMap(m,
|
||||
k, getMapValue(v, nil),
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return enc.Encode(m)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
139
server/pkg/envoy/json/decoder.go
Normal file
139
server/pkg/envoy/json/decoder.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
)
|
||||
|
||||
type (
|
||||
// wrapper struct for json related methods
|
||||
decoder struct{}
|
||||
|
||||
// json decoder wrapper for additional bits
|
||||
reader struct {
|
||||
header []string
|
||||
count uint64
|
||||
|
||||
cache []map[string]string
|
||||
cacheIndex int
|
||||
}
|
||||
)
|
||||
|
||||
// Decoder initializes and returns a fresh JSON decoder
|
||||
//
|
||||
// We'll only do jsonl for now -- for record importing.
|
||||
// We'll expand this to work with other resources later on.
|
||||
func Decoder() *decoder {
|
||||
return &decoder{}
|
||||
}
|
||||
|
||||
// CanDecodeFile determines if the file can be decoded by this decoder
|
||||
func (d *decoder) CanDecodeFile(f io.Reader) bool {
|
||||
var buff bytes.Buffer
|
||||
tr := io.TeeReader(f, &buff)
|
||||
|
||||
m, err := mimetype.DetectReader(tr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
ext := m.Extension()
|
||||
|
||||
return d.CanDecodeExt(ext)
|
||||
}
|
||||
|
||||
func (d *decoder) CanDecodeMime(m string) bool {
|
||||
return m == "application/json" || m == "application/jsonlines"
|
||||
}
|
||||
|
||||
func (d *decoder) CanDecodeExt(ext string) bool {
|
||||
pt := strings.Split(ext, ".")
|
||||
ext = strings.TrimSpace(pt[len(pt)-1])
|
||||
return ext == "jsonl" || ext == "json" || ext == "ndjson"
|
||||
}
|
||||
|
||||
// Decode decodes the given io.Reader into a generic resource dataset
|
||||
func (d *decoder) Decode(ctx context.Context, r io.Reader, do *envoy.DecoderOpts) ([]resource.Interface, error) {
|
||||
jr := &reader{
|
||||
cacheIndex: 0,
|
||||
cache: make([]map[string]string, 0, 1000),
|
||||
}
|
||||
|
||||
err := jr.prepare(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []resource.Interface{resource.NewResourceDataset(do.Name, jr)}, nil
|
||||
}
|
||||
|
||||
// The prepare step caches all of the rows into a cache array that will be used
|
||||
// to access already visited json rows.
|
||||
//
|
||||
// @todo implement FS cache file for larger files
|
||||
func (jr *reader) prepare(r io.Reader) (err error) {
|
||||
jReader := json.NewDecoder(r)
|
||||
|
||||
// JSON can omit empty values, so we can't be 100% that all of the headers
|
||||
// were read in the first go.
|
||||
// We'll determine all of the headers below where we count the entries
|
||||
hx := make(map[string]bool)
|
||||
jr.header = make([]string, 0, 100)
|
||||
|
||||
for jReader.More() {
|
||||
aux := make(map[string]string)
|
||||
err = jReader.Decode(&aux)
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Get all the header fields
|
||||
for h := range aux {
|
||||
if !hx[h] {
|
||||
jr.header = append(jr.header, h)
|
||||
hx[h] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Entry count
|
||||
jr.count++
|
||||
|
||||
// Cache entry
|
||||
jr.cache = append(jr.cache, aux)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (jr *reader) Reset() error {
|
||||
jr.cacheIndex = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fields returns every available field in this dataset
|
||||
func (jr *reader) Fields() []string {
|
||||
return jr.header
|
||||
}
|
||||
|
||||
// Next returns the field: value mapping for the next row
|
||||
func (jr *reader) Next() (map[string]string, error) {
|
||||
if jr.cacheIndex >= len(jr.cache) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
mr := jr.cache[jr.cacheIndex]
|
||||
jr.cacheIndex++
|
||||
return mr, nil
|
||||
}
|
||||
|
||||
func (cr *reader) Count() uint64 {
|
||||
return cr.count
|
||||
}
|
||||
58
server/pkg/envoy/json/decoder_test.go
Normal file
58
server/pkg/envoy/json/decoder_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func parseDocument(ctx context.Context, name string) (*resource.ResourceDataset, error) {
|
||||
path := fmt.Sprintf("testdata/%s.jsonl", name)
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
cd := Decoder()
|
||||
ii, err := cd.Decode(ctx, f, &envoy.DecoderOpts{
|
||||
Name: name,
|
||||
Path: path,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rd, _ := ii[0].(*resource.ResourceDataset)
|
||||
return rd, nil
|
||||
}
|
||||
|
||||
func TestDecoder(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("doc 1", func(t *testing.T) {
|
||||
req := require.New(t)
|
||||
|
||||
ds, err := parseDocument(ctx, "records_1")
|
||||
req.NoError(err)
|
||||
req.NotNil(ds)
|
||||
req.Equal(resource.MakeIdentifiers("records_1"), ds.Identifiers())
|
||||
req.Subset([]string{"id", "c1", "c2", "c3"}, ds.P.Fields())
|
||||
req.Equal(uint64(3), ds.P.Count())
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
n, err := ds.P.Next()
|
||||
req.NoError(err)
|
||||
req.NotNil(n)
|
||||
|
||||
req.Len(n, 4)
|
||||
}
|
||||
n, err := ds.P.Next()
|
||||
req.Nil(n)
|
||||
req.Nil(err)
|
||||
})
|
||||
}
|
||||
163
server/pkg/envoy/json/encoder.go
Normal file
163
server/pkg/envoy/json/encoder.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
)
|
||||
|
||||
type (
|
||||
bulkRecordEncoder struct {
|
||||
cfg *EncoderConfig
|
||||
|
||||
resState map[resource.Interface]*encoderState
|
||||
}
|
||||
|
||||
encoderState struct {
|
||||
res resourceState
|
||||
source io.ReadWriter
|
||||
resourceType string
|
||||
Scope string
|
||||
identifier string
|
||||
}
|
||||
|
||||
// EncoderConfig allows us to configure the resource encoding process
|
||||
EncoderConfig struct {
|
||||
// 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
|
||||
// Fields specifies what fields we wish to include in the export
|
||||
Fields map[string]bool
|
||||
}
|
||||
|
||||
// resourceState holds some intermedia values to help with encoding
|
||||
resourceState interface {
|
||||
Prepare(ctx context.Context, state *envoy.ResourceState) (err error)
|
||||
Encode(ctx context.Context, w io.Writer, state *envoy.ResourceState) (err error)
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnknownResource = fmt.Errorf("unknown resource")
|
||||
ErrResourceStateUndefined = fmt.Errorf("undefined resource state")
|
||||
)
|
||||
|
||||
func NewBulkRecordEncoder(cfg *EncoderConfig) envoy.PrepareEncodeStreamer {
|
||||
if cfg == nil {
|
||||
cfg = &EncoderConfig{}
|
||||
}
|
||||
|
||||
return &bulkRecordEncoder{
|
||||
cfg: cfg,
|
||||
|
||||
resState: make(map[resource.Interface]*encoderState),
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare prepares the encoder for the given set of resources
|
||||
//
|
||||
// It initializes and prepares the resource state for each provided resource
|
||||
func (se *bulkRecordEncoder) Prepare(ctx context.Context, ee ...*envoy.ResourceState) (err error) {
|
||||
f := func(rs resourceState, es *envoy.ResourceState) error {
|
||||
err = rs.Prepare(ctx, es)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
se.resState[es.Res] = &encoderState{
|
||||
res: rs,
|
||||
source: &bytes.Buffer{},
|
||||
resourceType: es.Res.ResourceType(),
|
||||
identifier: es.Res.Identifiers().First(),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, e := range ee {
|
||||
switch res := e.Res.(type) {
|
||||
// @todo other resources; we'll only do records for now
|
||||
case *resource.ComposeRecord:
|
||||
err = f(bulkComposeRecordEncoderFromResource(res, se.cfg), e)
|
||||
|
||||
default:
|
||||
err = ErrUnknownResource
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return se.WrapError("prepare", e.Res, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Encode encodes the resources into a series of jsonl documents
|
||||
func (se *bulkRecordEncoder) 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
|
||||
}
|
||||
|
||||
state := se.resState[e.Res]
|
||||
if state == nil {
|
||||
err = ErrResourceStateUndefined
|
||||
} else {
|
||||
err = state.res.Encode(ctx, state.source, e)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return se.WrapError("encode: build doc", e.Res, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, s := range se.resState {
|
||||
s.res = nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (se *bulkRecordEncoder) Stream() []*envoy.Stream {
|
||||
ss := make([]*envoy.Stream, 0, 20)
|
||||
|
||||
for _, s := range se.resState {
|
||||
ss = append(ss, &envoy.Stream{
|
||||
Resource: s.resourceType,
|
||||
Identifier: s.identifier,
|
||||
Source: s.source,
|
||||
})
|
||||
}
|
||||
|
||||
return ss
|
||||
}
|
||||
|
||||
// WrapError wraps errors related to json encoding
|
||||
//
|
||||
// Always wrap your errors.
|
||||
func (se *bulkRecordEncoder) WrapError(act string, res resource.Interface, err error) error {
|
||||
rt := strings.Join(strings.Split(strings.TrimSpace(strings.TrimRight(res.ResourceType(), ":")), ":"), " ")
|
||||
return fmt.Errorf("json 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)
|
||||
}
|
||||
3
server/pkg/envoy/json/testdata/records_1.jsonl
vendored
Normal file
3
server/pkg/envoy/json/testdata/records_1.jsonl
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{"id": "1", "c1": "c1.v1", "c2": "c2.v1", "c3": "c3.v1"}
|
||||
{"id": "2", "c1": "c1.v2", "c2": "c2.v2", "c3": "c3.v2"}
|
||||
{"id": "3", "c1": "c1.v3", "c2": "c2.v3", "c3": "c3.v3"}
|
||||
143
server/pkg/envoy/json/util_encode.go
Normal file
143
server/pkg/envoy/json/util_encode.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
)
|
||||
|
||||
type (
|
||||
seqNode []interface{}
|
||||
mapNode map[string]interface{}
|
||||
)
|
||||
|
||||
// mapTimestamps helper encodes Timestamps into the mapping node
|
||||
func mapTimestamps(n mapNode, ts *resource.Timestamps) (mapNode, 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 mapUserstamps(n mapNode, us *resource.Userstamps) (mapNode, 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,
|
||||
)
|
||||
}
|
||||
|
||||
// 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 mapNode) mapNode {
|
||||
m := make(mapNode)
|
||||
|
||||
for k, v := range n {
|
||||
if k != "" && v != nil {
|
||||
m[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// 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{}) (mapNode, error) {
|
||||
return addMap(make(mapNode), pp...)
|
||||
}
|
||||
|
||||
func isNil(i interface{}) bool {
|
||||
if i == nil {
|
||||
return true
|
||||
}
|
||||
switch reflect.TypeOf(i).Kind() {
|
||||
case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice:
|
||||
return reflect.ValueOf(i).IsNil()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 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 mapNode, pp ...interface{}) (mapNode, 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)
|
||||
}
|
||||
|
||||
for i := 0; i < len(pp); i += 2 {
|
||||
kRaw := pp[i]
|
||||
vRaw := pp[i+1]
|
||||
|
||||
if isNil(vRaw) {
|
||||
continue
|
||||
}
|
||||
|
||||
k, ok := kRaw.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("keys must be of type string: %v", kRaw)
|
||||
}
|
||||
|
||||
switch v := vRaw.(type) {
|
||||
case string:
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
case bool:
|
||||
if !v {
|
||||
continue
|
||||
}
|
||||
case int, uint,
|
||||
int32, uint32,
|
||||
int64, uint64:
|
||||
if v == 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
n[k] = vRaw
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// makeSeq creates a new sequence node based on the provided items
|
||||
func makeSeq(vv ...interface{}) (seqNode, error) {
|
||||
return addSeq(make(seqNode, 0, len(vv)), vv...)
|
||||
}
|
||||
|
||||
// addSeq adds new items to the sequence node
|
||||
func addSeq(n seqNode, vv ...interface{}) (seqNode, error) {
|
||||
for _, vRaw := range vv {
|
||||
if vRaw != "" && vRaw != nil {
|
||||
n = append(n, vRaw)
|
||||
}
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
99
server/pkg/envoy/locale.go
Normal file
99
server/pkg/envoy/locale.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package envoy
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
)
|
||||
|
||||
// NormalizeResourceTranslations takes the provided resource.ResourceTranslation
|
||||
// and merges duplicates based on the Priority parameter
|
||||
func NormalizeResourceTranslations(rr ...resource.Interface) []resource.Interface {
|
||||
out := make([]resource.Interface, 0, len(rr))
|
||||
locales := make([]*resource.ResourceTranslation, 0, len(rr))
|
||||
|
||||
// - collect all locale resources
|
||||
for _, r := range rr {
|
||||
if l, ok := r.(*resource.ResourceTranslation); ok {
|
||||
locales = append(locales, l)
|
||||
} else {
|
||||
out = append(out, r)
|
||||
}
|
||||
}
|
||||
|
||||
// make an index
|
||||
var byResource map[string][2]*resource.ResourceTranslation
|
||||
byResource = make(map[string][2]*resource.ResourceTranslation)
|
||||
|
||||
for _, locale := range locales {
|
||||
pp := byResource[locale.RefResource]
|
||||
pp[locale.Priority] = locale
|
||||
byResource[locale.RefResource] = pp
|
||||
}
|
||||
|
||||
// squash index by priority ascending
|
||||
for _, pp := range byResource {
|
||||
var aux *resource.ResourceTranslation
|
||||
seen := make(map[string]bool)
|
||||
|
||||
for _, p := range pp {
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if aux == nil {
|
||||
aux = p
|
||||
} else {
|
||||
for _, r := range p.Res {
|
||||
if seen[r.Lang.String()+r.K] {
|
||||
continue
|
||||
}
|
||||
|
||||
aux.Res = append(aux.Res, r)
|
||||
}
|
||||
}
|
||||
|
||||
for _, r := range p.Res {
|
||||
if r.K != "" {
|
||||
seen[r.Lang.String()+r.K] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if aux != nil {
|
||||
out = append(out, aux)
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func appendRefSet(a resource.RefSet, b *resource.Ref) resource.RefSet {
|
||||
return append(a, b)
|
||||
}
|
||||
|
||||
// FilterRequiredResourceTranslations returns only resource translations relevant for the given resources
|
||||
func FilterRequiredResourceTranslations(request resource.InterfaceSet, translations []*resource.ResourceTranslation) (out []*resource.ResourceTranslation) {
|
||||
out = make([]*resource.ResourceTranslation, 0, 100)
|
||||
|
||||
// Filter
|
||||
procResSet(request, func(r resource.Interface) {
|
||||
localeRes, ok := r.(resource.LocaleInterface)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
_, ref, pp := localeRes.ResourceTranslationParts()
|
||||
resourceRefSet := appendRefSet(pp, ref)
|
||||
|
||||
for _, t := range translations {
|
||||
translationRefSet := appendRefSet(t.RefPath, t.RefRes)
|
||||
// Res. tr. use strict equality to determine where it falls into
|
||||
if !translationRefSet.Equals(resourceRefSet) {
|
||||
continue
|
||||
}
|
||||
|
||||
out = append(out, t)
|
||||
}
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
46
server/pkg/envoy/marshaler.go
Normal file
46
server/pkg/envoy/marshaler.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package envoy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
)
|
||||
|
||||
type (
|
||||
Marshaller interface {
|
||||
MarshalEnvoy() ([]resource.Interface, error)
|
||||
}
|
||||
)
|
||||
|
||||
// MarshalMerge takes one or more nodes and Marshals and merges all
|
||||
func CollectNodes(ii ...interface{}) (nn []resource.Interface, err error) {
|
||||
for _, i := range ii {
|
||||
// i == nil is not enough in go
|
||||
if i == nil || (reflect.ValueOf(i).Kind() == reflect.Ptr && reflect.ValueOf(i).IsNil()) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch c := i.(type) {
|
||||
// case NodeSet:
|
||||
// nn = append(nn, c...)
|
||||
case resource.Interface:
|
||||
nn = append(nn, c)
|
||||
|
||||
case []resource.Interface:
|
||||
nn = append(nn, c...)
|
||||
|
||||
case Marshaller:
|
||||
if tmp, err := c.MarshalEnvoy(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
nn = append(nn, tmp...)
|
||||
}
|
||||
default:
|
||||
// @todo resoruceset?
|
||||
return nil, fmt.Errorf("failed to merge %T; expecting Resource, ResourceSet? or Marshaller interface", i)
|
||||
}
|
||||
}
|
||||
|
||||
return nn, nil
|
||||
}
|
||||
129
server/pkg/envoy/node.go
Normal file
129
server/pkg/envoy/node.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package envoy
|
||||
|
||||
import "github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
|
||||
type (
|
||||
// the node struct is used for nicer graph state management
|
||||
nodeSet []*node
|
||||
node struct {
|
||||
res resource.Interface
|
||||
missing resource.RefSet
|
||||
|
||||
pp nodeSet
|
||||
cc nodeSet
|
||||
}
|
||||
|
||||
// resource type -> identifier -> []nodes
|
||||
// There can be multiple resources with same identifier; for example
|
||||
// two modules under different namespaces.
|
||||
nodeIndex map[string]map[string]nodeSet
|
||||
)
|
||||
|
||||
func newNode(res resource.Interface) *node {
|
||||
return &node{
|
||||
res: res,
|
||||
cc: make(nodeSet, 0, 10),
|
||||
pp: make(nodeSet, 0, 10),
|
||||
}
|
||||
}
|
||||
|
||||
func (nn nodeSet) add(mm ...*node) nodeSet {
|
||||
return append(nn, mm...)
|
||||
}
|
||||
|
||||
func (nn nodeSet) has(m *node) bool {
|
||||
for _, n := range nn {
|
||||
if n == m {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (nn nodeSet) remove(mm ...*node) nodeSet {
|
||||
if len(mm) <= 0 {
|
||||
return nn
|
||||
}
|
||||
|
||||
nClean := make(nodeSet, 0, len(nn))
|
||||
mmSet := make(nodeSet, 0, len(mm))
|
||||
mmSet = append(mmSet, mm...)
|
||||
|
||||
for _, n := range nn {
|
||||
if !mmSet.has(n) {
|
||||
nClean = append(nClean, n)
|
||||
}
|
||||
}
|
||||
|
||||
return nClean
|
||||
}
|
||||
|
||||
func (ri nodeIndex) Add(nn ...*node) {
|
||||
for _, n := range nn {
|
||||
rt := n.res.ResourceType()
|
||||
if _, has := ri[rt]; !has {
|
||||
ri[rt] = make(map[string]nodeSet)
|
||||
}
|
||||
|
||||
for _, i := range n.res.Identifiers() {
|
||||
if ri[rt][i] == nil {
|
||||
ri[rt][i] = make(nodeSet, 0, 5)
|
||||
}
|
||||
ri[rt][i] = append(ri[rt][i], n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ri nodeIndex) GetRef(ref *resource.Ref) *node {
|
||||
refIi, has := ri[ref.ResourceType]
|
||||
if !has {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, i := range ref.Identifiers {
|
||||
rr, has := refIi[i]
|
||||
if !has || len(rr) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// No constraints? no worries
|
||||
if ref.Constraints == nil || len(ref.Constraints) == 0 {
|
||||
return rr[0]
|
||||
}
|
||||
|
||||
// Constraints? check if ok
|
||||
// If this loop makes you sick, don't worry; this will be at most 3x3x1
|
||||
for _, r := range rr {
|
||||
for _, c := range ref.Constraints {
|
||||
for _, ref := range r.res.Refs() {
|
||||
if ref.ResourceType == c.ResourceType && ref.Identifiers.HasAny(c.Identifiers) {
|
||||
return r
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ri nodeIndex) GetResourceType(rt string) nodeSet {
|
||||
ix, has := ri[rt]
|
||||
if !has {
|
||||
return nil
|
||||
}
|
||||
nm := make(map[*node]bool)
|
||||
for _, nn := range ix {
|
||||
for _, n := range nn {
|
||||
nm[n] = true
|
||||
}
|
||||
}
|
||||
|
||||
nn := make(nodeSet, 0, len(nm))
|
||||
for n := range nm {
|
||||
nn = append(nn, n)
|
||||
}
|
||||
return nn
|
||||
}
|
||||
41
server/pkg/envoy/rbac.go
Normal file
41
server/pkg/envoy/rbac.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package envoy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
)
|
||||
|
||||
// FilterRequestedRBACRules returns only RBAC rules relevant for the given resources
|
||||
func FilterRequestedRBACRules(request resource.InterfaceSet, rules []*resource.RbacRule) (out []*resource.RbacRule) {
|
||||
out = make([]*resource.RbacRule, 0, 10)
|
||||
|
||||
// Filter
|
||||
dupRuleIndex := make(map[string]bool)
|
||||
procResSet(request, func(r resource.Interface) {
|
||||
rbacRes, ok := r.(resource.RBACInterface)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
_, ref, pp := rbacRes.RBACParts()
|
||||
resourceRefSet := appendRefSet(pp, ref)
|
||||
|
||||
for _, rule := range rules {
|
||||
k := fmt.Sprintf("%s, %s, %d; %d", rule.Res.Resource, rule.Res.Operation, rule.Res.Access, rule.Res.RoleID)
|
||||
if dupRuleIndex[k] {
|
||||
continue
|
||||
}
|
||||
dupRuleIndex[k] = true
|
||||
ruleRefSet := appendRefSet(rule.RefPath, rule.RefRes)
|
||||
// Checking if rule is <= resource since wildflags can be used
|
||||
if !ruleRefSet.IsSubset(resourceRefSet) {
|
||||
continue
|
||||
}
|
||||
|
||||
out = append(out, rule)
|
||||
}
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
99
server/pkg/envoy/resource/api_gateway.go
Normal file
99
server/pkg/envoy/resource/api_gateway.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
APIGateway struct {
|
||||
*base
|
||||
Res *types.ApigwRoute
|
||||
|
||||
Filters []*APIGatewayFilter
|
||||
}
|
||||
|
||||
APIGatewayFilter struct {
|
||||
*base
|
||||
Res *types.ApigwFilter
|
||||
}
|
||||
)
|
||||
|
||||
func NewAPIGateway(res *types.ApigwRoute) *APIGateway {
|
||||
// @todo we could use method + path combo to uniquely identify them also
|
||||
r := &APIGateway{
|
||||
base: &base{},
|
||||
}
|
||||
r.SetResourceType(types.ApigwRouteResourceType)
|
||||
r.Res = res
|
||||
|
||||
r.AddIdentifier(identifiers(res.ID)...)
|
||||
|
||||
// Initial stamps
|
||||
r.SetTimestamps(MakeTimestampsCUDA(&res.CreatedAt, res.UpdatedAt, res.DeletedAt, nil))
|
||||
us := MakeUserstampsCUDO(res.CreatedBy, res.UpdatedBy, res.DeletedBy, 0)
|
||||
r.SetUserstamps(us)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *APIGateway) Resource() interface{} {
|
||||
return r.Res
|
||||
}
|
||||
|
||||
func (r *APIGateway) RBACParts() (resource string, ref *Ref, path []*Ref) {
|
||||
ref = r.Ref()
|
||||
path = nil
|
||||
resource = fmt.Sprintf(types.ApigwRouteRbacResourceTpl(), types.ApigwRouteResourceType, firstOkString(strconv.FormatUint(r.Res.ID, 10)))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *APIGateway) AddGatewayFilter(res *types.ApigwFilter) *APIGatewayFilter {
|
||||
f := &APIGatewayFilter{
|
||||
base: &base{},
|
||||
}
|
||||
|
||||
f.Res = res
|
||||
|
||||
// Initial stamps
|
||||
f.SetTimestamps(MakeTimestampsCUDA(&res.CreatedAt, res.UpdatedAt, res.DeletedAt, nil))
|
||||
f.SetUserstamps(MakeUserstampsCUDO(res.CreatedBy, res.UpdatedBy, res.DeletedBy, 0))
|
||||
|
||||
r.Filters = append(r.Filters, f)
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func (r *APIGateway) SysID() uint64 {
|
||||
return r.Res.ID
|
||||
}
|
||||
|
||||
// FindAPIGateway looks for the ApigwRoute in the resource set
|
||||
func FindAPIGateway(rr InterfaceSet, ii Identifiers) (ns *types.ApigwRoute) {
|
||||
var wfRes *APIGateway
|
||||
|
||||
rr.Walk(func(r Interface) error {
|
||||
wr, ok := r.(*APIGateway)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if wr.Identifiers().HasAny(ii) {
|
||||
wfRes = wr
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Found it
|
||||
if wfRes != nil {
|
||||
return wfRes.Res
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func APIGatewayErrUnresolved(ii Identifiers) error {
|
||||
return fmt.Errorf("automation apu gateway unresolved %v", ii.StringSlice())
|
||||
}
|
||||
80
server/pkg/envoy/resource/application.go
Normal file
80
server/pkg/envoy/resource/application.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
// Application represents a Application
|
||||
Application struct {
|
||||
*base
|
||||
Res *types.Application
|
||||
}
|
||||
)
|
||||
|
||||
func NewApplication(res *types.Application) *Application {
|
||||
r := &Application{base: &base{}}
|
||||
r.SetResourceType(types.ApplicationResourceType)
|
||||
r.Res = res
|
||||
|
||||
r.AddIdentifier(identifiers(res.Name, res.ID)...)
|
||||
|
||||
// Initial timestamps
|
||||
r.SetTimestamps(MakeTimestampsCUDA(&res.CreatedAt, res.UpdatedAt, res.DeletedAt, nil))
|
||||
// Initial userstamps
|
||||
if res.OwnerID > 0 {
|
||||
r.SetUserstamps(&Userstamps{
|
||||
OwnedBy: &Userstamp{UserID: res.OwnerID},
|
||||
})
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Application) Resource() interface{} {
|
||||
return r.Res
|
||||
}
|
||||
|
||||
func (r *Application) SysID() uint64 {
|
||||
return r.Res.ID
|
||||
}
|
||||
|
||||
func (r *Application) RBACParts() (resource string, ref *Ref, path []*Ref) {
|
||||
ref = r.Ref()
|
||||
path = nil
|
||||
resource = fmt.Sprintf(types.ApplicationRbacResourceTpl(), types.ApplicationResourceType, firstOkString(strconv.FormatUint(r.Res.ID, 10)))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// FindApplication looks for the app in the resource set
|
||||
func FindApplication(rr InterfaceSet, ii Identifiers) (ap *types.Application) {
|
||||
var apRes *Application
|
||||
|
||||
rr.Walk(func(r Interface) error {
|
||||
ar, ok := r.(*Application)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if ar.Identifiers().HasAny(ii) {
|
||||
apRes = ar
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
// Found it
|
||||
if apRes != nil {
|
||||
return apRes.Res
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ApplicationErrUnresolved(ii Identifiers) error {
|
||||
return fmt.Errorf("application unresolved %v", ii.StringSlice())
|
||||
}
|
||||
155
server/pkg/envoy/resource/automation_workflow.go
Normal file
155
server/pkg/envoy/resource/automation_workflow.go
Normal file
@@ -0,0 +1,155 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/automation/types"
|
||||
)
|
||||
|
||||
type (
|
||||
AutomationWorkflow struct {
|
||||
*base
|
||||
Res *types.Workflow
|
||||
|
||||
Triggers []*AutomationTrigger
|
||||
Steps []*AutomationWorkflowStep
|
||||
Paths []*AutomationWorkflowPath
|
||||
}
|
||||
|
||||
AutomationTrigger struct {
|
||||
*base
|
||||
Res *types.Trigger
|
||||
}
|
||||
|
||||
AutomationWorkflowStep struct {
|
||||
*base
|
||||
Res *types.WorkflowStep
|
||||
}
|
||||
|
||||
AutomationWorkflowPath struct {
|
||||
*base
|
||||
Res *types.WorkflowPath
|
||||
|
||||
ParentStep Identifiers
|
||||
ChildStep Identifiers
|
||||
}
|
||||
)
|
||||
|
||||
func NewAutomationWorkflow(res *types.Workflow) *AutomationWorkflow {
|
||||
r := &AutomationWorkflow{
|
||||
base: &base{},
|
||||
}
|
||||
r.SetResourceType(types.WorkflowResourceType)
|
||||
r.Res = res
|
||||
|
||||
r.AddIdentifier(identifiers(res.Handle, res.Meta.Name, res.ID)...)
|
||||
|
||||
// Initial stamps
|
||||
r.SetTimestamps(MakeTimestampsCUDA(&res.CreatedAt, res.UpdatedAt, res.DeletedAt, nil))
|
||||
us := MakeUserstampsCUDO(res.CreatedBy, res.UpdatedBy, res.DeletedBy, res.OwnedBy)
|
||||
us.RunAs = MakeUserstampFromID(res.RunAs)
|
||||
r.SetUserstamps(us)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *AutomationWorkflow) Resource() interface{} {
|
||||
return r.Res
|
||||
}
|
||||
|
||||
func (r *AutomationWorkflow) AddAutomationTrigger(res *types.Trigger) *AutomationTrigger {
|
||||
t := &AutomationTrigger{
|
||||
base: &base{},
|
||||
}
|
||||
|
||||
t.Res = res
|
||||
|
||||
// Initial stamps
|
||||
t.SetTimestamps(MakeTimestampsCUDA(&res.CreatedAt, res.UpdatedAt, res.DeletedAt, nil))
|
||||
t.SetUserstamps(MakeUserstampsCUDO(res.CreatedBy, res.UpdatedBy, res.DeletedBy, res.OwnedBy))
|
||||
|
||||
if r.Triggers == nil {
|
||||
r.Triggers = make([]*AutomationTrigger, 0, 2)
|
||||
}
|
||||
r.Triggers = append(r.Triggers, t)
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
func (r *AutomationWorkflow) RBACParts() (resource string, ref *Ref, path []*Ref) {
|
||||
ref = r.Ref()
|
||||
path = nil
|
||||
resource = fmt.Sprintf(types.WorkflowRbacResourceTpl(), types.WorkflowResourceType, firstOkString(strconv.FormatUint(r.Res.ID, 10), r.Res.Handle))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *AutomationWorkflow) ResourceTranslationParts() (resource string, ref *Ref, path []*Ref) {
|
||||
ref = r.Ref()
|
||||
path = nil
|
||||
resource = fmt.Sprintf(types.WorkflowResourceTranslationTpl(), types.WorkflowResourceTranslationType, firstOkString(strconv.FormatUint(r.Res.ID, 10), r.Res.Handle))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *AutomationWorkflow) AddAutomationWorkflowStep(res *types.WorkflowStep) *AutomationWorkflowStep {
|
||||
s := &AutomationWorkflowStep{
|
||||
base: &base{},
|
||||
}
|
||||
|
||||
s.Res = res
|
||||
|
||||
if r.Steps == nil {
|
||||
r.Steps = make([]*AutomationWorkflowStep, 0, 100)
|
||||
}
|
||||
r.Steps = append(r.Steps, s)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (r *AutomationWorkflow) AddAutomationWorkflowPath(res *types.WorkflowPath) *AutomationWorkflowPath {
|
||||
p := &AutomationWorkflowPath{
|
||||
base: &base{},
|
||||
}
|
||||
|
||||
p.Res = res
|
||||
|
||||
if r.Paths == nil {
|
||||
r.Paths = make([]*AutomationWorkflowPath, 0, 100)
|
||||
}
|
||||
r.Paths = append(r.Paths, p)
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func (r *AutomationWorkflow) SysID() uint64 {
|
||||
return r.Res.ID
|
||||
}
|
||||
|
||||
// FindAutomationWorkflow looks for the workflow in the resource set
|
||||
func FindAutomationWorkflow(rr InterfaceSet, ii Identifiers) (ns *types.Workflow) {
|
||||
var wfRes *AutomationWorkflow
|
||||
|
||||
rr.Walk(func(r Interface) error {
|
||||
wr, ok := r.(*AutomationWorkflow)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if wr.Identifiers().HasAny(ii) {
|
||||
wfRes = wr
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Found it
|
||||
if wfRes != nil {
|
||||
return wfRes.Res
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func AutomationWorkflowErrUnresolved(ii Identifiers) error {
|
||||
return fmt.Errorf("automation workflow unresolved %v", ii.StringSlice())
|
||||
}
|
||||
114
server/pkg/envoy/resource/compose_chart.go
Normal file
114
server/pkg/envoy/resource/compose_chart.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
)
|
||||
|
||||
type (
|
||||
// ComposeChart represents a ComposeChart
|
||||
ComposeChart struct {
|
||||
*base
|
||||
Res *types.Chart
|
||||
|
||||
// Might keep track of related namespace
|
||||
RefNs *Ref
|
||||
RefMods RefSet
|
||||
}
|
||||
)
|
||||
|
||||
func NewComposeChart(res *types.Chart, nsRef *Ref, mmRef RefSet) *ComposeChart {
|
||||
r := &ComposeChart{
|
||||
base: &base{},
|
||||
RefMods: make(RefSet, len(mmRef)),
|
||||
}
|
||||
r.SetResourceType(types.ChartResourceType)
|
||||
r.Res = res
|
||||
|
||||
r.AddIdentifier(identifiers(res.Handle, res.Name, res.ID)...)
|
||||
|
||||
r.RefNs = r.addRef(nsRef)
|
||||
for i, mRef := range mmRef {
|
||||
r.RefMods[i] = r.addRef(mRef).Constraint(r.RefNs)
|
||||
}
|
||||
|
||||
// Initial timestamps
|
||||
r.SetTimestamps(MakeTimestampsCUDA(&res.CreatedAt, res.UpdatedAt, res.DeletedAt, nil))
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *ComposeChart) Resource() interface{} {
|
||||
return r.Res
|
||||
}
|
||||
|
||||
func (r *ComposeChart) ReRef(old RefSet, new RefSet) {
|
||||
r.base.ReRef(old, new)
|
||||
|
||||
// Additional references
|
||||
for _, n := range new {
|
||||
if n.ResourceType == types.NamespaceResourceType {
|
||||
r.RefNs = MakeRef(types.NamespaceResourceType, n.Identifiers)
|
||||
}
|
||||
}
|
||||
|
||||
for i, o := range old {
|
||||
if o.ResourceType == types.ModuleResourceType {
|
||||
r.RefMods = r.RefMods.replaceRef(o, new[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ComposeChart) SysID() uint64 {
|
||||
return r.Res.ID
|
||||
}
|
||||
|
||||
func (r *ComposeChart) resourceParts(tpl string) (resource string, ref *Ref, path []*Ref) {
|
||||
ref = r.Ref().Constraint(r.RefNs)
|
||||
path = []*Ref{r.RefNs}
|
||||
resource = fmt.Sprintf(types.ChartRbacResourceTpl(), types.ChartResourceType, r.RefNs.Identifiers.First(), firstOkString(strconv.FormatUint(r.Res.ID, 10), r.Res.Handle))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *ComposeChart) RBACParts() (resource string, ref *Ref, path []*Ref) {
|
||||
return r.resourceParts(types.ChartRbacResourceTpl())
|
||||
}
|
||||
|
||||
func (r *ComposeChart) ResourceTranslationParts() (resource string, ref *Ref, path []*Ref) {
|
||||
return r.resourceParts(types.ChartResourceTranslationTpl())
|
||||
}
|
||||
|
||||
func (r *ComposeChart) encodeTranslations() ([]*ResourceTranslation, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// FindComposeChart looks for the chart in the resources
|
||||
func FindComposeChart(rr InterfaceSet, ii Identifiers) (ch *types.Chart) {
|
||||
var chRes *ComposeChart
|
||||
|
||||
rr.Walk(func(r Interface) error {
|
||||
cr, ok := r.(*ComposeChart)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if cr.Identifiers().HasAny(ii) {
|
||||
chRes = cr
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Found it
|
||||
if chRes != nil {
|
||||
return chRes.Res
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ComposeChartErrUnresolved(ii Identifiers) error {
|
||||
return fmt.Errorf("compose chart unresolved %v", ii.StringSlice())
|
||||
}
|
||||
220
server/pkg/envoy/resource/compose_layout.go
Normal file
220
server/pkg/envoy/resource/compose_layout.go
Normal file
@@ -0,0 +1,220 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
automationTypes "github.com/cortezaproject/corteza/server/automation/types"
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
)
|
||||
|
||||
type (
|
||||
// ComposePageLayout represents a ComposePageLayout
|
||||
ComposePageLayout struct {
|
||||
*base
|
||||
Res *types.PageLayout
|
||||
|
||||
RefNs *Ref
|
||||
RefMod *Ref
|
||||
RefParent *Ref
|
||||
|
||||
WfRefs RefSet
|
||||
ModRefs RefSet
|
||||
RefCharts RefSet
|
||||
|
||||
BlockRefs map[int]RefSet
|
||||
}
|
||||
)
|
||||
|
||||
func NewComposePageLayout(pg *types.PageLayout, nsRef, modRef, parentRef *Ref) *ComposePageLayout {
|
||||
r := &ComposePageLayout{
|
||||
base: &base{},
|
||||
WfRefs: make(RefSet, 0, 10),
|
||||
ModRefs: make(RefSet, 0, 10),
|
||||
RefCharts: make(RefSet, 0, 10),
|
||||
BlockRefs: make(map[int]RefSet),
|
||||
}
|
||||
r.SetResourceType(types.PageLayoutResourceType)
|
||||
r.Res = pg
|
||||
|
||||
r.AddIdentifier(identifiers(pg.Handle, pg.Meta.Title, pg.ID)...)
|
||||
|
||||
r.RefNs = r.addRef(nsRef)
|
||||
if modRef != nil {
|
||||
r.RefMod = r.addRef(modRef).Constraint(r.RefNs)
|
||||
}
|
||||
|
||||
if parentRef != nil {
|
||||
r.RefParent = r.addRef(parentRef).Constraint(r.RefNs)
|
||||
}
|
||||
|
||||
// Initial timestamps
|
||||
r.SetTimestamps(MakeTimestampsCUDA(&pg.CreatedAt, pg.UpdatedAt, pg.DeletedAt, nil))
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func UnpackComposePageLayout(p *types.PageLayout) (*types.PageLayout, *Ref, *Ref) {
|
||||
var modRef *Ref
|
||||
var parentRef *Ref
|
||||
|
||||
return p, modRef, parentRef
|
||||
}
|
||||
|
||||
func (r *ComposePageLayout) Resource() interface{} {
|
||||
return r.Res
|
||||
}
|
||||
|
||||
func (r *ComposePageLayout) ReRef(old RefSet, new RefSet) {
|
||||
r.base.ReRef(old, new)
|
||||
|
||||
for _, n := range new {
|
||||
switch n.ResourceType {
|
||||
case types.NamespaceResourceType:
|
||||
r.RefNs = MakeRef(types.NamespaceResourceType, n.Identifiers)
|
||||
case types.ModuleResourceType:
|
||||
r.RefMod = MakeRef(types.ModuleResourceType, n.Identifiers)
|
||||
case types.PageLayoutResourceType:
|
||||
r.RefParent = MakeRef(types.PageLayoutResourceType, n.Identifiers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ComposePageLayout) Prune(ref *Ref) {
|
||||
}
|
||||
|
||||
func (r *ComposePageLayout) SysID() uint64 {
|
||||
return r.Res.ID
|
||||
}
|
||||
|
||||
func (r *ComposePageLayout) resourceParts(tpl string) (resource string, ref *Ref, path []*Ref) {
|
||||
ref = r.Ref().Constraint(r.RefNs)
|
||||
path = []*Ref{r.RefNs}
|
||||
resource = fmt.Sprintf(types.PageLayoutRbacResourceTpl(), types.PageLayoutResourceType, r.RefNs.Identifiers.First(), firstOkString(strconv.FormatUint(r.Res.ID, 10), r.Res.Handle))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *ComposePageLayout) RBACParts() (resource string, ref *Ref, path []*Ref) {
|
||||
return r.resourceParts(types.PageLayoutRbacResourceTpl())
|
||||
}
|
||||
|
||||
func (r *ComposePageLayout) ResourceTranslationParts() (resource string, ref *Ref, path []*Ref) {
|
||||
return r.resourceParts(types.PageLayoutResourceTranslationTpl())
|
||||
}
|
||||
|
||||
func (r *ComposePageLayout) encodeTranslations() ([]*ResourceTranslation, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// FindComposePageLayout looks for the page in the resources
|
||||
func FindComposePageLayout(rr InterfaceSet, ii Identifiers) (pg *types.PageLayout) {
|
||||
var pgRes *ComposePageLayout
|
||||
|
||||
rr.Walk(func(r Interface) error {
|
||||
pr, ok := r.(*ComposePageLayout)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if pr.Identifiers().HasAny(ii) {
|
||||
pgRes = pr
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Found it
|
||||
if pgRes != nil {
|
||||
return pgRes.Res
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ComposePageLayoutErrUnresolved(ii Identifiers) error {
|
||||
return fmt.Errorf("compose page unresolved %v", ii.StringSlice())
|
||||
}
|
||||
|
||||
// PageLayout block utilities
|
||||
func (r *ComposePageLayout) removeBlock(i int) {
|
||||
// do the swap remove thing
|
||||
r.Res.Blocks[i] = r.Res.Blocks[len(r.Res.Blocks)-1]
|
||||
r.Res.Blocks = r.Res.Blocks[:len(r.Res.Blocks)-1]
|
||||
|
||||
// correct block refs.
|
||||
// +1 here because it's already cut
|
||||
r.BlockRefs[i] = r.BlockRefs[len(r.Res.Blocks)]
|
||||
delete(r.BlockRefs, len(r.Res.Blocks))
|
||||
}
|
||||
|
||||
func (r *ComposePageLayout) optString(opt map[string]interface{}, kk ...string) string {
|
||||
for _, k := range kk {
|
||||
if vr, has := opt[k]; has {
|
||||
v, _ := vr.(string)
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (r *ComposePageLayout) pbRecordList(opt map[string]interface{}) (out *Ref) {
|
||||
id := r.optString(opt, "module", "moduleID")
|
||||
if id == "" || id == "0" {
|
||||
return
|
||||
}
|
||||
|
||||
return MakeRef(types.ModuleResourceType, MakeIdentifiers(id)).Constraint(r.RefNs)
|
||||
}
|
||||
|
||||
func (r *ComposePageLayout) pbComment(opt map[string]interface{}) (out *Ref) {
|
||||
id := r.optString(opt, "module", "moduleID")
|
||||
if id == "" || id == "0" {
|
||||
return
|
||||
}
|
||||
|
||||
return MakeRef(types.ModuleResourceType, MakeIdentifiers(id)).Constraint(r.RefNs)
|
||||
}
|
||||
|
||||
func (r *ComposePageLayout) pbAutomation(opt map[string]interface{}) (out *Ref) {
|
||||
id := r.optString(opt, "workflow", "workflowID")
|
||||
if id == "" || id == "0" {
|
||||
return
|
||||
}
|
||||
|
||||
return MakeRef(automationTypes.WorkflowResourceType, MakeIdentifiers(id))
|
||||
}
|
||||
|
||||
func (r *ComposePageLayout) pbRecordOrganizer(opt map[string]interface{}) (out *Ref) {
|
||||
id := r.optString(opt, "module", "moduleID")
|
||||
if id == "" || id == "0" {
|
||||
return
|
||||
}
|
||||
|
||||
return MakeRef(types.ModuleResourceType, MakeIdentifiers(id)).Constraint(r.RefNs)
|
||||
}
|
||||
|
||||
func (r *ComposePageLayout) pbChart(opt map[string]interface{}) (out *Ref) {
|
||||
id := r.optString(opt, "chart", "chartID")
|
||||
if id == "" || id == "0" {
|
||||
return
|
||||
}
|
||||
|
||||
return MakeRef(types.ChartResourceType, MakeIdentifiers(id)).Constraint(r.RefNs)
|
||||
}
|
||||
|
||||
func (r *ComposePageLayout) pbCalendar(opt map[string]interface{}) (out *Ref) {
|
||||
id := r.optString(opt, "module", "moduleID")
|
||||
if id == "" || id == "0" {
|
||||
return
|
||||
}
|
||||
|
||||
return MakeRef(types.ModuleResourceType, MakeIdentifiers(id)).Constraint(r.RefNs)
|
||||
}
|
||||
|
||||
func (r *ComposePageLayout) pbMetric(opt map[string]interface{}) (out *Ref) {
|
||||
id := r.optString(opt, "module", "moduleID")
|
||||
if id == "" || id == "0" {
|
||||
return
|
||||
}
|
||||
|
||||
return MakeRef(types.ModuleResourceType, MakeIdentifiers(id)).Constraint(r.RefNs)
|
||||
}
|
||||
308
server/pkg/envoy/resource/compose_module.go
Normal file
308
server/pkg/envoy/resource/compose_module.go
Normal file
@@ -0,0 +1,308 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
systemTypes "github.com/cortezaproject/corteza/server/system/types"
|
||||
"github.com/modern-go/reflect2"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
type (
|
||||
ComposeModuleField struct {
|
||||
*base
|
||||
Res *types.ModuleField
|
||||
|
||||
RefNs *Ref
|
||||
RefMod *Ref
|
||||
}
|
||||
|
||||
ComposeModule struct {
|
||||
*base
|
||||
Res *types.Module
|
||||
ResFields []*ComposeModuleField
|
||||
|
||||
// Might keep track of related NS
|
||||
RefNs *Ref
|
||||
RefMods RefSet
|
||||
RefRoles RefSet
|
||||
}
|
||||
)
|
||||
|
||||
func NewComposeModule(res *types.Module, nsRef *Ref) *ComposeModule {
|
||||
r := &ComposeModule{
|
||||
base: &base{},
|
||||
RefMods: make(RefSet, 0, len(res.Fields)),
|
||||
}
|
||||
r.SetResourceType(types.ModuleResourceType)
|
||||
r.Res = res
|
||||
|
||||
r.AddIdentifier(identifiers(res.Handle, res.Name, res.ID)...)
|
||||
|
||||
r.RefNs = r.addRef(nsRef)
|
||||
|
||||
// Field deps
|
||||
for _, f := range res.Fields {
|
||||
switch f.Kind {
|
||||
case "Record":
|
||||
refMod := f.Options.String("module")
|
||||
if refMod == "" {
|
||||
refMod = f.Options.String("moduleID")
|
||||
}
|
||||
if refMod != "" && refMod != "0" {
|
||||
r.RefMods = append(r.RefMods, r.AddRef(types.ModuleResourceType, refMod).Constraint(r.RefNs))
|
||||
}
|
||||
|
||||
case "User":
|
||||
refRoles := ComposeModuleFieldExtractUserFieldRoles(f.Options["roles"])
|
||||
if len(refRoles) == 0 {
|
||||
refRoles = ComposeModuleFieldExtractUserFieldRoles(f.Options["role"])
|
||||
}
|
||||
if len(refRoles) == 0 {
|
||||
refRoles = ComposeModuleFieldExtractUserFieldRoles(f.Options["roleID"])
|
||||
}
|
||||
|
||||
for _, refRole := range refRoles {
|
||||
r.RefRoles = append(r.RefRoles, r.AddRef(systemTypes.RoleResourceType, refRole))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initial timestamps
|
||||
r.SetTimestamps(MakeTimestampsCUDA(&res.CreatedAt, res.UpdatedAt, res.DeletedAt, nil))
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *ComposeModule) Resource() interface{} {
|
||||
return r.Res
|
||||
}
|
||||
|
||||
func (r *ComposeModule) ReRef(old RefSet, new RefSet) {
|
||||
r.base.ReRef(old, new)
|
||||
|
||||
// Additional references
|
||||
for _, n := range new {
|
||||
if n.ResourceType == types.NamespaceResourceType {
|
||||
r.RefNs = MakeRef(types.NamespaceResourceType, n.Identifiers)
|
||||
}
|
||||
}
|
||||
|
||||
for i, o := range old {
|
||||
if o.ResourceType == types.ModuleResourceType {
|
||||
r.RefMods = r.RefMods.replaceRef(o, new[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ComposeModule) SysID() uint64 {
|
||||
return r.Res.ID
|
||||
}
|
||||
|
||||
func (r *ComposeModule) resourceParts(tpl string) (resource string, ref *Ref, path []*Ref) {
|
||||
ref = r.Ref().Constraint(r.RefNs)
|
||||
path = []*Ref{r.RefNs}
|
||||
resource = fmt.Sprintf(tpl, types.ModuleResourceType, r.RefNs.Identifiers.First(), firstOkString(strconv.FormatUint(r.Res.ID, 10), r.Res.Handle))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *ComposeModule) RBACParts() (resource string, ref *Ref, path []*Ref) {
|
||||
return r.resourceParts(types.ModuleRbacResourceTpl())
|
||||
}
|
||||
|
||||
func (r *ComposeModule) ResourceTranslationParts() (resource string, ref *Ref, path []*Ref) {
|
||||
return r.resourceParts(types.ModuleResourceTranslationTpl())
|
||||
}
|
||||
|
||||
func (r *ComposeModule) encodeTranslations() ([]*ResourceTranslation, error) {
|
||||
out := make([]*ResourceTranslation, 0, len(r.ResFields))
|
||||
|
||||
for _, f := range r.ResFields {
|
||||
rr := f.Res.EncodeTranslations()
|
||||
rr.SetLanguage(defaultLanguage)
|
||||
res, ref, pp := f.ResourceTranslationParts()
|
||||
out = append(out, NewResourceTranslation(systemTypes.FromLocale(rr), res, ref, pp...))
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// FindComposeModule looks for the module in the resource set
|
||||
func FindComposeModule(rr InterfaceSet, ii Identifiers) (ns *types.Module) {
|
||||
var modRes *ComposeModule
|
||||
|
||||
rr.Walk(func(r Interface) error {
|
||||
mr, ok := r.(*ComposeModule)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if mr.Identifiers().HasAny(ii) {
|
||||
modRes = mr
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Found it
|
||||
if modRes != nil {
|
||||
return modRes.Res
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ComposeModule) AddField(f *ComposeModuleField) {
|
||||
r.ResFields = append(r.ResFields, f)
|
||||
}
|
||||
|
||||
// FindComposeModule looks for the module in the resource set
|
||||
func FindComposeModuleField(rr InterfaceSet, mod, ii Identifiers) (field *types.ModuleField) {
|
||||
var fieldRes *ComposeModuleField
|
||||
|
||||
rr.Walk(func(r Interface) error {
|
||||
mr, ok := r.(*ComposeModule)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !mr.Identifiers().HasAny(mod) {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, rf := range mr.ResFields {
|
||||
if rf.Identifiers().HasAny(ii) {
|
||||
fieldRes = rf
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Found it
|
||||
if fieldRes != nil {
|
||||
return fieldRes.Res
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ComposeModuleField) resourceParts(tpl string) (resource string, ref *Ref, path []*Ref) {
|
||||
ref = r.Ref().Constraint(r.RefNs).Constraint(r.RefMod)
|
||||
path = []*Ref{r.RefNs, r.RefMod}
|
||||
resource = fmt.Sprintf(types.ModuleFieldResourceTranslationTpl(), types.ModuleFieldResourceTranslationType, r.RefNs.Identifiers.First(), r.RefMod.Identifiers.First(), firstOkString(strconv.FormatUint(r.Res.ID, 10), r.Res.Name))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *ComposeModuleField) RBACParts() (resource string, ref *Ref, path []*Ref) {
|
||||
return r.resourceParts(types.ModuleFieldRbacResourceTpl())
|
||||
}
|
||||
|
||||
func (r *ComposeModuleField) ResourceTranslationParts() (resource string, ref *Ref, path []*Ref) {
|
||||
return r.resourceParts(types.ModuleFieldResourceTranslationTpl())
|
||||
}
|
||||
|
||||
func (r *ComposeModuleField) ResourceTranslations() ([]*ResourceTranslation, error) {
|
||||
out := make([]*ResourceTranslation, 0, 10)
|
||||
|
||||
rr := r.Res.EncodeTranslations()
|
||||
rr.SetLanguage(defaultLanguage)
|
||||
res, ref, pp := r.ResourceTranslationParts()
|
||||
out = append(out, NewResourceTranslation(systemTypes.FromLocale(rr), res, ref, pp...))
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func ComposeModuleErrUnresolved(ii Identifiers) error {
|
||||
return fmt.Errorf("compose module unresolved %v", ii.StringSlice())
|
||||
}
|
||||
|
||||
func ComposeModuleFieldErrUnresolved(ii Identifiers) error {
|
||||
return fmt.Errorf("compose module field unresolved %v", ii.StringSlice())
|
||||
}
|
||||
|
||||
func NewComposeModuleField(res *types.ModuleField, nsRef, modRef *Ref) *ComposeModuleField {
|
||||
r := &ComposeModuleField{
|
||||
base: &base{},
|
||||
}
|
||||
r.SetResourceType(types.ModuleFieldResourceType)
|
||||
r.Res = res
|
||||
|
||||
r.AddIdentifier(identifiers(res.Name, res.ID)...)
|
||||
|
||||
r.RefNs = r.addRef(nsRef)
|
||||
r.RefMod = r.addRef(modRef).Constraint(r.RefNs)
|
||||
|
||||
// Initial timestamps
|
||||
r.SetTimestamps(MakeTimestampsCUDA(&res.CreatedAt, res.UpdatedAt, res.DeletedAt, nil))
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *ComposeModuleField) Resource() interface{} {
|
||||
return r.Res
|
||||
}
|
||||
|
||||
// ComposeModuleFieldExtractUserFieldRoles is a helper to extract roles
|
||||
// from the given filer options.
|
||||
func ComposeModuleFieldExtractUserFieldRoles(i interface{}) []string {
|
||||
if reflect2.IsNil(i) {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make([]string, 0, 1)
|
||||
|
||||
isOk := func(v string) bool {
|
||||
return v != "" && v != "0"
|
||||
}
|
||||
|
||||
switch v := i.(type) {
|
||||
case uint64:
|
||||
aux := strconv.FormatUint(v, 10)
|
||||
if !isOk(aux) {
|
||||
return nil
|
||||
}
|
||||
return []string{aux}
|
||||
case []uint64:
|
||||
for _, i := range v {
|
||||
aux := strconv.FormatUint(i, 10)
|
||||
if !isOk(aux) {
|
||||
continue
|
||||
}
|
||||
out = append(out, aux)
|
||||
}
|
||||
return out
|
||||
|
||||
case string:
|
||||
if !isOk(v) {
|
||||
return nil
|
||||
}
|
||||
return []string{v}
|
||||
case []string:
|
||||
for _, aux := range v {
|
||||
if !isOk(aux) {
|
||||
continue
|
||||
}
|
||||
out = append(out, aux)
|
||||
}
|
||||
return out
|
||||
|
||||
case []interface{}:
|
||||
for _, i := range v {
|
||||
aux := cast.ToString(i)
|
||||
if !isOk(aux) {
|
||||
continue
|
||||
}
|
||||
out = append(out, aux)
|
||||
}
|
||||
return out
|
||||
case interface{}:
|
||||
aux := cast.ToString(v)
|
||||
if !isOk(aux) {
|
||||
return nil
|
||||
}
|
||||
return []string{aux}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
80
server/pkg/envoy/resource/compose_namespace.go
Normal file
80
server/pkg/envoy/resource/compose_namespace.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
)
|
||||
|
||||
type (
|
||||
ComposeNamespace struct {
|
||||
*base
|
||||
Res *types.Namespace
|
||||
}
|
||||
)
|
||||
|
||||
func NewComposeNamespace(ns *types.Namespace) *ComposeNamespace {
|
||||
r := &ComposeNamespace{base: &base{}}
|
||||
r.SetResourceType(types.NamespaceResourceType)
|
||||
r.Res = ns
|
||||
|
||||
r.AddIdentifier(identifiers(ns.Slug, ns.Name, ns.ID)...)
|
||||
|
||||
// Initial timestamps
|
||||
r.SetTimestamps(MakeTimestampsCUDA(&ns.CreatedAt, ns.UpdatedAt, ns.DeletedAt, nil))
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *ComposeNamespace) Resource() interface{} {
|
||||
return r.Res
|
||||
}
|
||||
|
||||
func (r *ComposeNamespace) RBACParts() (resource string, ref *Ref, path []*Ref) {
|
||||
ref = r.Ref()
|
||||
path = nil
|
||||
resource = fmt.Sprintf(types.NamespaceRbacResourceTpl(), types.NamespaceResourceType, firstOkString(strconv.FormatUint(r.Res.ID, 10), r.Res.Slug))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *ComposeNamespace) ResourceTranslationParts() (resource string, ref *Ref, path []*Ref) {
|
||||
ref = r.Ref()
|
||||
path = nil
|
||||
resource = fmt.Sprintf(types.NamespaceResourceTranslationTpl(), types.NamespaceResourceTranslationType, firstOkString(strconv.FormatUint(r.Res.ID, 10), r.Res.Slug))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *ComposeNamespace) SysID() uint64 {
|
||||
return r.Res.ID
|
||||
}
|
||||
|
||||
// FindComposeNamespace looks for the namespace in the resource set
|
||||
func FindComposeNamespace(rr InterfaceSet, ii Identifiers) (ns *types.Namespace) {
|
||||
var nsRes *ComposeNamespace
|
||||
|
||||
rr.Walk(func(r Interface) error {
|
||||
nr, ok := r.(*ComposeNamespace)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if nr.Identifiers().HasAny(ii) {
|
||||
nsRes = nr
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Found it
|
||||
if nsRes != nil {
|
||||
return nsRes.Res
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ComposeNamespaceErrUnresolved(ii Identifiers) error {
|
||||
return fmt.Errorf("compose namespace unresolved %v", ii.StringSlice())
|
||||
}
|
||||
340
server/pkg/envoy/resource/compose_page.go
Normal file
340
server/pkg/envoy/resource/compose_page.go
Normal file
@@ -0,0 +1,340 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
automationTypes "github.com/cortezaproject/corteza/server/automation/types"
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
)
|
||||
|
||||
type (
|
||||
// ComposePage represents a ComposePage
|
||||
ComposePage struct {
|
||||
*base
|
||||
Res *types.Page
|
||||
|
||||
RefNs *Ref
|
||||
RefMod *Ref
|
||||
RefParent *Ref
|
||||
|
||||
WfRefs RefSet
|
||||
ModRefs RefSet
|
||||
RefCharts RefSet
|
||||
|
||||
BlockRefs map[int]RefSet
|
||||
}
|
||||
)
|
||||
|
||||
func NewComposePage(pg *types.Page, nsRef, modRef, parentRef *Ref) *ComposePage {
|
||||
r := &ComposePage{
|
||||
base: &base{},
|
||||
WfRefs: make(RefSet, 0, 10),
|
||||
ModRefs: make(RefSet, 0, 10),
|
||||
RefCharts: make(RefSet, 0, 10),
|
||||
BlockRefs: make(map[int]RefSet),
|
||||
}
|
||||
r.SetResourceType(types.PageResourceType)
|
||||
r.Res = pg
|
||||
|
||||
r.AddIdentifier(identifiers(pg.Handle, pg.Title, pg.ID)...)
|
||||
|
||||
r.RefNs = r.addRef(nsRef)
|
||||
if modRef != nil {
|
||||
r.RefMod = r.addRef(modRef).Constraint(r.RefNs)
|
||||
}
|
||||
|
||||
if parentRef != nil {
|
||||
r.RefParent = r.addRef(parentRef).Constraint(r.RefNs)
|
||||
}
|
||||
|
||||
var ref *Ref
|
||||
for i, b := range pg.Blocks {
|
||||
switch b.Kind {
|
||||
case "RecordList":
|
||||
ref = r.pbRecordList(b.Options)
|
||||
if ref != nil {
|
||||
r.addRef(ref)
|
||||
r.BlockRefs[i] = append(r.BlockRefs[i], ref)
|
||||
r.ModRefs = append(r.ModRefs, ref)
|
||||
}
|
||||
|
||||
case "Automation":
|
||||
bb, _ := b.Options["buttons"].([]interface{})
|
||||
for _, b := range bb {
|
||||
button, _ := b.(map[string]interface{})
|
||||
ref = r.pbAutomation(button)
|
||||
if ref != nil {
|
||||
r.addRef(ref)
|
||||
r.BlockRefs[i] = append(r.BlockRefs[i], ref)
|
||||
r.WfRefs = append(r.WfRefs, ref)
|
||||
}
|
||||
}
|
||||
|
||||
case "RecordOrganizer":
|
||||
ref = r.pbRecordList(b.Options)
|
||||
if ref != nil {
|
||||
r.addRef(ref)
|
||||
r.BlockRefs[i] = append(r.BlockRefs[i], ref)
|
||||
r.ModRefs = append(r.ModRefs, ref)
|
||||
}
|
||||
|
||||
case "Chart":
|
||||
ref = r.pbChart(b.Options)
|
||||
if ref != nil {
|
||||
r.addRef(ref)
|
||||
r.BlockRefs[i] = append(r.BlockRefs[i], ref)
|
||||
r.RefCharts = append(r.RefCharts, ref)
|
||||
}
|
||||
|
||||
case "Calendar":
|
||||
ff, _ := b.Options["feeds"].([]interface{})
|
||||
for _, f := range ff {
|
||||
feed, _ := f.(map[string]interface{})
|
||||
fOpts, _ := (feed["options"]).(map[string]interface{})
|
||||
|
||||
ref = r.pbCalendar(fOpts)
|
||||
if ref != nil {
|
||||
r.addRef(ref)
|
||||
r.BlockRefs[i] = append(r.BlockRefs[i], ref)
|
||||
r.ModRefs = append(r.ModRefs, ref)
|
||||
}
|
||||
}
|
||||
|
||||
case "Metric":
|
||||
mm, _ := b.Options["metrics"].([]interface{})
|
||||
for _, m := range mm {
|
||||
mops, _ := m.(map[string]interface{})
|
||||
ref = r.pbMetric(mops)
|
||||
if ref != nil {
|
||||
r.addRef(ref)
|
||||
r.BlockRefs[i] = append(r.BlockRefs[i], ref)
|
||||
r.ModRefs = append(r.ModRefs, ref)
|
||||
}
|
||||
}
|
||||
|
||||
case "Comment":
|
||||
ref = r.pbComment(b.Options)
|
||||
if ref != nil {
|
||||
r.addRef(ref.Constraint(r.RefNs))
|
||||
r.BlockRefs[i] = append(r.BlockRefs[i], ref)
|
||||
r.ModRefs = append(r.ModRefs, ref)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initial timestamps
|
||||
r.SetTimestamps(MakeTimestampsCUDA(&pg.CreatedAt, pg.UpdatedAt, pg.DeletedAt, nil))
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func UnpackComposePage(p *types.Page) (*types.Page, *Ref, *Ref) {
|
||||
var modRef *Ref
|
||||
var parentRef *Ref
|
||||
|
||||
if p.ModuleID != 0 {
|
||||
modRef = MakeModuleRef(p.ModuleID, "", "")
|
||||
}
|
||||
if p.SelfID != 0 {
|
||||
parentRef = MakePageRef(p.SelfID, "", "")
|
||||
}
|
||||
|
||||
return p, modRef, parentRef
|
||||
}
|
||||
|
||||
func (r *ComposePage) Resource() interface{} {
|
||||
return r.Res
|
||||
}
|
||||
|
||||
func (r *ComposePage) ReRef(old RefSet, new RefSet) {
|
||||
r.base.ReRef(old, new)
|
||||
|
||||
for _, n := range new {
|
||||
switch n.ResourceType {
|
||||
case types.NamespaceResourceType:
|
||||
r.RefNs = MakeRef(types.NamespaceResourceType, n.Identifiers)
|
||||
case types.ModuleResourceType:
|
||||
r.RefMod = MakeRef(types.ModuleResourceType, n.Identifiers)
|
||||
case types.PageResourceType:
|
||||
r.RefParent = MakeRef(types.PageResourceType, n.Identifiers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ComposePage) Prune(ref *Ref) {
|
||||
var auxRef *Ref
|
||||
|
||||
outer:
|
||||
for i := len(r.Res.Blocks) - 1; i >= 0; i-- {
|
||||
b := r.Res.Blocks[i]
|
||||
|
||||
switch b.Kind {
|
||||
// Implement the rest when support is needed
|
||||
case "Automation":
|
||||
if b.Options["buttons"] == nil {
|
||||
// In case the block isn't connected to a workflow (placeholder, script)
|
||||
if auxRef == nil {
|
||||
r.removeBlock(i)
|
||||
continue outer
|
||||
}
|
||||
} else {
|
||||
bb, _ := b.Options["buttons"].([]interface{})
|
||||
for _, b := range bb {
|
||||
button, _ := b.(map[string]interface{})
|
||||
auxRef = r.pbAutomation(button)
|
||||
|
||||
// In case the block isn't connected to a workflow (placeholder, script)
|
||||
if auxRef == nil {
|
||||
r.removeBlock(i)
|
||||
continue outer
|
||||
}
|
||||
|
||||
// In case we are removing it
|
||||
if auxRef.equals(ref) {
|
||||
r.ReplaceRef(ref, nil)
|
||||
r.WfRefs = r.WfRefs.replaceRef(ref, nil)
|
||||
r.removeBlock(i)
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ComposePage) SysID() uint64 {
|
||||
return r.Res.ID
|
||||
}
|
||||
|
||||
func (r *ComposePage) resourceParts(tpl string) (resource string, ref *Ref, path []*Ref) {
|
||||
ref = r.Ref().Constraint(r.RefNs)
|
||||
path = []*Ref{r.RefNs}
|
||||
resource = fmt.Sprintf(types.PageRbacResourceTpl(), types.PageResourceType, r.RefNs.Identifiers.First(), firstOkString(strconv.FormatUint(r.Res.ID, 10), r.Res.Handle))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *ComposePage) RBACParts() (resource string, ref *Ref, path []*Ref) {
|
||||
return r.resourceParts(types.PageRbacResourceTpl())
|
||||
}
|
||||
|
||||
func (r *ComposePage) ResourceTranslationParts() (resource string, ref *Ref, path []*Ref) {
|
||||
return r.resourceParts(types.PageResourceTranslationTpl())
|
||||
}
|
||||
|
||||
func (r *ComposePage) encodeTranslations() ([]*ResourceTranslation, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// FindComposePage looks for the page in the resources
|
||||
func FindComposePage(rr InterfaceSet, ii Identifiers) (pg *types.Page) {
|
||||
var pgRes *ComposePage
|
||||
|
||||
rr.Walk(func(r Interface) error {
|
||||
pr, ok := r.(*ComposePage)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if pr.Identifiers().HasAny(ii) {
|
||||
pgRes = pr
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Found it
|
||||
if pgRes != nil {
|
||||
return pgRes.Res
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ComposePageErrUnresolved(ii Identifiers) error {
|
||||
return fmt.Errorf("compose page unresolved %v", ii.StringSlice())
|
||||
}
|
||||
|
||||
// Page block utilities
|
||||
func (r *ComposePage) removeBlock(i int) {
|
||||
// do the swap remove thing
|
||||
r.Res.Blocks[i] = r.Res.Blocks[len(r.Res.Blocks)-1]
|
||||
r.Res.Blocks = r.Res.Blocks[:len(r.Res.Blocks)-1]
|
||||
|
||||
// correct block refs.
|
||||
// +1 here because it's already cut
|
||||
r.BlockRefs[i] = r.BlockRefs[len(r.Res.Blocks)]
|
||||
delete(r.BlockRefs, len(r.Res.Blocks))
|
||||
}
|
||||
|
||||
func (r *ComposePage) optString(opt map[string]interface{}, kk ...string) string {
|
||||
for _, k := range kk {
|
||||
if vr, has := opt[k]; has {
|
||||
v, _ := vr.(string)
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (r *ComposePage) pbRecordList(opt map[string]interface{}) (out *Ref) {
|
||||
id := r.optString(opt, "module", "moduleID")
|
||||
if id == "" || id == "0" {
|
||||
return
|
||||
}
|
||||
|
||||
return MakeRef(types.ModuleResourceType, MakeIdentifiers(id)).Constraint(r.RefNs)
|
||||
}
|
||||
|
||||
func (r *ComposePage) pbComment(opt map[string]interface{}) (out *Ref) {
|
||||
id := r.optString(opt, "module", "moduleID")
|
||||
if id == "" || id == "0" {
|
||||
return
|
||||
}
|
||||
|
||||
return MakeRef(types.ModuleResourceType, MakeIdentifiers(id)).Constraint(r.RefNs)
|
||||
}
|
||||
|
||||
func (r *ComposePage) pbAutomation(opt map[string]interface{}) (out *Ref) {
|
||||
id := r.optString(opt, "workflow", "workflowID")
|
||||
if id == "" || id == "0" {
|
||||
return
|
||||
}
|
||||
|
||||
return MakeRef(automationTypes.WorkflowResourceType, MakeIdentifiers(id))
|
||||
}
|
||||
|
||||
func (r *ComposePage) pbRecordOrganizer(opt map[string]interface{}) (out *Ref) {
|
||||
id := r.optString(opt, "module", "moduleID")
|
||||
if id == "" || id == "0" {
|
||||
return
|
||||
}
|
||||
|
||||
return MakeRef(types.ModuleResourceType, MakeIdentifiers(id)).Constraint(r.RefNs)
|
||||
}
|
||||
|
||||
func (r *ComposePage) pbChart(opt map[string]interface{}) (out *Ref) {
|
||||
id := r.optString(opt, "chart", "chartID")
|
||||
if id == "" || id == "0" {
|
||||
return
|
||||
}
|
||||
|
||||
return MakeRef(types.ChartResourceType, MakeIdentifiers(id)).Constraint(r.RefNs)
|
||||
}
|
||||
|
||||
func (r *ComposePage) pbCalendar(opt map[string]interface{}) (out *Ref) {
|
||||
id := r.optString(opt, "module", "moduleID")
|
||||
if id == "" || id == "0" {
|
||||
return
|
||||
}
|
||||
|
||||
return MakeRef(types.ModuleResourceType, MakeIdentifiers(id)).Constraint(r.RefNs)
|
||||
}
|
||||
|
||||
func (r *ComposePage) pbMetric(opt map[string]interface{}) (out *Ref) {
|
||||
id := r.optString(opt, "module", "moduleID")
|
||||
if id == "" || id == "0" {
|
||||
return
|
||||
}
|
||||
|
||||
return MakeRef(types.ModuleResourceType, MakeIdentifiers(id)).Constraint(r.RefNs)
|
||||
}
|
||||
115
server/pkg/envoy/resource/compose_record.go
Normal file
115
server/pkg/envoy/resource/compose_record.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
composeTypes "github.com/cortezaproject/corteza/server/compose/types"
|
||||
systemTypes "github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
rawSysValues struct {
|
||||
OwnedBy string
|
||||
CreatedAt string
|
||||
CreatedBy string
|
||||
UpdatedAt string
|
||||
UpdatedBy string
|
||||
DeletedAt string
|
||||
DeletedBy string
|
||||
}
|
||||
|
||||
ComposeRecordRaw struct {
|
||||
ID string
|
||||
Values map[string]ComposeRecordRawValue
|
||||
|
||||
Ts *Timestamps
|
||||
Us *Userstamps
|
||||
|
||||
Config *EnvoyConfig
|
||||
}
|
||||
ComposeRecordRawSet []*ComposeRecordRaw
|
||||
|
||||
ComposeRecordRawValue struct {
|
||||
Name string
|
||||
IsMulti bool
|
||||
|
||||
Value string
|
||||
Values []string
|
||||
}
|
||||
|
||||
CrsWalker func(f func(r *ComposeRecordRaw) error) error
|
||||
|
||||
ComposeRecord struct {
|
||||
*base
|
||||
|
||||
Walker CrsWalker
|
||||
|
||||
RefNs *Ref
|
||||
|
||||
RefMod *Ref
|
||||
RelMod *composeTypes.Module
|
||||
|
||||
IDMap map[string]uint64
|
||||
// UserFlakes help the system by predefining a set of potential sys user references.
|
||||
// This should make the operation cheaper for larger datasets.
|
||||
UserFlakes UserstampIndex
|
||||
}
|
||||
)
|
||||
|
||||
func NewComposeRecordSet(w CrsWalker, nsRef, modRef string) *ComposeRecord {
|
||||
r := &ComposeRecord{
|
||||
base: &base{},
|
||||
IDMap: make(map[string]uint64),
|
||||
}
|
||||
|
||||
r.SetResourceType(composeTypes.RecordResourceType)
|
||||
r.Walker = w
|
||||
|
||||
r.AddIdentifier(identifiers(modRef)...)
|
||||
|
||||
r.RefNs = r.AddRef(composeTypes.NamespaceResourceType, nsRef)
|
||||
r.RefMod = r.AddRef(composeTypes.ModuleResourceType, modRef).Constraint(r.RefNs)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *ComposeRecord) Resource() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ComposeRecord) SetUserFlakes(uu UserstampIndex) {
|
||||
r.UserFlakes = uu
|
||||
|
||||
// Set user refs as wildflag, indicating it refers to any user resource
|
||||
r.AddRef(systemTypes.UserResourceType, "*")
|
||||
}
|
||||
|
||||
func (r *ComposeRecord) RBACParts() (resource string, ref *Ref, path []*Ref) {
|
||||
ref = r.Ref()
|
||||
path = []*Ref{r.RefNs, r.RefMod}
|
||||
// @todo specific records
|
||||
resource = fmt.Sprintf(types.RecordRbacResourceTpl(), types.RecordResourceType, r.RefNs.Identifiers.First(), r.RefMod.Identifiers.First(), "*")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func FindComposeRecordResource(rr InterfaceSet, ii Identifiers) (rec *ComposeRecord) {
|
||||
rr.Walk(func(r Interface) error {
|
||||
crr, ok := r.(*ComposeRecord)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if crr.Identifiers().HasAny(ii) {
|
||||
rec = crr
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return rec
|
||||
}
|
||||
|
||||
func ComposeRecordErrUnresolved(ii Identifiers) error {
|
||||
return fmt.Errorf("compose record unresolved %v", ii.StringSlice())
|
||||
}
|
||||
205
server/pkg/envoy/resource/compose_record_shaper.go
Normal file
205
server/pkg/envoy/resource/compose_record_shaper.go
Normal file
@@ -0,0 +1,205 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type (
|
||||
// A simple wrapper struct for related resources
|
||||
composeRecordShaper struct{}
|
||||
)
|
||||
|
||||
// ComposeRecordShaper initializes and returns a new compose record resource shaper
|
||||
func ComposeRecordShaper() shaper {
|
||||
return &composeRecordShaper{}
|
||||
}
|
||||
|
||||
// Shape shapes ResourceDatasets based on the related compose template
|
||||
//
|
||||
// The first step finds the matching pair;
|
||||
// The second step creates an actual resource based on the two.
|
||||
func (crt *composeRecordShaper) Shape(rr []Interface) ([]Interface, error) {
|
||||
// This will do for most cases
|
||||
ii := make([]Interface, 0, int(len(rr)/2)+1)
|
||||
|
||||
for _, r := range rr {
|
||||
rt, ok := r.(*ComposeRecordTemplate)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
rd := findResourceDataset(rr, rt.Identifiers())
|
||||
if rd == nil {
|
||||
return nil, genericSourceErrUnresolved(rt.Identifiers())
|
||||
}
|
||||
|
||||
ii = append(ii, crt.toResource(rt, rd))
|
||||
}
|
||||
|
||||
return ii, nil
|
||||
}
|
||||
|
||||
func (r *composeRecordShaper) Resource() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crt *composeRecordShaper) toResource(def *ComposeRecordTemplate, dt *ResourceDataset) Interface {
|
||||
w := func(f func(r *ComposeRecordRaw) error) error {
|
||||
if err := dt.P.Reset(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
mr, err := dt.P.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if mr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get the bits in order
|
||||
vv := crt.mapValues(mr, def.FieldMap, def.Defaultable)
|
||||
auxVals := make(map[string]ComposeRecordRawValue)
|
||||
|
||||
for k, v := range vv {
|
||||
auxVals[k] = ComposeRecordRawValue{
|
||||
Name: k,
|
||||
Value: v,
|
||||
}
|
||||
}
|
||||
|
||||
rRaw := &ComposeRecordRaw{
|
||||
ID: crt.getKey(mr, def.Key),
|
||||
Values: auxVals,
|
||||
}
|
||||
|
||||
crt.getTimestamps(rRaw)
|
||||
crt.getUserstamps(rRaw)
|
||||
|
||||
// Process it
|
||||
err = f(rRaw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NewComposeRecordSet(w, def.NsRef.Identifiers.First(), def.ModRef.Identifiers.First())
|
||||
}
|
||||
|
||||
// mapValues maps original values based on the provided mapping
|
||||
func (crt *composeRecordShaper) mapValues(ov map[string]string, fm MappingTplSet, defaultable bool) map[string]string {
|
||||
nv := make(map[string]string)
|
||||
|
||||
// Mappings are provided as a slice since we'll want to do some additional conditional mappings.
|
||||
// We'll make an index for a nicer lookup for now.
|
||||
mx := make(map[string]*MappingTpl)
|
||||
for _, m := range fm {
|
||||
mx[m.Cell] = m
|
||||
}
|
||||
|
||||
if defaultable {
|
||||
for k, v := range ov {
|
||||
if m, has := mx[k]; has {
|
||||
if !m.IsIgnored() {
|
||||
nv[m.Field] = v
|
||||
}
|
||||
} else {
|
||||
nv[k] = v
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if len(mx) > 0 {
|
||||
for k, v := range ov {
|
||||
if m, has := mx[k]; has && !m.IsIgnored() {
|
||||
nv[m.Field] = v
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for k, v := range ov {
|
||||
nv[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nv
|
||||
}
|
||||
|
||||
func (crt *composeRecordShaper) getTimestamps(r *ComposeRecordRaw) {
|
||||
ts := &Timestamps{}
|
||||
// Provided values are already mapped
|
||||
for k, v := range crt.cloneValues(r) {
|
||||
switch strings.ToLower(k) {
|
||||
case "createdat",
|
||||
"created_at":
|
||||
ts.CreatedAt = MakeTimestamp(v.Value)
|
||||
delete(r.Values, k)
|
||||
|
||||
case "updatedat",
|
||||
"updated_at":
|
||||
ts.UpdatedAt = MakeTimestamp(v.Value)
|
||||
delete(r.Values, k)
|
||||
|
||||
case "deletedat",
|
||||
"deleted_at":
|
||||
ts.DeletedAt = MakeTimestamp(v.Value)
|
||||
delete(r.Values, k)
|
||||
}
|
||||
}
|
||||
|
||||
r.Ts = ts
|
||||
}
|
||||
|
||||
func (crt *composeRecordShaper) getUserstamps(r *ComposeRecordRaw) {
|
||||
us := &Userstamps{}
|
||||
// Provided values are already mapped
|
||||
for k, v := range crt.cloneValues(r) {
|
||||
switch strings.ToLower(k) {
|
||||
case "createdby",
|
||||
"creatorid",
|
||||
"creator":
|
||||
us.CreatedBy = MakeUserstampFromRef(v.Value)
|
||||
delete(r.Values, k)
|
||||
|
||||
case "updatedby":
|
||||
us.UpdatedBy = MakeUserstampFromRef(v.Value)
|
||||
delete(r.Values, k)
|
||||
|
||||
case "deletedby":
|
||||
us.DeletedBy = MakeUserstampFromRef(v.Value)
|
||||
delete(r.Values, k)
|
||||
|
||||
case "ownedby",
|
||||
"ownerid",
|
||||
"owner":
|
||||
us.OwnedBy = MakeUserstampFromRef(v.Value)
|
||||
delete(r.Values, k)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
r.Us = us
|
||||
}
|
||||
|
||||
func (crt *composeRecordShaper) cloneValues(r *ComposeRecordRaw) map[string]ComposeRecordRawValue {
|
||||
rr := make(map[string]ComposeRecordRawValue)
|
||||
for k, v := range r.Values {
|
||||
rr[k] = ComposeRecordRawValue{
|
||||
Name: v.Name,
|
||||
Value: v.Value,
|
||||
}
|
||||
}
|
||||
return rr
|
||||
}
|
||||
|
||||
func (crt *composeRecordShaper) getKey(vv map[string]string, kk []string) (rtr string) {
|
||||
if len(kk) <= 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
for _, k := range kk {
|
||||
rtr += k + "."
|
||||
}
|
||||
// Remove the trailing dot
|
||||
return vv[rtr[0:len(rtr)-1]]
|
||||
}
|
||||
77
server/pkg/envoy/resource/compose_record_template.go
Normal file
77
server/pkg/envoy/resource/compose_record_template.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
)
|
||||
|
||||
type (
|
||||
MappingTpl struct {
|
||||
// Index is like Cell, but the index
|
||||
Index uint
|
||||
Cell string
|
||||
Field string
|
||||
|
||||
// If specifies when the field should be mapped
|
||||
// @todo
|
||||
If string
|
||||
|
||||
// Expr specifies how to aditionally manipulate the value
|
||||
// @todo
|
||||
Expr string
|
||||
}
|
||||
MappingTplSet []*MappingTpl
|
||||
|
||||
ComposeRecordTemplate struct {
|
||||
// We'll do this so it conforms to the resource.Interface
|
||||
*base
|
||||
|
||||
ModRef *Ref
|
||||
NsRef *Ref
|
||||
|
||||
// Name is the source name; topically file name
|
||||
Name string
|
||||
// Key determines what defines an identifier
|
||||
Key []string
|
||||
FieldMap MappingTplSet
|
||||
Defaultable bool
|
||||
}
|
||||
)
|
||||
|
||||
// NewComposeRecordTemplate returns a record template based on the provided parameters.
|
||||
//
|
||||
// The template is only valid up until the resource shaping; it is not allowed after the fact.
|
||||
func NewComposeRecordTemplate(modRef, nsRef, name string, defaultable bool, fieldMap MappingTplSet, key ...string) *ComposeRecordTemplate {
|
||||
r := &ComposeRecordTemplate{base: &base{}}
|
||||
r.Name = name
|
||||
r.Key = key
|
||||
r.FieldMap = fieldMap
|
||||
r.Defaultable = defaultable
|
||||
|
||||
r.NsRef = r.AddRef(types.NamespaceResourceType, nsRef)
|
||||
r.ModRef = r.AddRef(types.ModuleResourceType, modRef).Constraint(r.NsRef)
|
||||
|
||||
r.SetResourceType(DataSourceResourceType)
|
||||
r.AddIdentifier(identifiers(name)...)
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *ComposeRecordTemplate) Resource() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MapToMappingTplSet converts the given string map to a propper MappingTplSet
|
||||
func MapToMappingTplSet(b map[string]string) MappingTplSet {
|
||||
mp := make(MappingTplSet, 0, len(b))
|
||||
for c, f := range b {
|
||||
mp = append(mp, &MappingTpl{
|
||||
Cell: c,
|
||||
Field: f,
|
||||
})
|
||||
}
|
||||
return mp
|
||||
}
|
||||
|
||||
func (t *MappingTpl) IsIgnored() bool {
|
||||
// @todo expand this?
|
||||
return t.Field == "/"
|
||||
}
|
||||
30
server/pkg/envoy/resource/dataset.go
Normal file
30
server/pkg/envoy/resource/dataset.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package resource
|
||||
|
||||
type (
|
||||
provider interface {
|
||||
Fields() []string
|
||||
Count() uint64
|
||||
Reset() error
|
||||
Next() (map[string]string, error)
|
||||
}
|
||||
|
||||
ResourceDataset struct {
|
||||
*base
|
||||
|
||||
P provider
|
||||
}
|
||||
)
|
||||
|
||||
func NewResourceDataset(name string, p provider) *ResourceDataset {
|
||||
r := &ResourceDataset{base: &base{}}
|
||||
r.P = p
|
||||
|
||||
r.SetResourceType(DataSourceResourceType)
|
||||
r.AddIdentifier(identifiers(name)...)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *ResourceDataset) Resource() interface{} {
|
||||
return nil
|
||||
}
|
||||
24
server/pkg/envoy/resource/rbac_references_automation.gen.go
generated
Normal file
24
server/pkg/envoy/resource/rbac_references_automation.gen.go
generated
Normal file
@@ -0,0 +1,24 @@
|
||||
package resource
|
||||
|
||||
// This file is auto-generated.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
//
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/automation/types"
|
||||
)
|
||||
|
||||
// AutomationWorkflowRbacReferences generates RBAC references
|
||||
//
|
||||
// Resources with "envoy: false" are skipped
|
||||
//
|
||||
// This function is auto-generated
|
||||
func AutomationWorkflowRbacReferences(workflow string) (res *Ref, pp []*Ref, err error) {
|
||||
if workflow != "*" {
|
||||
res = &Ref{ResourceType: types.WorkflowResourceType, Identifiers: MakeIdentifiers(workflow)}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
129
server/pkg/envoy/resource/rbac_references_compose.gen.go
generated
Normal file
129
server/pkg/envoy/resource/rbac_references_compose.gen.go
generated
Normal file
@@ -0,0 +1,129 @@
|
||||
package resource
|
||||
|
||||
// This file is auto-generated.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
//
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
)
|
||||
|
||||
// ComposeChartRbacReferences generates RBAC references
|
||||
//
|
||||
// Resources with "envoy: false" are skipped
|
||||
//
|
||||
// This function is auto-generated
|
||||
func ComposeChartRbacReferences(namespaceID string, chart string) (res *Ref, pp []*Ref, err error) {
|
||||
if namespaceID != "*" {
|
||||
pp = append(pp, &Ref{ResourceType: types.NamespaceResourceType, Identifiers: MakeIdentifiers(namespaceID)})
|
||||
}
|
||||
if chart != "*" {
|
||||
res = &Ref{ResourceType: types.ChartResourceType, Identifiers: MakeIdentifiers(chart)}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ComposeModuleRbacReferences generates RBAC references
|
||||
//
|
||||
// Resources with "envoy: false" are skipped
|
||||
//
|
||||
// This function is auto-generated
|
||||
func ComposeModuleRbacReferences(namespaceID string, module string) (res *Ref, pp []*Ref, err error) {
|
||||
if namespaceID != "*" {
|
||||
pp = append(pp, &Ref{ResourceType: types.NamespaceResourceType, Identifiers: MakeIdentifiers(namespaceID)})
|
||||
}
|
||||
if module != "*" {
|
||||
res = &Ref{ResourceType: types.ModuleResourceType, Identifiers: MakeIdentifiers(module)}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ComposeModuleFieldRbacReferences generates RBAC references
|
||||
//
|
||||
// Resources with "envoy: false" are skipped
|
||||
//
|
||||
// This function is auto-generated
|
||||
func ComposeModuleFieldRbacReferences(namespaceID string, moduleID string, moduleField string) (res *Ref, pp []*Ref, err error) {
|
||||
if namespaceID != "*" {
|
||||
pp = append(pp, &Ref{ResourceType: types.NamespaceResourceType, Identifiers: MakeIdentifiers(namespaceID)})
|
||||
}
|
||||
if moduleID != "*" {
|
||||
pp = append(pp, &Ref{ResourceType: types.ModuleResourceType, Identifiers: MakeIdentifiers(moduleID)})
|
||||
}
|
||||
if moduleField != "*" {
|
||||
res = &Ref{ResourceType: types.ModuleFieldResourceType, Identifiers: MakeIdentifiers(moduleField)}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ComposeNamespaceRbacReferences generates RBAC references
|
||||
//
|
||||
// Resources with "envoy: false" are skipped
|
||||
//
|
||||
// This function is auto-generated
|
||||
func ComposeNamespaceRbacReferences(namespace string) (res *Ref, pp []*Ref, err error) {
|
||||
if namespace != "*" {
|
||||
res = &Ref{ResourceType: types.NamespaceResourceType, Identifiers: MakeIdentifiers(namespace)}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ComposePageRbacReferences generates RBAC references
|
||||
//
|
||||
// Resources with "envoy: false" are skipped
|
||||
//
|
||||
// This function is auto-generated
|
||||
func ComposePageRbacReferences(namespaceID string, page string) (res *Ref, pp []*Ref, err error) {
|
||||
if namespaceID != "*" {
|
||||
pp = append(pp, &Ref{ResourceType: types.NamespaceResourceType, Identifiers: MakeIdentifiers(namespaceID)})
|
||||
}
|
||||
if page != "*" {
|
||||
res = &Ref{ResourceType: types.PageResourceType, Identifiers: MakeIdentifiers(page)}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ComposePageLayoutRbacReferences generates RBAC references
|
||||
//
|
||||
// Resources with "envoy: false" are skipped
|
||||
//
|
||||
// This function is auto-generated
|
||||
func ComposePageLayoutRbacReferences(namespaceID string, pageID string, pageLayout string) (res *Ref, pp []*Ref, err error) {
|
||||
if namespaceID != "*" {
|
||||
pp = append(pp, &Ref{ResourceType: types.NamespaceResourceType, Identifiers: MakeIdentifiers(namespaceID)})
|
||||
}
|
||||
if pageID != "*" {
|
||||
pp = append(pp, &Ref{ResourceType: types.PageResourceType, Identifiers: MakeIdentifiers(pageID)})
|
||||
}
|
||||
if pageLayout != "*" {
|
||||
res = &Ref{ResourceType: types.PageLayoutResourceType, Identifiers: MakeIdentifiers(pageLayout)}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ComposeRecordRbacReferences generates RBAC references
|
||||
//
|
||||
// Resources with "envoy: false" are skipped
|
||||
//
|
||||
// This function is auto-generated
|
||||
func ComposeRecordRbacReferences(namespaceID string, moduleID string, record string) (res *Ref, pp []*Ref, err error) {
|
||||
if namespaceID != "*" {
|
||||
pp = append(pp, &Ref{ResourceType: types.NamespaceResourceType, Identifiers: MakeIdentifiers(namespaceID)})
|
||||
}
|
||||
if moduleID != "*" {
|
||||
pp = append(pp, &Ref{ResourceType: types.ModuleResourceType, Identifiers: MakeIdentifiers(moduleID)})
|
||||
}
|
||||
if record != "*" {
|
||||
res = &Ref{ResourceType: types.RecordResourceType, Identifiers: MakeIdentifiers(record)}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
56
server/pkg/envoy/resource/rbac_references_federation.gen.go
generated
Normal file
56
server/pkg/envoy/resource/rbac_references_federation.gen.go
generated
Normal file
@@ -0,0 +1,56 @@
|
||||
package resource
|
||||
|
||||
// This file is auto-generated.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
//
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/federation/types"
|
||||
)
|
||||
|
||||
// FederationNodeRbacReferences generates RBAC references
|
||||
//
|
||||
// Resources with "envoy: false" are skipped
|
||||
//
|
||||
// This function is auto-generated
|
||||
func FederationNodeRbacReferences(node string) (res *Ref, pp []*Ref, err error) {
|
||||
if node != "*" {
|
||||
res = &Ref{ResourceType: types.NodeResourceType, Identifiers: MakeIdentifiers(node)}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// FederationExposedModuleRbacReferences generates RBAC references
|
||||
//
|
||||
// Resources with "envoy: false" are skipped
|
||||
//
|
||||
// This function is auto-generated
|
||||
func FederationExposedModuleRbacReferences(nodeID string, exposedModule string) (res *Ref, pp []*Ref, err error) {
|
||||
if nodeID != "*" {
|
||||
pp = append(pp, &Ref{ResourceType: types.NodeResourceType, Identifiers: MakeIdentifiers(nodeID)})
|
||||
}
|
||||
if exposedModule != "*" {
|
||||
res = &Ref{ResourceType: types.ExposedModuleResourceType, Identifiers: MakeIdentifiers(exposedModule)}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// FederationSharedModuleRbacReferences generates RBAC references
|
||||
//
|
||||
// Resources with "envoy: false" are skipped
|
||||
//
|
||||
// This function is auto-generated
|
||||
func FederationSharedModuleRbacReferences(nodeID string, sharedModule string) (res *Ref, pp []*Ref, err error) {
|
||||
if nodeID != "*" {
|
||||
pp = append(pp, &Ref{ResourceType: types.NodeResourceType, Identifiers: MakeIdentifiers(nodeID)})
|
||||
}
|
||||
if sharedModule != "*" {
|
||||
res = &Ref{ResourceType: types.SharedModuleResourceType, Identifiers: MakeIdentifiers(sharedModule)}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
141
server/pkg/envoy/resource/rbac_references_system.gen.go
generated
Normal file
141
server/pkg/envoy/resource/rbac_references_system.gen.go
generated
Normal file
@@ -0,0 +1,141 @@
|
||||
package resource
|
||||
|
||||
// This file is auto-generated.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
//
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
// SystemApplicationRbacReferences generates RBAC references
|
||||
//
|
||||
// Resources with "envoy: false" are skipped
|
||||
//
|
||||
// This function is auto-generated
|
||||
func SystemApplicationRbacReferences(application string) (res *Ref, pp []*Ref, err error) {
|
||||
if application != "*" {
|
||||
res = &Ref{ResourceType: types.ApplicationResourceType, Identifiers: MakeIdentifiers(application)}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SystemApigwRouteRbacReferences generates RBAC references
|
||||
//
|
||||
// Resources with "envoy: false" are skipped
|
||||
//
|
||||
// This function is auto-generated
|
||||
func SystemApigwRouteRbacReferences(apigwRoute string) (res *Ref, pp []*Ref, err error) {
|
||||
if apigwRoute != "*" {
|
||||
res = &Ref{ResourceType: types.ApigwRouteResourceType, Identifiers: MakeIdentifiers(apigwRoute)}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SystemAuthClientRbacReferences generates RBAC references
|
||||
//
|
||||
// Resources with "envoy: false" are skipped
|
||||
//
|
||||
// This function is auto-generated
|
||||
func SystemAuthClientRbacReferences(authClient string) (res *Ref, pp []*Ref, err error) {
|
||||
if authClient != "*" {
|
||||
res = &Ref{ResourceType: types.AuthClientResourceType, Identifiers: MakeIdentifiers(authClient)}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SystemDataPrivacyRequestRbacReferences generates RBAC references
|
||||
//
|
||||
// Resources with "envoy: false" are skipped
|
||||
//
|
||||
// This function is auto-generated
|
||||
func SystemDataPrivacyRequestRbacReferences(dataPrivacyRequest string) (res *Ref, pp []*Ref, err error) {
|
||||
if dataPrivacyRequest != "*" {
|
||||
res = &Ref{ResourceType: types.DataPrivacyRequestResourceType, Identifiers: MakeIdentifiers(dataPrivacyRequest)}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SystemQueueRbacReferences generates RBAC references
|
||||
//
|
||||
// Resources with "envoy: false" are skipped
|
||||
//
|
||||
// This function is auto-generated
|
||||
func SystemQueueRbacReferences(queue string) (res *Ref, pp []*Ref, err error) {
|
||||
if queue != "*" {
|
||||
res = &Ref{ResourceType: types.QueueResourceType, Identifiers: MakeIdentifiers(queue)}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SystemReportRbacReferences generates RBAC references
|
||||
//
|
||||
// Resources with "envoy: false" are skipped
|
||||
//
|
||||
// This function is auto-generated
|
||||
func SystemReportRbacReferences(report string) (res *Ref, pp []*Ref, err error) {
|
||||
if report != "*" {
|
||||
res = &Ref{ResourceType: types.ReportResourceType, Identifiers: MakeIdentifiers(report)}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SystemRoleRbacReferences generates RBAC references
|
||||
//
|
||||
// Resources with "envoy: false" are skipped
|
||||
//
|
||||
// This function is auto-generated
|
||||
func SystemRoleRbacReferences(role string) (res *Ref, pp []*Ref, err error) {
|
||||
if role != "*" {
|
||||
res = &Ref{ResourceType: types.RoleResourceType, Identifiers: MakeIdentifiers(role)}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SystemTemplateRbacReferences generates RBAC references
|
||||
//
|
||||
// Resources with "envoy: false" are skipped
|
||||
//
|
||||
// This function is auto-generated
|
||||
func SystemTemplateRbacReferences(template string) (res *Ref, pp []*Ref, err error) {
|
||||
if template != "*" {
|
||||
res = &Ref{ResourceType: types.TemplateResourceType, Identifiers: MakeIdentifiers(template)}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SystemUserRbacReferences generates RBAC references
|
||||
//
|
||||
// Resources with "envoy: false" are skipped
|
||||
//
|
||||
// This function is auto-generated
|
||||
func SystemUserRbacReferences(user string) (res *Ref, pp []*Ref, err error) {
|
||||
if user != "*" {
|
||||
res = &Ref{ResourceType: types.UserResourceType, Identifiers: MakeIdentifiers(user)}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SystemDalConnectionRbacReferences generates RBAC references
|
||||
//
|
||||
// Resources with "envoy: false" are skipped
|
||||
//
|
||||
// This function is auto-generated
|
||||
func SystemDalConnectionRbacReferences(dalConnection string) (res *Ref, pp []*Ref, err error) {
|
||||
if dalConnection != "*" {
|
||||
res = &Ref{ResourceType: types.DalConnectionResourceType, Identifiers: MakeIdentifiers(dalConnection)}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
122
server/pkg/envoy/resource/rbac_rule.go
Normal file
122
server/pkg/envoy/resource/rbac_rule.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
composeTypes "github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/rbac"
|
||||
)
|
||||
|
||||
type (
|
||||
RbacRule struct {
|
||||
*base
|
||||
Res *rbac.Rule
|
||||
|
||||
RefResource string
|
||||
RefRes *Ref
|
||||
RefRole *Ref
|
||||
RefPath []*Ref
|
||||
}
|
||||
)
|
||||
|
||||
func NewRbacRule(res *rbac.Rule, refRole, refRes *Ref, refResource string, refPath ...*Ref) *RbacRule {
|
||||
r := &RbacRule{base: &base{}}
|
||||
r.SetResourceType(RbacResourceType)
|
||||
r.Res = res
|
||||
|
||||
r.RefRole = r.addRef(refRole)
|
||||
|
||||
r.RefResource = refResource
|
||||
if refRes != nil {
|
||||
r.RefRes = r.addRef(refRes)
|
||||
}
|
||||
|
||||
// any additional constraints
|
||||
for i, rp := range refPath {
|
||||
ref := MakeRef(rp.ResourceType, rp.Identifiers)
|
||||
|
||||
if r.RefRes != nil {
|
||||
r.RefRes.Constraint(ref)
|
||||
}
|
||||
|
||||
if i > 0 {
|
||||
for j := i - 1; j < i; j++ {
|
||||
aux := refPath[j]
|
||||
ref = ref.Constraint(MakeRef(aux.ResourceType, aux.Identifiers))
|
||||
}
|
||||
}
|
||||
|
||||
r.RefPath = append(r.RefPath, r.addRef(ref))
|
||||
}
|
||||
|
||||
// Handle cases for nested resources
|
||||
if refRes != nil {
|
||||
switch refRes.ResourceType {
|
||||
case composeTypes.RecordResourceType:
|
||||
r.handleComposeRecord(res, refPath)
|
||||
case composeTypes.ModuleFieldResourceType:
|
||||
r.handleComposeModuleField(res, refPath)
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *RbacRule) WrapError(act string, err error) error {
|
||||
return fmt.Errorf("%s %s %v: %s",
|
||||
act,
|
||||
r.ResourceType(),
|
||||
fmt.Sprintf("{ role: %s, resource: %s, operation: %s, access: %s }", r.RefRole.Identifiers.First(), r.RefResource, r.Res.Operation, r.Res.Access),
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
func (r *RbacRule) Resource() interface{} {
|
||||
return r.Res
|
||||
}
|
||||
|
||||
func (r *RbacRule) ReRef(old RefSet, new RefSet) {
|
||||
r.base.ReRef(old, new)
|
||||
|
||||
// Care for wildcards
|
||||
if r.RefRes != nil {
|
||||
for i, o := range old {
|
||||
if o.equals(r.RefRes) {
|
||||
r.RefRes = new[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i, o := range old {
|
||||
if RefSet(r.RefPath).findRef(o) > -1 {
|
||||
r.RefPath = RefSet(r.RefPath).replaceRef(o, new[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RbacRule) handleComposeRecord(res *rbac.Rule, refPath []*Ref) {
|
||||
|
||||
// records are grouped under module
|
||||
|
||||
if len(refPath) < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
r.AddRef(composeTypes.RecordResourceType, refPath[1].Identifiers.StringSlice()...)
|
||||
}
|
||||
|
||||
func (r *RbacRule) handleComposeModuleField(res *rbac.Rule, refPath []*Ref) {
|
||||
|
||||
// module fields are grouped under module
|
||||
|
||||
if len(refPath) < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
r.AddRef(composeTypes.ModuleFieldResourceTranslationType, refPath[1].Identifiers.StringSlice()...)
|
||||
}
|
||||
|
||||
func RbacResourceErrNotFound(ii Identifiers) error {
|
||||
return fmt.Errorf("rbac resource not found %v", ii.StringSlice())
|
||||
}
|
||||
282
server/pkg/envoy/resource/rbac_rules_parse.gen.go
generated
Normal file
282
server/pkg/envoy/resource/rbac_rules_parse.gen.go
generated
Normal file
@@ -0,0 +1,282 @@
|
||||
package resource
|
||||
|
||||
// This file is auto-generated.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
//
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
automationTypes "github.com/cortezaproject/corteza/server/automation/types"
|
||||
composeTypes "github.com/cortezaproject/corteza/server/compose/types"
|
||||
federationTypes "github.com/cortezaproject/corteza/server/federation/types"
|
||||
systemTypes "github.com/cortezaproject/corteza/server/system/types"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Parse generates resource setting logic for each resource
|
||||
//
|
||||
// Resources with "envoy: false" are skipped
|
||||
//
|
||||
// This function is auto-generated
|
||||
func ParseRule(res string) (string, *Ref, []*Ref, error) {
|
||||
if res == "" {
|
||||
return "", nil, nil, fmt.Errorf("empty resource")
|
||||
}
|
||||
|
||||
sp := "/"
|
||||
|
||||
res = strings.TrimSpace(res)
|
||||
res = strings.TrimRight(res, sp)
|
||||
rr := strings.Split(res, sp)
|
||||
|
||||
// only service defined (corteza::system, corteza::compose, ...)
|
||||
if len(rr) == 1 {
|
||||
return res, nil, nil, nil
|
||||
}
|
||||
|
||||
// full thing
|
||||
resourceType, path := rr[0], rr[1:]
|
||||
|
||||
for p := 1; p < len(path); p++ {
|
||||
if path[p] != "*" && path[p-1] == "*" {
|
||||
return "", nil, nil, fmt.Errorf("invalid path wildcard combination for '%s'", res)
|
||||
}
|
||||
}
|
||||
|
||||
// make the resource provide the slice of parent resources we should nest under
|
||||
switch resourceType {
|
||||
case systemTypes.ApplicationResourceType:
|
||||
if len(path) != 1 {
|
||||
return "", nil, nil, fmt.Errorf("expecting 1 reference components in path, got %d", len(path))
|
||||
}
|
||||
ref, pp, err := SystemApplicationRbacReferences(
|
||||
path[0],
|
||||
)
|
||||
return resourceType, ref, pp, err
|
||||
|
||||
case systemTypes.ApigwRouteResourceType:
|
||||
if len(path) != 1 {
|
||||
return "", nil, nil, fmt.Errorf("expecting 1 reference components in path, got %d", len(path))
|
||||
}
|
||||
ref, pp, err := SystemApigwRouteRbacReferences(
|
||||
path[0],
|
||||
)
|
||||
return resourceType, ref, pp, err
|
||||
|
||||
case systemTypes.AuthClientResourceType:
|
||||
if len(path) != 1 {
|
||||
return "", nil, nil, fmt.Errorf("expecting 1 reference components in path, got %d", len(path))
|
||||
}
|
||||
ref, pp, err := SystemAuthClientRbacReferences(
|
||||
path[0],
|
||||
)
|
||||
return resourceType, ref, pp, err
|
||||
|
||||
case systemTypes.DataPrivacyRequestResourceType:
|
||||
if len(path) != 1 {
|
||||
return "", nil, nil, fmt.Errorf("expecting 1 reference components in path, got %d", len(path))
|
||||
}
|
||||
ref, pp, err := SystemDataPrivacyRequestRbacReferences(
|
||||
path[0],
|
||||
)
|
||||
return resourceType, ref, pp, err
|
||||
|
||||
case systemTypes.QueueResourceType:
|
||||
if len(path) != 1 {
|
||||
return "", nil, nil, fmt.Errorf("expecting 1 reference components in path, got %d", len(path))
|
||||
}
|
||||
ref, pp, err := SystemQueueRbacReferences(
|
||||
path[0],
|
||||
)
|
||||
return resourceType, ref, pp, err
|
||||
|
||||
case systemTypes.ReportResourceType:
|
||||
if len(path) != 1 {
|
||||
return "", nil, nil, fmt.Errorf("expecting 1 reference components in path, got %d", len(path))
|
||||
}
|
||||
ref, pp, err := SystemReportRbacReferences(
|
||||
path[0],
|
||||
)
|
||||
return resourceType, ref, pp, err
|
||||
|
||||
case systemTypes.RoleResourceType:
|
||||
if len(path) != 1 {
|
||||
return "", nil, nil, fmt.Errorf("expecting 1 reference components in path, got %d", len(path))
|
||||
}
|
||||
ref, pp, err := SystemRoleRbacReferences(
|
||||
path[0],
|
||||
)
|
||||
return resourceType, ref, pp, err
|
||||
|
||||
case systemTypes.TemplateResourceType:
|
||||
if len(path) != 1 {
|
||||
return "", nil, nil, fmt.Errorf("expecting 1 reference components in path, got %d", len(path))
|
||||
}
|
||||
ref, pp, err := SystemTemplateRbacReferences(
|
||||
path[0],
|
||||
)
|
||||
return resourceType, ref, pp, err
|
||||
|
||||
case systemTypes.UserResourceType:
|
||||
if len(path) != 1 {
|
||||
return "", nil, nil, fmt.Errorf("expecting 1 reference components in path, got %d", len(path))
|
||||
}
|
||||
ref, pp, err := SystemUserRbacReferences(
|
||||
path[0],
|
||||
)
|
||||
return resourceType, ref, pp, err
|
||||
|
||||
case systemTypes.DalConnectionResourceType:
|
||||
if len(path) != 1 {
|
||||
return "", nil, nil, fmt.Errorf("expecting 1 reference components in path, got %d", len(path))
|
||||
}
|
||||
ref, pp, err := SystemDalConnectionRbacReferences(
|
||||
path[0],
|
||||
)
|
||||
return resourceType, ref, pp, err
|
||||
|
||||
case composeTypes.ChartResourceType:
|
||||
if len(path) != 2 {
|
||||
return "", nil, nil, fmt.Errorf("expecting 2 reference components in path, got %d", len(path))
|
||||
}
|
||||
ref, pp, err := ComposeChartRbacReferences(
|
||||
path[0],
|
||||
path[1],
|
||||
)
|
||||
return resourceType, ref, pp, err
|
||||
|
||||
case composeTypes.ModuleResourceType:
|
||||
if len(path) != 2 {
|
||||
return "", nil, nil, fmt.Errorf("expecting 2 reference components in path, got %d", len(path))
|
||||
}
|
||||
ref, pp, err := ComposeModuleRbacReferences(
|
||||
path[0],
|
||||
path[1],
|
||||
)
|
||||
return resourceType, ref, pp, err
|
||||
|
||||
case composeTypes.ModuleFieldResourceType:
|
||||
if len(path) != 3 {
|
||||
return "", nil, nil, fmt.Errorf("expecting 3 reference components in path, got %d", len(path))
|
||||
}
|
||||
ref, pp, err := ComposeModuleFieldRbacReferences(
|
||||
path[0],
|
||||
path[1],
|
||||
path[2],
|
||||
)
|
||||
return resourceType, ref, pp, err
|
||||
|
||||
case composeTypes.NamespaceResourceType:
|
||||
if len(path) != 1 {
|
||||
return "", nil, nil, fmt.Errorf("expecting 1 reference components in path, got %d", len(path))
|
||||
}
|
||||
ref, pp, err := ComposeNamespaceRbacReferences(
|
||||
path[0],
|
||||
)
|
||||
return resourceType, ref, pp, err
|
||||
|
||||
case composeTypes.PageResourceType:
|
||||
if len(path) != 2 {
|
||||
return "", nil, nil, fmt.Errorf("expecting 2 reference components in path, got %d", len(path))
|
||||
}
|
||||
ref, pp, err := ComposePageRbacReferences(
|
||||
path[0],
|
||||
path[1],
|
||||
)
|
||||
return resourceType, ref, pp, err
|
||||
|
||||
case composeTypes.PageLayoutResourceType:
|
||||
if len(path) != 3 {
|
||||
return "", nil, nil, fmt.Errorf("expecting 3 reference components in path, got %d", len(path))
|
||||
}
|
||||
ref, pp, err := ComposePageLayoutRbacReferences(
|
||||
path[0],
|
||||
path[1],
|
||||
path[2],
|
||||
)
|
||||
return resourceType, ref, pp, err
|
||||
|
||||
case composeTypes.RecordResourceType:
|
||||
if len(path) != 3 {
|
||||
return "", nil, nil, fmt.Errorf("expecting 3 reference components in path, got %d", len(path))
|
||||
}
|
||||
ref, pp, err := ComposeRecordRbacReferences(
|
||||
path[0],
|
||||
path[1],
|
||||
path[2],
|
||||
)
|
||||
return resourceType, ref, pp, err
|
||||
|
||||
case automationTypes.WorkflowResourceType:
|
||||
if len(path) != 1 {
|
||||
return "", nil, nil, fmt.Errorf("expecting 1 reference components in path, got %d", len(path))
|
||||
}
|
||||
ref, pp, err := AutomationWorkflowRbacReferences(
|
||||
path[0],
|
||||
)
|
||||
return resourceType, ref, pp, err
|
||||
|
||||
case federationTypes.NodeResourceType:
|
||||
if len(path) != 1 {
|
||||
return "", nil, nil, fmt.Errorf("expecting 1 reference components in path, got %d", len(path))
|
||||
}
|
||||
ref, pp, err := FederationNodeRbacReferences(
|
||||
path[0],
|
||||
)
|
||||
return resourceType, ref, pp, err
|
||||
|
||||
case federationTypes.ExposedModuleResourceType:
|
||||
if len(path) != 2 {
|
||||
return "", nil, nil, fmt.Errorf("expecting 2 reference components in path, got %d", len(path))
|
||||
}
|
||||
ref, pp, err := FederationExposedModuleRbacReferences(
|
||||
path[0],
|
||||
path[1],
|
||||
)
|
||||
return resourceType, ref, pp, err
|
||||
|
||||
case federationTypes.SharedModuleResourceType:
|
||||
if len(path) != 2 {
|
||||
return "", nil, nil, fmt.Errorf("expecting 2 reference components in path, got %d", len(path))
|
||||
}
|
||||
ref, pp, err := FederationSharedModuleRbacReferences(
|
||||
path[0],
|
||||
path[1],
|
||||
)
|
||||
return resourceType, ref, pp, err
|
||||
|
||||
case systemTypes.ComponentResourceType:
|
||||
if len(path) != 0 {
|
||||
return "", nil, nil, fmt.Errorf("expecting 0 reference components in path, got %d", len(path))
|
||||
}
|
||||
|
||||
// Component resource, no path
|
||||
return resourceType, nil, nil, nil
|
||||
case composeTypes.ComponentResourceType:
|
||||
if len(path) != 0 {
|
||||
return "", nil, nil, fmt.Errorf("expecting 0 reference components in path, got %d", len(path))
|
||||
}
|
||||
|
||||
// Component resource, no path
|
||||
return resourceType, nil, nil, nil
|
||||
case automationTypes.ComponentResourceType:
|
||||
if len(path) != 0 {
|
||||
return "", nil, nil, fmt.Errorf("expecting 0 reference components in path, got %d", len(path))
|
||||
}
|
||||
|
||||
// Component resource, no path
|
||||
return resourceType, nil, nil, nil
|
||||
case federationTypes.ComponentResourceType:
|
||||
if len(path) != 0 {
|
||||
return "", nil, nil, fmt.Errorf("expecting 0 reference components in path, got %d", len(path))
|
||||
}
|
||||
|
||||
// Component resource, no path
|
||||
return resourceType, nil, nil, nil
|
||||
}
|
||||
|
||||
// return unhandled resource as-is
|
||||
return resourceType, nil, nil, nil
|
||||
}
|
||||
121
server/pkg/envoy/resource/report.go
Normal file
121
server/pkg/envoy/resource/report.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
Report struct {
|
||||
*base
|
||||
Res *types.Report
|
||||
|
||||
Sources []*ReportSource
|
||||
Blocks []*ReportBlock
|
||||
}
|
||||
|
||||
ReportSource struct {
|
||||
*base
|
||||
Res *types.ReportDataSource
|
||||
}
|
||||
|
||||
ReportBlock struct {
|
||||
*base
|
||||
Res *types.ReportBlock
|
||||
}
|
||||
)
|
||||
|
||||
func NewReport(res *types.Report) *Report {
|
||||
r := &Report{
|
||||
base: &base{},
|
||||
}
|
||||
r.SetResourceType(types.ReportResourceType)
|
||||
r.Res = res
|
||||
|
||||
r.AddIdentifier(identifiers(res.Handle, res.ID)...)
|
||||
|
||||
// Initial stamps
|
||||
r.SetTimestamps(MakeTimestampsCUDA(&res.CreatedAt, res.UpdatedAt, res.DeletedAt, nil))
|
||||
r.SetUserstamps(MakeUserstampsCUDO(res.CreatedBy, res.UpdatedBy, res.DeletedBy, res.OwnedBy))
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Report) Resource() interface{} {
|
||||
return r.Res
|
||||
}
|
||||
|
||||
func (r *Report) AddReportSource(res *types.ReportDataSource) *ReportSource {
|
||||
s := &ReportSource{
|
||||
base: &base{},
|
||||
}
|
||||
|
||||
s.Res = res
|
||||
r.Sources = append(r.Sources, s)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (r *Report) RBACParts() (resource string, ref *Ref, path []*Ref) {
|
||||
ref = r.Ref()
|
||||
path = nil
|
||||
resource = fmt.Sprintf(types.ReportRbacResourceTpl(), types.ReportResourceType, firstOkString(strconv.FormatUint(r.Res.ID, 10), r.Res.Handle))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// func (r *Report) ResourceTranslationParts() (resource string, ref *Ref, path []*Ref) {
|
||||
// ref = r.Ref()
|
||||
// path = nil
|
||||
// resource = fmt.Sprintf(types.ReportResourceTranslationTpl(), types.ReportResourceTranslationType, firstOkString(strconv.FormatUint(r.Res.ID, 10), r.Res.Handle))
|
||||
|
||||
// return
|
||||
// }
|
||||
|
||||
func (r *Report) AddReportBlock(res *types.ReportBlock) *ReportBlock {
|
||||
p := &ReportBlock{
|
||||
base: &base{},
|
||||
}
|
||||
|
||||
p.Res = res
|
||||
r.Blocks = append(r.Blocks, p)
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func (r *Report) SysID() uint64 {
|
||||
return r.Res.ID
|
||||
}
|
||||
|
||||
func (r *Report) encodeTranslations() ([]*ResourceTranslation, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// FindReport looks for the workflow in the resource set
|
||||
func FindReport(rr InterfaceSet, ii Identifiers) (ns *types.Report) {
|
||||
var wfRes *Report
|
||||
|
||||
rr.Walk(func(r Interface) error {
|
||||
wr, ok := r.(*Report)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if wr.Identifiers().HasAny(ii) {
|
||||
wfRes = wr
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Found it
|
||||
if wfRes != nil {
|
||||
return wfRes.Res
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReportErrUnresolved(ii Identifiers) error {
|
||||
return fmt.Errorf("report unresolved %v", ii.StringSlice())
|
||||
}
|
||||
232
server/pkg/envoy/resource/resource.go
Normal file
232
server/pkg/envoy/resource/resource.go
Normal file
@@ -0,0 +1,232 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
composeTypes "github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
base struct {
|
||||
rt string
|
||||
ii Identifiers
|
||||
rr RefSet
|
||||
ph bool
|
||||
|
||||
ts *Timestamps
|
||||
us *Userstamps
|
||||
cfg *EnvoyConfig
|
||||
urefs RefSet
|
||||
}
|
||||
|
||||
EnvoyConfig struct {
|
||||
// SkipIf determines when the encoding should be skipped for this resource
|
||||
SkipIf string
|
||||
OnExisting MergeAlg
|
||||
}
|
||||
|
||||
MergeAlg int
|
||||
)
|
||||
|
||||
const (
|
||||
// Default takes the operation defined default
|
||||
Default MergeAlg = iota
|
||||
// Skip skips the existing resource
|
||||
Skip
|
||||
// Replace replaces the existing resource
|
||||
Replace
|
||||
// MergeLeft updates the existing resource, giving priority to the existing data
|
||||
MergeLeft
|
||||
// MergeRight updates the existing resource, giving priority to the new data
|
||||
MergeRight
|
||||
)
|
||||
|
||||
// fn converts identifier values (string, fmt.Stringer, uint64) to string slice
|
||||
//
|
||||
// Each value is checked and should not be empty or zero
|
||||
func identifiers(ii ...interface{}) []string {
|
||||
ss := make([]string, 0, len(ii))
|
||||
|
||||
for _, i := range ii {
|
||||
switch c := i.(type) {
|
||||
case uint64:
|
||||
if c == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
ss = append(ss, strconv.FormatUint(c, 10))
|
||||
|
||||
case fmt.Stringer:
|
||||
if c.String() == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
ss = append(ss, c.String())
|
||||
|
||||
case string:
|
||||
if c == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
ss = append(ss, c)
|
||||
}
|
||||
}
|
||||
|
||||
return ss
|
||||
}
|
||||
|
||||
// Check checks if the identifier c is in the set of identifiers ii
|
||||
func Check(a string, ii ...interface{}) bool {
|
||||
for _, i := range ii {
|
||||
switch pi := i.(type) {
|
||||
case string:
|
||||
if a == pi {
|
||||
return true
|
||||
}
|
||||
case uint64:
|
||||
if pi > 0 && strconv.FormatUint(pi, 10) == i {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// AddIdentifier adds a set of identifiers to the current resource
|
||||
func (t *base) AddIdentifier(ss ...string) {
|
||||
t.ii = t.ii.Add(ss...)
|
||||
}
|
||||
|
||||
// SetIdentifier sets the identifiers to whatever was provided
|
||||
func (t *base) SetIdentifier(ii Identifiers) {
|
||||
t.ii = ii
|
||||
}
|
||||
|
||||
// AddRef adds a new reference to the current resource
|
||||
func (t *base) AddRef(rt string, ii ...string) *Ref {
|
||||
iiC := make([]string, 0, len(ii))
|
||||
for _, i := range ii {
|
||||
if i != "" {
|
||||
iiC = append(iiC, i)
|
||||
}
|
||||
}
|
||||
|
||||
return t.addRef(&Ref{ResourceType: rt, Identifiers: Identifiers{}.Add(iiC...)})
|
||||
}
|
||||
|
||||
func (t *base) addRef(r *Ref) *Ref {
|
||||
t.rr = append(t.rr, r)
|
||||
return r
|
||||
}
|
||||
|
||||
// ReplaceRef replaces the given ref with the new one.
|
||||
// The new ref is added regardles if the old one exists or not.
|
||||
func (t *base) ReplaceRef(old, new *Ref) {
|
||||
t.rr = t.rr.replaceRef(old, new)
|
||||
}
|
||||
|
||||
// SetResourceType sets the resource type of the current resource struct
|
||||
func (t *base) SetResourceType(rt string) {
|
||||
t.rt = rt
|
||||
}
|
||||
|
||||
func (t *base) SetTimestamps(ts *Timestamps) {
|
||||
t.ts = ts
|
||||
}
|
||||
|
||||
func (t *base) Timestamps() *Timestamps {
|
||||
return t.ts
|
||||
}
|
||||
|
||||
func (t *base) SetUserstamps(us *Userstamps) {
|
||||
t.us = us
|
||||
|
||||
if us != nil {
|
||||
uu := []*Userstamp{us.CreatedBy, us.UpdatedBy, us.DeletedBy, us.OwnedBy, us.RunAs}
|
||||
t.SetUserRefs(uu)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *base) Userstamps() *Userstamps {
|
||||
return t.us
|
||||
}
|
||||
|
||||
func (t *base) SetConfig(cfg *EnvoyConfig) {
|
||||
t.cfg = cfg
|
||||
}
|
||||
|
||||
func (t *base) Config() *EnvoyConfig {
|
||||
return t.cfg
|
||||
}
|
||||
|
||||
func (t *base) SetUserRefs(uu []*Userstamp) {
|
||||
if t.urefs == nil {
|
||||
t.urefs = make(RefSet, 0, 4)
|
||||
}
|
||||
|
||||
for _, u := range uu {
|
||||
if u == nil {
|
||||
continue
|
||||
}
|
||||
if u.UserID > 0 {
|
||||
t.urefs = append(t.urefs, t.AddRef(types.UserResourceType, strconv.FormatUint(u.UserID, 10)))
|
||||
} else if u.Ref != "" {
|
||||
t.urefs = append(t.urefs, t.AddRef(types.UserResourceType, u.Ref))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *base) UserRefs() RefSet {
|
||||
return t.urefs
|
||||
}
|
||||
|
||||
func (t *base) Identifiers() Identifiers {
|
||||
return t.ii
|
||||
}
|
||||
|
||||
func (t *base) ResourceType() string {
|
||||
return t.rt
|
||||
}
|
||||
|
||||
func (t *base) Refs() RefSet {
|
||||
return t.rr
|
||||
}
|
||||
|
||||
func (t *base) Ref() *Ref {
|
||||
return &Ref{ResourceType: t.rt, Identifiers: t.ii}
|
||||
}
|
||||
|
||||
func (t *base) HasRefs() bool {
|
||||
return t.rr == nil || len(t.rr) == 0
|
||||
}
|
||||
|
||||
// MarkPlaceholder denotes that the given resource should be treated as a placeholder
|
||||
//
|
||||
// Placeholder resources should not be encoded but should only provide additional
|
||||
// context to resources that depend on it
|
||||
func (t *base) MarkPlaceholder() {
|
||||
t.ph = true
|
||||
}
|
||||
|
||||
// Placeholder resources should not be encoded but should only provide additional
|
||||
// context to resources that depend on it
|
||||
func (t *base) Placeholder() bool {
|
||||
return t.ph
|
||||
}
|
||||
|
||||
func IgnoreDepResolution(ref *Ref) bool {
|
||||
return ref.ResourceType == composeTypes.ModuleFieldResourceType
|
||||
}
|
||||
|
||||
func (t *base) ReID(ii Identifiers) {
|
||||
t.SetIdentifier(ii)
|
||||
}
|
||||
|
||||
func (t *base) ReRef(old RefSet, new RefSet) {
|
||||
for i, o := range old {
|
||||
t.ReplaceRef(o, new[i])
|
||||
}
|
||||
}
|
||||
80
server/pkg/envoy/resource/resource_translation.gen.go
generated
Normal file
80
server/pkg/envoy/resource/resource_translation.gen.go
generated
Normal file
@@ -0,0 +1,80 @@
|
||||
package resource
|
||||
|
||||
// This file is auto-generated.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
//
|
||||
|
||||
import (
|
||||
systemTypes "github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
func (r *ComposeChart) EncodeTranslations() ([]*ResourceTranslation, error) {
|
||||
out := make([]*ResourceTranslation, 0, 10)
|
||||
|
||||
rr := r.Res.EncodeTranslations()
|
||||
rr.SetLanguage(defaultLanguage)
|
||||
res, ref, pp := r.ResourceTranslationParts()
|
||||
out = append(out, NewResourceTranslation(systemTypes.FromLocale(rr), res, ref, pp...))
|
||||
|
||||
tmp, err := r.encodeTranslations()
|
||||
return append(out, tmp...), err
|
||||
|
||||
}
|
||||
func (r *ComposeModule) EncodeTranslations() ([]*ResourceTranslation, error) {
|
||||
out := make([]*ResourceTranslation, 0, 10)
|
||||
|
||||
rr := r.Res.EncodeTranslations()
|
||||
rr.SetLanguage(defaultLanguage)
|
||||
res, ref, pp := r.ResourceTranslationParts()
|
||||
out = append(out, NewResourceTranslation(systemTypes.FromLocale(rr), res, ref, pp...))
|
||||
|
||||
tmp, err := r.encodeTranslations()
|
||||
return append(out, tmp...), err
|
||||
|
||||
}
|
||||
func (r *ComposeModuleField) EncodeTranslations() ([]*ResourceTranslation, error) {
|
||||
out := make([]*ResourceTranslation, 0, 10)
|
||||
|
||||
rr := r.Res.EncodeTranslations()
|
||||
rr.SetLanguage(defaultLanguage)
|
||||
res, ref, pp := r.ResourceTranslationParts()
|
||||
out = append(out, NewResourceTranslation(systemTypes.FromLocale(rr), res, ref, pp...))
|
||||
|
||||
return out, nil
|
||||
}
|
||||
func (r *ComposeNamespace) EncodeTranslations() ([]*ResourceTranslation, error) {
|
||||
out := make([]*ResourceTranslation, 0, 10)
|
||||
|
||||
rr := r.Res.EncodeTranslations()
|
||||
rr.SetLanguage(defaultLanguage)
|
||||
res, ref, pp := r.ResourceTranslationParts()
|
||||
out = append(out, NewResourceTranslation(systemTypes.FromLocale(rr), res, ref, pp...))
|
||||
|
||||
return out, nil
|
||||
}
|
||||
func (r *ComposePage) EncodeTranslations() ([]*ResourceTranslation, error) {
|
||||
out := make([]*ResourceTranslation, 0, 10)
|
||||
|
||||
rr := r.Res.EncodeTranslations()
|
||||
rr.SetLanguage(defaultLanguage)
|
||||
res, ref, pp := r.ResourceTranslationParts()
|
||||
out = append(out, NewResourceTranslation(systemTypes.FromLocale(rr), res, ref, pp...))
|
||||
|
||||
tmp, err := r.encodeTranslations()
|
||||
return append(out, tmp...), err
|
||||
|
||||
}
|
||||
func (r *ComposePageLayout) EncodeTranslations() ([]*ResourceTranslation, error) {
|
||||
out := make([]*ResourceTranslation, 0, 10)
|
||||
|
||||
rr := r.Res.EncodeTranslations()
|
||||
rr.SetLanguage(defaultLanguage)
|
||||
res, ref, pp := r.ResourceTranslationParts()
|
||||
out = append(out, NewResourceTranslation(systemTypes.FromLocale(rr), res, ref, pp...))
|
||||
|
||||
tmp, err := r.encodeTranslations()
|
||||
return append(out, tmp...), err
|
||||
|
||||
}
|
||||
92
server/pkg/envoy/resource/resource_translation.go
Normal file
92
server/pkg/envoy/resource/resource_translation.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
type (
|
||||
ResourceTranslation struct {
|
||||
*base
|
||||
Res types.ResourceTranslationSet
|
||||
|
||||
RefResource string
|
||||
RefRes *Ref
|
||||
RefPath []*Ref
|
||||
|
||||
Priority int
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
defaultLanguage = language.English
|
||||
)
|
||||
|
||||
func NewResourceTranslation(res types.ResourceTranslationSet, refResource string, refRes *Ref, refPath ...*Ref) *ResourceTranslation {
|
||||
r := &ResourceTranslation{base: &base{}}
|
||||
r.SetResourceType(ResourceTranslationType)
|
||||
r.Res = res
|
||||
|
||||
r.RefResource = refResource
|
||||
r.RefRes = r.addRef(refRes)
|
||||
|
||||
// any additional constraints
|
||||
for i, rp := range refPath {
|
||||
ref := MakeRef(rp.ResourceType, rp.Identifiers)
|
||||
|
||||
r.RefRes.Constraint(ref)
|
||||
if i > 0 {
|
||||
for j := i - 1; j < i; j++ {
|
||||
aux := refPath[j]
|
||||
ref = ref.Constraint(MakeRef(aux.ResourceType, aux.Identifiers))
|
||||
}
|
||||
}
|
||||
|
||||
r.RefPath = append(r.RefPath, r.addRef(ref))
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *ResourceTranslation) Resource() interface{} {
|
||||
return r.Res
|
||||
}
|
||||
|
||||
func (r *ResourceTranslation) ReRef(old RefSet, new RefSet) {
|
||||
r.base.ReRef(old, new)
|
||||
|
||||
for i, o := range old {
|
||||
if o.equals(r.RefRes) {
|
||||
r.RefRes = new[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for i, o := range old {
|
||||
if RefSet(r.RefPath).findRef(o) > -1 {
|
||||
r.RefPath = RefSet(r.RefPath).replaceRef(o, new[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *ResourceTranslation) MarkDefault() {
|
||||
l.Priority = 1
|
||||
}
|
||||
|
||||
func (l *ResourceTranslation) IsDefault() bool {
|
||||
return l.Priority == 1
|
||||
}
|
||||
|
||||
func (l *ResourceTranslation) MarkGeneric() {
|
||||
l.Priority = 0
|
||||
}
|
||||
|
||||
func (l *ResourceTranslation) IsGeneric() bool {
|
||||
return l.Priority == 0
|
||||
}
|
||||
|
||||
func ResourceTranslationErrNotFound(ii Identifiers) error {
|
||||
return fmt.Errorf("resource translation not found %v", ii.StringSlice())
|
||||
}
|
||||
115
server/pkg/envoy/resource/resource_translation_parse.gen.go
generated
Normal file
115
server/pkg/envoy/resource/resource_translation_parse.gen.go
generated
Normal file
@@ -0,0 +1,115 @@
|
||||
package resource
|
||||
|
||||
// This file is auto-generated.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
//
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
composeTypes "github.com/cortezaproject/corteza/server/compose/types"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ParseResourceTranslation generates resource setting logic for each resource
|
||||
//
|
||||
// Resources with "envoy: false" are skipped
|
||||
//
|
||||
// This function is auto-generated
|
||||
func ParseResourceTranslation(res string) (string, *Ref, []*Ref, error) {
|
||||
if res == "" {
|
||||
return "", nil, nil, fmt.Errorf("empty resource")
|
||||
}
|
||||
|
||||
sp := "/"
|
||||
|
||||
if strings.Index(res, "corteza::") == 0 {
|
||||
res = res[9:]
|
||||
}
|
||||
|
||||
res = strings.TrimSpace(res)
|
||||
res = strings.TrimRight(res, sp)
|
||||
rr := strings.Split(res, sp)
|
||||
|
||||
// only service defined (corteza::system, corteza::compose, ...)
|
||||
if len(rr) == 1 {
|
||||
return "", nil, nil, fmt.Errorf("only service defined: %s", res)
|
||||
}
|
||||
|
||||
// full thing
|
||||
resourceType, path := rr[0], rr[1:]
|
||||
for p := 1; p < len(path); p++ {
|
||||
if path[p] == "*" {
|
||||
return "", nil, nil, fmt.Errorf("path wildcard not allowed for locale resources: '%s'", res)
|
||||
}
|
||||
}
|
||||
|
||||
// make the resource provide the slice of parent resources we should nest under
|
||||
switch resourceType {
|
||||
case composeTypes.ChartResourceTranslationType:
|
||||
if len(path) != 2 {
|
||||
return "", nil, nil, fmt.Errorf("expecting 2 reference components in path, got %d", len(path))
|
||||
}
|
||||
ref, pp, err := ComposeChartResourceTranslationReferences(
|
||||
path[0],
|
||||
path[1],
|
||||
)
|
||||
return composeTypes.ChartResourceTranslationType, ref, pp, err
|
||||
|
||||
case composeTypes.ModuleResourceTranslationType:
|
||||
if len(path) != 2 {
|
||||
return "", nil, nil, fmt.Errorf("expecting 2 reference components in path, got %d", len(path))
|
||||
}
|
||||
ref, pp, err := ComposeModuleResourceTranslationReferences(
|
||||
path[0],
|
||||
path[1],
|
||||
)
|
||||
return composeTypes.ModuleResourceTranslationType, ref, pp, err
|
||||
|
||||
case composeTypes.ModuleFieldResourceTranslationType:
|
||||
if len(path) != 3 {
|
||||
return "", nil, nil, fmt.Errorf("expecting 3 reference components in path, got %d", len(path))
|
||||
}
|
||||
ref, pp, err := ComposeModuleFieldResourceTranslationReferences(
|
||||
path[0],
|
||||
path[1],
|
||||
path[2],
|
||||
)
|
||||
return composeTypes.ModuleFieldResourceTranslationType, ref, pp, err
|
||||
|
||||
case composeTypes.NamespaceResourceTranslationType:
|
||||
if len(path) != 1 {
|
||||
return "", nil, nil, fmt.Errorf("expecting 1 reference components in path, got %d", len(path))
|
||||
}
|
||||
ref, pp, err := ComposeNamespaceResourceTranslationReferences(
|
||||
path[0],
|
||||
)
|
||||
return composeTypes.NamespaceResourceTranslationType, ref, pp, err
|
||||
|
||||
case composeTypes.PageResourceTranslationType:
|
||||
if len(path) != 2 {
|
||||
return "", nil, nil, fmt.Errorf("expecting 2 reference components in path, got %d", len(path))
|
||||
}
|
||||
ref, pp, err := ComposePageResourceTranslationReferences(
|
||||
path[0],
|
||||
path[1],
|
||||
)
|
||||
return composeTypes.PageResourceTranslationType, ref, pp, err
|
||||
|
||||
case composeTypes.PageLayoutResourceTranslationType:
|
||||
if len(path) != 3 {
|
||||
return "", nil, nil, fmt.Errorf("expecting 3 reference components in path, got %d", len(path))
|
||||
}
|
||||
ref, pp, err := ComposePageLayoutResourceTranslationReferences(
|
||||
path[0],
|
||||
path[1],
|
||||
path[2],
|
||||
)
|
||||
return composeTypes.PageLayoutResourceTranslationType, ref, pp, err
|
||||
|
||||
}
|
||||
|
||||
// return unhandled resource as-is
|
||||
return resourceType, nil, nil, nil
|
||||
}
|
||||
25
server/pkg/envoy/resource/resource_translation_references_automation.gen.go
generated
Normal file
25
server/pkg/envoy/resource/resource_translation_references_automation.gen.go
generated
Normal file
@@ -0,0 +1,25 @@
|
||||
package resource
|
||||
|
||||
// This file is auto-generated.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
//
|
||||
|
||||
// Definitions file that controls how this file is generated:
|
||||
// - automation.workflow.yaml
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/automation/types"
|
||||
)
|
||||
|
||||
// AutomationWorkflowResourceTranslationReferences generates Locale references
|
||||
//
|
||||
// Resources with "envoy: false" are skipped
|
||||
//
|
||||
// This function is auto-generated
|
||||
func AutomationWorkflowResourceTranslationReferences(workflow string) (res *Ref, pp []*Ref, err error) {
|
||||
res = &Ref{ResourceType: types.WorkflowResourceType, Identifiers: MakeIdentifiers(workflow)}
|
||||
|
||||
return
|
||||
}
|
||||
72
server/pkg/envoy/resource/resource_translation_references_compose.gen.go
generated
Normal file
72
server/pkg/envoy/resource/resource_translation_references_compose.gen.go
generated
Normal file
@@ -0,0 +1,72 @@
|
||||
package resource
|
||||
|
||||
// This file is auto-generated.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
//
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
)
|
||||
|
||||
// ComposeChartResourceTranslationReferences generates Locale references
|
||||
//
|
||||
// This function is auto-generated
|
||||
func ComposeChartResourceTranslationReferences(namespaceID string, self string) (res *Ref, pp []*Ref, err error) {
|
||||
res = &Ref{ResourceType: types.ChartResourceType, Identifiers: MakeIdentifiers(self)}
|
||||
pp = append(pp, &Ref{ResourceType: types.NamespaceResourceType, Identifiers: MakeIdentifiers(namespaceID)})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ComposeModuleResourceTranslationReferences generates Locale references
|
||||
//
|
||||
// This function is auto-generated
|
||||
func ComposeModuleResourceTranslationReferences(namespaceID string, self string) (res *Ref, pp []*Ref, err error) {
|
||||
res = &Ref{ResourceType: types.ModuleResourceType, Identifiers: MakeIdentifiers(self)}
|
||||
pp = append(pp, &Ref{ResourceType: types.NamespaceResourceType, Identifiers: MakeIdentifiers(namespaceID)})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ComposeModuleFieldResourceTranslationReferences generates Locale references
|
||||
//
|
||||
// This function is auto-generated
|
||||
func ComposeModuleFieldResourceTranslationReferences(namespaceID string, moduleID string, self string) (res *Ref, pp []*Ref, err error) {
|
||||
res = &Ref{ResourceType: types.ModuleFieldResourceType, Identifiers: MakeIdentifiers(self)}
|
||||
pp = append(pp, &Ref{ResourceType: types.NamespaceResourceType, Identifiers: MakeIdentifiers(namespaceID)})
|
||||
pp = append(pp, &Ref{ResourceType: types.ModuleResourceType, Identifiers: MakeIdentifiers(moduleID)})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ComposeNamespaceResourceTranslationReferences generates Locale references
|
||||
//
|
||||
// This function is auto-generated
|
||||
func ComposeNamespaceResourceTranslationReferences(self string) (res *Ref, pp []*Ref, err error) {
|
||||
res = &Ref{ResourceType: types.NamespaceResourceType, Identifiers: MakeIdentifiers(self)}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ComposePageResourceTranslationReferences generates Locale references
|
||||
//
|
||||
// This function is auto-generated
|
||||
func ComposePageResourceTranslationReferences(namespaceID string, self string) (res *Ref, pp []*Ref, err error) {
|
||||
res = &Ref{ResourceType: types.PageResourceType, Identifiers: MakeIdentifiers(self)}
|
||||
pp = append(pp, &Ref{ResourceType: types.NamespaceResourceType, Identifiers: MakeIdentifiers(namespaceID)})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ComposePageLayoutResourceTranslationReferences generates Locale references
|
||||
//
|
||||
// This function is auto-generated
|
||||
func ComposePageLayoutResourceTranslationReferences(namespaceID string, pageID string, self string) (res *Ref, pp []*Ref, err error) {
|
||||
res = &Ref{ResourceType: types.PageLayoutResourceType, Identifiers: MakeIdentifiers(self)}
|
||||
pp = append(pp, &Ref{ResourceType: types.NamespaceResourceType, Identifiers: MakeIdentifiers(namespaceID)})
|
||||
pp = append(pp, &Ref{ResourceType: types.PageResourceType, Identifiers: MakeIdentifiers(pageID)})
|
||||
|
||||
return
|
||||
}
|
||||
73
server/pkg/envoy/resource/role.go
Normal file
73
server/pkg/envoy/resource/role.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
// Role represents a Role
|
||||
Role struct {
|
||||
*base
|
||||
Res *types.Role
|
||||
}
|
||||
)
|
||||
|
||||
func NewRole(rl *types.Role) *Role {
|
||||
r := &Role{base: &base{}}
|
||||
r.SetResourceType(types.RoleResourceType)
|
||||
r.Res = rl
|
||||
|
||||
r.AddIdentifier(identifiers(rl.Handle, rl.Name, rl.ID)...)
|
||||
|
||||
// Initial timestamps
|
||||
r.SetTimestamps(MakeTimestampsCUDA(&rl.CreatedAt, rl.UpdatedAt, rl.DeletedAt, rl.ArchivedAt))
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Role) Resource() interface{} {
|
||||
return r.Res
|
||||
}
|
||||
|
||||
func (r *Role) SysID() uint64 {
|
||||
return r.Res.ID
|
||||
}
|
||||
|
||||
func (r *Role) RBACParts() (resource string, ref *Ref, path []*Ref) {
|
||||
ref = r.Ref()
|
||||
path = nil
|
||||
resource = fmt.Sprintf(types.RoleRbacResourceTpl(), types.RoleResourceType, firstOkString(strconv.FormatUint(r.Res.ID, 10), r.Res.Handle))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// FindRole looks for the role in the resources
|
||||
func FindRole(rr InterfaceSet, ii Identifiers) (rl *types.Role) {
|
||||
var rlRes *Role
|
||||
|
||||
rr.Walk(func(r Interface) error {
|
||||
rr, ok := r.(*Role)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if rr.Identifiers().HasAny(ii) {
|
||||
rlRes = rr
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Found it
|
||||
if rlRes != nil {
|
||||
return rlRes.Res
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func RoleErrUnresolved(ii Identifiers) error {
|
||||
return fmt.Errorf("role unresolved %v", ii.StringSlice())
|
||||
}
|
||||
57
server/pkg/envoy/resource/settings.go
Normal file
57
server/pkg/envoy/resource/settings.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
Settings struct {
|
||||
*base
|
||||
Res types.SettingValueSet
|
||||
}
|
||||
|
||||
Setting struct {
|
||||
*base
|
||||
Res types.SettingValue
|
||||
}
|
||||
)
|
||||
|
||||
func NewSettings(vv map[string]interface{}) *Settings {
|
||||
r := &Settings{base: &base{}}
|
||||
r.SetResourceType(SettingsResourceType)
|
||||
|
||||
r.Res = make(types.SettingValueSet, 0, len(vv))
|
||||
for k, v := range vv {
|
||||
sv := &types.SettingValue{
|
||||
Name: k,
|
||||
}
|
||||
sv.SetSetting(v)
|
||||
r.Res = append(r.Res, sv)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func NewSetting(s *types.SettingValue) *Setting {
|
||||
r := &Setting{base: &base{}}
|
||||
r.SetResourceType(SettingsResourceType)
|
||||
r.Res = *s
|
||||
|
||||
r.AddIdentifier(identifiers(s.Name)...)
|
||||
|
||||
// Set initial stamps
|
||||
r.SetTimestamps(MakeTimestampsCUDA(nil, &s.UpdatedAt, nil, nil))
|
||||
r.SetUserstamps(&Userstamps{
|
||||
UpdatedBy: &Userstamp{UserID: s.UpdatedBy},
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Setting) Resource() interface{} {
|
||||
return r.Res
|
||||
}
|
||||
|
||||
func (r *Settings) Resource() interface{} {
|
||||
return r.Res
|
||||
}
|
||||
64
server/pkg/envoy/resource/shaper.go
Normal file
64
server/pkg/envoy/resource/shaper.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package resource
|
||||
|
||||
import "fmt"
|
||||
|
||||
type (
|
||||
shaper interface {
|
||||
Shape([]Interface) ([]Interface, error)
|
||||
}
|
||||
)
|
||||
|
||||
// Shape shapes ResourceDatasets based on their correlated Template
|
||||
//
|
||||
// During the shaping step, no data is removed, nor added to the original resource slice.
|
||||
// The shapers are ran in the same order they were provided.
|
||||
func Shape(rr []Interface, ss ...shaper) ([]Interface, error) {
|
||||
ii := make([]Interface, 0, int(len(rr)/2))
|
||||
|
||||
// Firstly handle any resource shaping
|
||||
for _, s := range ss {
|
||||
sdd, err := s.Shape(rr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ii = append(ii, sdd...)
|
||||
}
|
||||
|
||||
// Cleanup the final shaped resource slice.
|
||||
// @todo make this cleaner!!1!'
|
||||
//
|
||||
// After this point, all of the data should be ready for further processing.
|
||||
for _, r := range rr {
|
||||
if _, ok := r.(*ComposeRecordTemplate); ok {
|
||||
continue
|
||||
}
|
||||
if _, ok := r.(*ResourceDataset); ok {
|
||||
continue
|
||||
}
|
||||
|
||||
ii = append(ii, r)
|
||||
}
|
||||
|
||||
return ii, nil
|
||||
}
|
||||
|
||||
// findResourceDataset finds and returns the first dataset that matches the
|
||||
// provided identifiers.
|
||||
func findResourceDataset(rr []Interface, ii Identifiers) *ResourceDataset {
|
||||
for _, r := range rr {
|
||||
genR, ok := r.(*ResourceDataset)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if !genR.Identifiers().HasAny(ii) {
|
||||
continue
|
||||
}
|
||||
|
||||
return genR
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func genericSourceErrUnresolved(ii Identifiers) error {
|
||||
return fmt.Errorf("generic source unresolved %v", ii.StringSlice())
|
||||
}
|
||||
73
server/pkg/envoy/resource/template.go
Normal file
73
server/pkg/envoy/resource/template.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
// Template represents a Template
|
||||
Template struct {
|
||||
*base
|
||||
Res *types.Template
|
||||
}
|
||||
)
|
||||
|
||||
func NewTemplate(t *types.Template) *Template {
|
||||
r := &Template{base: &base{}}
|
||||
r.SetResourceType(types.TemplateResourceType)
|
||||
r.Res = t
|
||||
|
||||
r.AddIdentifier(identifiers(t.Handle, t.Meta.Short, t.ID)...)
|
||||
|
||||
// Initial timestamps
|
||||
r.SetTimestamps(MakeTimestampsCUDAS(&t.CreatedAt, t.UpdatedAt, t.DeletedAt, nil, nil))
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Template) Resource() interface{} {
|
||||
return r.Res
|
||||
}
|
||||
|
||||
func (r *Template) SysID() uint64 {
|
||||
return r.Res.ID
|
||||
}
|
||||
|
||||
func (r *Template) RBACParts() (resource string, ref *Ref, path []*Ref) {
|
||||
ref = r.Ref()
|
||||
path = nil
|
||||
resource = fmt.Sprintf(types.TemplateRbacResourceTpl(), types.TemplateResourceType, firstOkString(strconv.FormatUint(r.Res.ID, 10), r.Res.Handle))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// FindTemplate looks for the template in the resources
|
||||
func FindTemplate(rr InterfaceSet, ii Identifiers) (u *types.Template) {
|
||||
var tRes *Template
|
||||
|
||||
rr.Walk(func(r Interface) error {
|
||||
tr, ok := r.(*Template)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if tr.Identifiers().HasAny(ii) {
|
||||
tRes = tr
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Found it
|
||||
if tRes != nil {
|
||||
return tRes.Res
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func TemplateErrUnresolved(ii Identifiers) error {
|
||||
return fmt.Errorf("template unresolved %v", ii.StringSlice())
|
||||
}
|
||||
119
server/pkg/envoy/resource/timestamps.go
Normal file
119
server/pkg/envoy/resource/timestamps.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
Timestamp struct {
|
||||
// S is a stringified representation of the timestamp
|
||||
S string
|
||||
// T is a time representation of the timestamp
|
||||
T *time.Time
|
||||
// Tz is the timezone the timestamp will be encoded into
|
||||
Tz string
|
||||
// Tpl is the template the timestamp will be encoded into
|
||||
Tpl string
|
||||
}
|
||||
Timestamps struct {
|
||||
CreatedAt *Timestamp
|
||||
UpdatedAt *Timestamp
|
||||
DeletedAt *Timestamp
|
||||
ArchivedAt *Timestamp
|
||||
SuspendedAt *Timestamp
|
||||
}
|
||||
)
|
||||
|
||||
func (ts *Timestamp) MarshalYAML() (interface{}, error) {
|
||||
return ts.S, nil
|
||||
}
|
||||
|
||||
func (ts *Timestamp) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(ts.S)
|
||||
}
|
||||
|
||||
// MakeTimestamp initializes a Timestamp from the passed timestamp
|
||||
func MakeTimestamp(ts string) *Timestamp {
|
||||
t := toTime(ts)
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &Timestamp{
|
||||
S: ts,
|
||||
T: t,
|
||||
}
|
||||
}
|
||||
|
||||
// MakeTimestampsCUDA initializes timestamps for createdAt, updatedAt, deletedAt and archivedAt
|
||||
func MakeTimestampsCUDA(c, u, d, a *time.Time) *Timestamps {
|
||||
t := &Timestamps{}
|
||||
|
||||
if c != nil && !c.IsZero() {
|
||||
t.CreatedAt = &Timestamp{T: c}
|
||||
}
|
||||
if u != nil && !u.IsZero() {
|
||||
t.UpdatedAt = &Timestamp{T: u}
|
||||
}
|
||||
if d != nil && !d.IsZero() {
|
||||
t.DeletedAt = &Timestamp{T: d}
|
||||
}
|
||||
if a != nil && !a.IsZero() {
|
||||
t.ArchivedAt = &Timestamp{T: a}
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
// MakeTimestampsCUDAS initializes timestamps for createdAt, updatedAt, deletedAt, archivedAt and suspendedAt
|
||||
func MakeTimestampsCUDAS(c, u, d, a, s *time.Time) *Timestamps {
|
||||
t := MakeTimestampsCUDA(c, u, d, a)
|
||||
|
||||
if s != nil && !s.IsZero() {
|
||||
t.SuspendedAt = &Timestamp{T: s}
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
// Model stringifies the timestamps based on the passed template and timezone
|
||||
func (tt *Timestamps) Model(tpl string, tz string) (*Timestamps, error) {
|
||||
var err error
|
||||
if tt.CreatedAt != nil {
|
||||
tt.CreatedAt, err = tt.CreatedAt.Model(tpl, tz)
|
||||
}
|
||||
if tt.UpdatedAt != nil {
|
||||
tt.UpdatedAt, err = tt.UpdatedAt.Model(tpl, tz)
|
||||
}
|
||||
if tt.DeletedAt != nil {
|
||||
tt.DeletedAt, err = tt.DeletedAt.Model(tpl, tz)
|
||||
}
|
||||
if tt.ArchivedAt != nil {
|
||||
tt.ArchivedAt, err = tt.ArchivedAt.Model(tpl, tz)
|
||||
}
|
||||
if tt.SuspendedAt != nil {
|
||||
tt.SuspendedAt, err = tt.SuspendedAt.Model(tpl, tz)
|
||||
}
|
||||
|
||||
return tt, err
|
||||
}
|
||||
|
||||
// Model stringifies the timestamp based on the passed template and timezone
|
||||
func (ts *Timestamp) Model(tpl string, tz string) (*Timestamp, error) {
|
||||
if tz != "" {
|
||||
tzL, err := time.LoadLocation(tz)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t := ts.T.In(tzL)
|
||||
ts.T = &t
|
||||
}
|
||||
|
||||
if tpl == "" {
|
||||
tpl = time.RFC3339
|
||||
}
|
||||
|
||||
ts.S = ts.T.Format(tpl)
|
||||
return ts, nil
|
||||
}
|
||||
343
server/pkg/envoy/resource/types.go
Normal file
343
server/pkg/envoy/resource/types.go
Normal file
@@ -0,0 +1,343 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type (
|
||||
Interface interface {
|
||||
Identifiers() Identifiers
|
||||
ResourceType() string
|
||||
Resource() interface{}
|
||||
Refs() RefSet
|
||||
MarkPlaceholder()
|
||||
Placeholder() bool
|
||||
ReID(Identifiers)
|
||||
ReRef(old RefSet, new RefSet)
|
||||
}
|
||||
|
||||
ErrorWrapper interface {
|
||||
WrapError(act string, err error) error
|
||||
}
|
||||
|
||||
InterfaceSet []Interface
|
||||
|
||||
IdentifiableInterface interface {
|
||||
Interface
|
||||
|
||||
SysID() uint64
|
||||
}
|
||||
|
||||
RefableInterface interface {
|
||||
Interface
|
||||
|
||||
Ref() string
|
||||
}
|
||||
|
||||
RBACInterface interface {
|
||||
Interface
|
||||
|
||||
RBACParts() (string, *Ref, []*Ref)
|
||||
}
|
||||
|
||||
LocaleInterface interface {
|
||||
Interface
|
||||
|
||||
ResourceTranslationParts() (string, *Ref, []*Ref)
|
||||
EncodeTranslations() ([]*ResourceTranslation, error)
|
||||
}
|
||||
|
||||
PrunableInterface interface {
|
||||
Interface
|
||||
|
||||
Prune(*Ref)
|
||||
}
|
||||
|
||||
RefSet []*Ref
|
||||
Ref struct {
|
||||
// @todo check with Denis regarding strings here (the cdocs comment)
|
||||
// @todo should this become node type instead?
|
||||
ResourceType string
|
||||
Identifiers Identifiers
|
||||
Constraints RefSet
|
||||
}
|
||||
|
||||
Identifiers []string
|
||||
)
|
||||
|
||||
var (
|
||||
DataSourceResourceType = "data:raw"
|
||||
SettingsResourceType = "setting"
|
||||
RbacResourceType = "rbac-rule"
|
||||
ResourceTranslationType = "resource-translation"
|
||||
)
|
||||
|
||||
func MakeRef(rt string, ii Identifiers) *Ref {
|
||||
return &Ref{ResourceType: rt, Identifiers: ii}
|
||||
}
|
||||
|
||||
func MakeWildRef(rt string) *Ref {
|
||||
return &Ref{ResourceType: rt, Identifiers: MakeIdentifiers("*")}
|
||||
}
|
||||
|
||||
func MakeIdentifiers(ss ...string) Identifiers {
|
||||
ii := make(Identifiers, 0, len(ss))
|
||||
ii = ii.Add(ss...)
|
||||
return ii
|
||||
}
|
||||
|
||||
func (ri Identifiers) Add(ii ...string) Identifiers {
|
||||
for _, i := range ii {
|
||||
if len(i) > 0 {
|
||||
ri = append(ri, i)
|
||||
}
|
||||
}
|
||||
|
||||
return ri
|
||||
}
|
||||
|
||||
func (ri Identifiers) Clone() Identifiers {
|
||||
out := make(Identifiers, 0, len(ri))
|
||||
out = append(out, ri...)
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (ri Identifiers) HasAny(check Identifiers) bool {
|
||||
// The size of these will be tiny so no need for hashmaps
|
||||
for _, i := range ri {
|
||||
for _, j := range check {
|
||||
if i == j {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (ri Identifiers) StringSlice() []string {
|
||||
return ri
|
||||
}
|
||||
|
||||
func (ri Identifiers) First() string {
|
||||
if len(ri) == 0 {
|
||||
return ""
|
||||
}
|
||||
return ri[0]
|
||||
}
|
||||
|
||||
func (ri Identifiers) FirstID() uint64 {
|
||||
if len(ri) <= 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
for _, s := range ri {
|
||||
if v, err := strconv.ParseUint(s, 10, 64); err != nil {
|
||||
continue
|
||||
} else {
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func (rr InterfaceSet) Walk(f func(r Interface) error) (err error) {
|
||||
for _, r := range rr {
|
||||
err = f(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SearchForIdentifiers returns the resources where the provided identifiers exist
|
||||
//
|
||||
// The Resource is matching if at least one identifier matches.
|
||||
func (rr InterfaceSet) SearchForIdentifiers(resource string, ii Identifiers) (out InterfaceSet) {
|
||||
out = make(InterfaceSet, 0, len(rr)/2)
|
||||
|
||||
for _, r := range rr {
|
||||
if r.ResourceType() != resource {
|
||||
continue
|
||||
}
|
||||
|
||||
if r.Identifiers().HasAny(ii) {
|
||||
out = append(out, r)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SearchForReferences returns the resources where the provided references exist
|
||||
//
|
||||
// The Resource is matching if at least one reference matches.
|
||||
func (rr InterfaceSet) SearchForReferences(ref *Ref) (out InterfaceSet) {
|
||||
out = make(InterfaceSet, 0, len(rr)/2)
|
||||
|
||||
for _, r := range rr {
|
||||
if r.Refs().HasRef(ref) {
|
||||
out = append(out, r)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Constraint returns the current reference with added constraint
|
||||
func (r *Ref) Constraint(c *Ref) *Ref {
|
||||
if r.Constraints == nil {
|
||||
r.Constraints = make(RefSet, 0, 1)
|
||||
}
|
||||
|
||||
r.Constraints = append(r.Constraints, &Ref{
|
||||
ResourceType: c.ResourceType,
|
||||
Identifiers: MakeIdentifiers(c.Identifiers.StringSlice()...),
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// IsWildcard checks if this Ref points to all resources of a specific resource type
|
||||
func (r *Ref) IsWildcard() bool {
|
||||
if len(r.Identifiers) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, i := range r.Identifiers {
|
||||
if i == "*" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *Ref) equals(b *Ref) bool {
|
||||
if a == nil && a == b {
|
||||
return true
|
||||
}
|
||||
|
||||
if a == nil && a != b {
|
||||
return false
|
||||
}
|
||||
|
||||
if a.ResourceType != b.ResourceType {
|
||||
return false
|
||||
}
|
||||
|
||||
if !b.IsWildcard() && !a.Identifiers.HasAny(b.Identifiers) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, c := range b.Constraints {
|
||||
if !a.Constraints.HasRef(c) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (rr RefSet) findRef(ref *Ref) int {
|
||||
for i, r := range rr {
|
||||
if r.equals(ref) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
// replaceRef replaces the reference both on the ref level and on the
|
||||
// constraint level.
|
||||
func (rr RefSet) replaceRef(old, new *Ref) RefSet {
|
||||
found := false
|
||||
|
||||
for x := len(rr) - 1; x >= 0; x-- {
|
||||
r := rr[x]
|
||||
|
||||
if r.equals(old) {
|
||||
found = true
|
||||
if new == nil {
|
||||
rr[x] = rr[len(rr)-1]
|
||||
return rr[:len(rr)-1]
|
||||
} else {
|
||||
rr[x] = new
|
||||
}
|
||||
} else {
|
||||
if len(r.Constraints) > 0 {
|
||||
r.Constraints = r.Constraints.replaceRef(old, new)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found && new != nil {
|
||||
return append(rr, new)
|
||||
}
|
||||
|
||||
return rr
|
||||
}
|
||||
|
||||
func (rr RefSet) HasRef(ref *Ref) bool {
|
||||
return rr.findRef(ref) > -1
|
||||
}
|
||||
|
||||
// IsSbubset checks if aa is subset of bb
|
||||
//
|
||||
// Order is important; primary use is RBAC and res. tr. path checking.
|
||||
func (aa RefSet) IsSubset(bb RefSet) bool {
|
||||
if len(bb) > len(aa) {
|
||||
return false
|
||||
}
|
||||
|
||||
return aa[0:len(bb)].Equals(bb)
|
||||
}
|
||||
|
||||
// Equals checks if two RefSets match
|
||||
//
|
||||
// They match if everything matches, even constraints (important)
|
||||
func (aa RefSet) Equals(bb RefSet) bool {
|
||||
if len(aa) != len(bb) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i, a := range aa {
|
||||
if !a.equals(bb[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Unique returns only unique references
|
||||
//
|
||||
// Uniqueness is defined as "two references may not define
|
||||
// the same resource type and identifier" combinations.
|
||||
func (rr RefSet) Unique() RefSet {
|
||||
out := make(RefSet, 0, len(rr))
|
||||
seen := make(map[string]Identifiers)
|
||||
|
||||
for _, r := range rr {
|
||||
ii, ok := seen[r.ResourceType]
|
||||
|
||||
// type not seen at all, unique
|
||||
if !ok {
|
||||
out = append(out, r)
|
||||
seen[r.ResourceType] = r.Identifiers.Clone()
|
||||
continue
|
||||
}
|
||||
|
||||
// not yet seen
|
||||
if !ii.HasAny(r.Identifiers) {
|
||||
out = append(out, r)
|
||||
seen[r.ResourceType] = seen[r.ResourceType].Add(r.Identifiers...)
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
96
server/pkg/envoy/resource/user.go
Normal file
96
server/pkg/envoy/resource/user.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
// User represents a User
|
||||
User struct {
|
||||
*base
|
||||
Res *types.User
|
||||
|
||||
RoleMembership []Identifiers
|
||||
RefRoles RefSet
|
||||
}
|
||||
)
|
||||
|
||||
func NewUser(u *types.User, roles ...string) *User {
|
||||
r := &User{base: &base{}}
|
||||
r.SetResourceType(types.UserResourceType)
|
||||
r.Res = u
|
||||
|
||||
r.AddIdentifier(identifiers(u.Handle, u.Email, u.Name, u.ID)...)
|
||||
|
||||
// Role membership
|
||||
for _, role := range roles {
|
||||
rid := MakeIdentifiers(role)
|
||||
r.RoleMembership = append(r.RoleMembership, rid)
|
||||
|
||||
r.RefRoles = append(r.RefRoles, r.AddRef(types.RoleResourceType, role))
|
||||
}
|
||||
|
||||
// Initial timestamps
|
||||
r.SetTimestamps(MakeTimestampsCUDAS(&u.CreatedAt, u.UpdatedAt, u.DeletedAt, nil, u.SuspendedAt))
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (u *User) AddRoles(roles ...*types.Role) *User {
|
||||
for _, r := range roles {
|
||||
idf := firstOkString(r.Handle, strconv.FormatUint(r.ID, 10))
|
||||
rid := MakeIdentifiers(idf)
|
||||
u.RoleMembership = append(u.RoleMembership, rid)
|
||||
|
||||
u.RefRoles = append(u.RefRoles, u.AddRef(types.RoleResourceType, idf))
|
||||
}
|
||||
|
||||
return u
|
||||
}
|
||||
|
||||
func (r *User) Resource() interface{} {
|
||||
return r.Res
|
||||
}
|
||||
|
||||
func (r *User) SysID() uint64 {
|
||||
return r.Res.ID
|
||||
}
|
||||
|
||||
func (r *User) RBACParts() (resource string, ref *Ref, path []*Ref) {
|
||||
ref = r.Ref()
|
||||
path = nil
|
||||
resource = fmt.Sprintf(types.UserRbacResourceTpl(), types.UserResourceType, firstOkString(strconv.FormatUint(r.Res.ID, 10), r.Res.Handle, r.Res.Username))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// FindUser looks for the user in the resources
|
||||
func FindUser(rr InterfaceSet, ii Identifiers) (u *types.User) {
|
||||
var uRes *User
|
||||
|
||||
rr.Walk(func(r Interface) error {
|
||||
ur, ok := r.(*User)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if ur.Identifiers().HasAny(ii) {
|
||||
uRes = ur
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Found it
|
||||
if uRes != nil {
|
||||
return uRes.Res
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func UserErrUnresolved(ii Identifiers) error {
|
||||
return fmt.Errorf("user unresolved %v", ii.StringSlice())
|
||||
}
|
||||
242
server/pkg/envoy/resource/userstamps.go
Normal file
242
server/pkg/envoy/resource/userstamps.go
Normal file
@@ -0,0 +1,242 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
Userstamp struct {
|
||||
UserID uint64
|
||||
Ref string
|
||||
U *types.User
|
||||
// S is the stringified user representation
|
||||
S string
|
||||
}
|
||||
|
||||
Userstamps struct {
|
||||
CreatedBy *Userstamp
|
||||
UpdatedBy *Userstamp
|
||||
DeletedBy *Userstamp
|
||||
OwnedBy *Userstamp
|
||||
RunAs *Userstamp
|
||||
}
|
||||
UserstampIndex map[string]*Userstamp
|
||||
)
|
||||
|
||||
func (us *Userstamp) MarshalYAML() (interface{}, error) {
|
||||
if us == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if us.U != nil {
|
||||
if us.U.Handle != "" {
|
||||
return us.U.Handle, nil
|
||||
}
|
||||
if us.U.Username != "" {
|
||||
return us.U.Username, nil
|
||||
}
|
||||
if us.U.Email != "" {
|
||||
return us.U.Email, nil
|
||||
}
|
||||
if us.U.Name != "" {
|
||||
return us.U.Name, nil
|
||||
}
|
||||
}
|
||||
|
||||
if us.Ref != "" {
|
||||
return us.Ref, nil
|
||||
}
|
||||
|
||||
if us.UserID > 0 {
|
||||
return us.UserID, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("invalid userstamp")
|
||||
}
|
||||
|
||||
func (us *Userstamp) MarshalJSON() ([]byte, error) {
|
||||
if us == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
l := ""
|
||||
|
||||
if us.U != nil {
|
||||
if us.U.Handle != "" {
|
||||
l = us.U.Handle
|
||||
}
|
||||
if us.U.Username != "" {
|
||||
l = us.U.Username
|
||||
}
|
||||
if us.U.Email != "" {
|
||||
l = us.U.Email
|
||||
}
|
||||
if us.U.Name != "" {
|
||||
l = us.U.Name
|
||||
}
|
||||
} else {
|
||||
if us.Ref != "" {
|
||||
l = us.Ref
|
||||
}
|
||||
|
||||
if us.UserID > 0 {
|
||||
l = strconv.FormatUint(us.UserID, 10)
|
||||
}
|
||||
}
|
||||
|
||||
if l == "" {
|
||||
return nil, fmt.Errorf("invalid userstamp")
|
||||
}
|
||||
|
||||
return json.Marshal(l)
|
||||
}
|
||||
|
||||
// MakeUserstamp initializes a userstamp from the passed user struct
|
||||
func MakeUserstamp(u *types.User) *Userstamp {
|
||||
sID := strconv.FormatUint(u.ID, 10)
|
||||
return &Userstamp{
|
||||
UserID: u.ID,
|
||||
U: u,
|
||||
Ref: firstOkString(u.Handle, u.Email, u.Username, sID),
|
||||
}
|
||||
}
|
||||
|
||||
// MakeUserstampFromRef initializes a userstamp from the passed reference
|
||||
//
|
||||
// If possible, the function determines the userID
|
||||
func MakeUserstampFromRef(ref string) *Userstamp {
|
||||
id, err := strconv.ParseUint(ref, 10, 64)
|
||||
|
||||
us := &Userstamp{}
|
||||
|
||||
if err == nil && id != 0 {
|
||||
us.UserID = id
|
||||
us.U = &types.User{ID: id}
|
||||
}
|
||||
us.Ref = ref
|
||||
|
||||
return us
|
||||
}
|
||||
|
||||
// MakeUserstampFromRef initializes a userstamp from the passed userID
|
||||
func MakeUserstampFromID(ID uint64) *Userstamp {
|
||||
if ID == 0 {
|
||||
return nil
|
||||
}
|
||||
return MakeUserstampFromRef(strconv.FormatUint(ID, 10))
|
||||
}
|
||||
|
||||
// MakeUserstampsCUDO initializes userstamps for createdAt, updatedAt, deletedAt and ownedBy
|
||||
func MakeUserstampsCUDO(c, u, d, o uint64) *Userstamps {
|
||||
us := &Userstamps{}
|
||||
|
||||
if c > 0 {
|
||||
us.CreatedBy = &Userstamp{
|
||||
UserID: c,
|
||||
Ref: strconv.FormatUint(c, 10),
|
||||
}
|
||||
}
|
||||
if u > 0 {
|
||||
us.UpdatedBy = &Userstamp{
|
||||
UserID: u,
|
||||
Ref: strconv.FormatUint(u, 10),
|
||||
}
|
||||
}
|
||||
if d > 0 {
|
||||
us.DeletedBy = &Userstamp{
|
||||
UserID: d,
|
||||
Ref: strconv.FormatUint(d, 10),
|
||||
}
|
||||
}
|
||||
if o > 0 {
|
||||
us.OwnedBy = &Userstamp{
|
||||
UserID: o,
|
||||
Ref: strconv.FormatUint(o, 10),
|
||||
}
|
||||
}
|
||||
|
||||
return us
|
||||
}
|
||||
|
||||
// Model stringifies the userstamp
|
||||
//
|
||||
// @todo make this configurable
|
||||
func (us *Userstamp) Model() (string, error) {
|
||||
if us == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if us.U != nil {
|
||||
if us.U.Handle != "" {
|
||||
return us.U.Handle, nil
|
||||
}
|
||||
if us.U.Username != "" {
|
||||
return us.U.Username, nil
|
||||
}
|
||||
if us.U.Email != "" {
|
||||
return us.U.Email, nil
|
||||
}
|
||||
if us.U.Name != "" {
|
||||
return us.U.Name, nil
|
||||
}
|
||||
}
|
||||
|
||||
if us.Ref != "" {
|
||||
return us.Ref, nil
|
||||
}
|
||||
|
||||
if us.UserID > 0 {
|
||||
return strconv.FormatUint(us.UserID, 10), nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("invalid userstamp")
|
||||
}
|
||||
|
||||
func (ux UserstampIndex) Add(uu ...*types.User) {
|
||||
for _, u := range uu {
|
||||
sID := strconv.FormatUint(u.ID, 10)
|
||||
s := MakeUserstamp(u)
|
||||
|
||||
ux[sID] = s
|
||||
ux[u.Email] = s
|
||||
if u.Handle != "" {
|
||||
ux[u.Handle] = s
|
||||
}
|
||||
if u.Username != "" {
|
||||
ux[u.Username] = s
|
||||
}
|
||||
if u.Name != "" {
|
||||
ux[u.Name+" "+u.Email] = s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ux UserstampIndex) GetByKey(kr interface{}) *Userstamp {
|
||||
if k, ok := kr.(string); ok {
|
||||
return ux[k]
|
||||
} else if k, ok := kr.(uint64); ok {
|
||||
return ux[strconv.FormatUint(k, 10)]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ux UserstampIndex) GetByStamp(s *Userstamp) *Userstamp {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if s.Ref != "" {
|
||||
return ux.GetByKey(s.Ref)
|
||||
}
|
||||
if s.UserID > 0 {
|
||||
return ux.GetByKey(s.UserID)
|
||||
}
|
||||
if s.U != nil && s.U.ID > 0 {
|
||||
return ux.GetByKey(s.U.ID)
|
||||
}
|
||||
return s
|
||||
}
|
||||
80
server/pkg/envoy/resource/util.go
Normal file
80
server/pkg/envoy/resource/util.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
systemTypes "github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
func firstOkString(ss ...string) string {
|
||||
for _, s := range ss {
|
||||
if s != "" && s != "0" {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Taken (and modified) from compose/service/values/sanitizer.go
|
||||
func toTime(v string) *time.Time {
|
||||
ff := []string{
|
||||
time.RFC3339,
|
||||
time.RFC1123Z,
|
||||
time.RFC1123,
|
||||
time.RFC850,
|
||||
time.RFC822Z,
|
||||
time.RFC822,
|
||||
time.RubyDate,
|
||||
time.UnixDate,
|
||||
time.ANSIC,
|
||||
"2006/_1/_2 15:04:05",
|
||||
"2006/_1/_2 15:04",
|
||||
}
|
||||
|
||||
for _, f := range ff {
|
||||
parsed, err := time.Parse(f, v)
|
||||
if err == nil {
|
||||
return &parsed
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ref builders
|
||||
|
||||
func MakeNamespaceRef(id uint64, handle, name string) *Ref {
|
||||
return makeGenericRef(types.NamespaceResourceType, id, handle, name)
|
||||
}
|
||||
|
||||
func MakeModuleRef(id uint64, handle, name string) *Ref {
|
||||
return makeGenericRef(types.ModuleResourceType, id, handle, name)
|
||||
}
|
||||
|
||||
func MakePageRef(id uint64, handle, title string) *Ref {
|
||||
return makeGenericRef(types.PageResourceType, id, handle, title)
|
||||
}
|
||||
|
||||
func MakeRoleRef(id uint64, handle, name string) *Ref {
|
||||
return makeGenericRef(systemTypes.RoleResourceType, id, handle, name)
|
||||
}
|
||||
|
||||
func makeGenericRef(t string, id uint64, ii ...string) *Ref {
|
||||
args := make([]interface{}, len(ii)+1)
|
||||
args[0] = id
|
||||
for i := range ii {
|
||||
args[i+1] = ii[i]
|
||||
}
|
||||
|
||||
aux := &Ref{
|
||||
ResourceType: t,
|
||||
Identifiers: identifiers(args...),
|
||||
}
|
||||
|
||||
if t == "" || len(aux.Identifiers) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return aux
|
||||
}
|
||||
19
server/pkg/envoy/shared.go
Normal file
19
server/pkg/envoy/shared.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package envoy
|
||||
|
||||
import "github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
|
||||
// procResSet is a little utility to run some op over given resources
|
||||
//
|
||||
// Helps cover special cases such as modules & module fields
|
||||
func procResSet(resources resource.InterfaceSet, fn func(r resource.Interface)) {
|
||||
for _, res := range resources {
|
||||
fn(res)
|
||||
|
||||
// Special case for modules since it has
|
||||
if modR, ok := res.(*resource.ComposeModule); ok {
|
||||
for _, f := range modR.ResFields {
|
||||
fn(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
113
server/pkg/envoy/store/api_gateway.go
Normal file
113
server/pkg/envoy/store/api_gateway.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
apiGateway struct {
|
||||
cfg *EncoderConfig
|
||||
|
||||
res *resource.APIGateway
|
||||
gwr *types.ApigwRoute
|
||||
ff types.ApigwFilterSet
|
||||
|
||||
ux *userIndex
|
||||
}
|
||||
apiGatewaySet []*apiGateway
|
||||
|
||||
apiGatewayFilter struct {
|
||||
cfg *EncoderConfig
|
||||
|
||||
res *resource.APIGatewayFilter
|
||||
tr *types.ApigwFilter
|
||||
}
|
||||
apiGatewayFilterSet []*apiGatewayFilter
|
||||
)
|
||||
|
||||
// mergeAPIGateways merges b into a, prioritising a
|
||||
func mergeAPIGateways(a, b *types.ApigwRoute) *types.ApigwRoute {
|
||||
c := a
|
||||
|
||||
if c.Endpoint == "" {
|
||||
c.Endpoint = b.Endpoint
|
||||
}
|
||||
if c.Method == "" {
|
||||
c.Method = b.Method
|
||||
}
|
||||
|
||||
c.Enabled = b.Enabled
|
||||
|
||||
if c.Group == 0 {
|
||||
c.Group = b.Group
|
||||
}
|
||||
|
||||
c.Meta = b.Meta
|
||||
|
||||
if c.CreatedBy == 0 {
|
||||
c.CreatedBy = b.CreatedBy
|
||||
}
|
||||
if c.UpdatedBy == 0 {
|
||||
c.UpdatedBy = b.UpdatedBy
|
||||
}
|
||||
if c.DeletedBy == 0 {
|
||||
c.DeletedBy = b.DeletedBy
|
||||
}
|
||||
|
||||
if c.CreatedAt.IsZero() {
|
||||
c.CreatedAt = b.CreatedAt
|
||||
}
|
||||
|
||||
if c.UpdatedAt == nil {
|
||||
c.UpdatedAt = b.UpdatedAt
|
||||
}
|
||||
|
||||
if c.DeletedAt == nil {
|
||||
c.DeletedAt = b.DeletedAt
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// findAPIGateway looks for the workflow in the resources & the store
|
||||
//
|
||||
// Provided resources are prioritized.
|
||||
func findAPIGateway(ctx context.Context, s store.Storer, rr resource.InterfaceSet, ii resource.Identifiers) (wf *types.ApigwRoute, err error) {
|
||||
wf = resource.FindAPIGateway(rr, ii)
|
||||
if wf != nil {
|
||||
return wf, nil
|
||||
}
|
||||
|
||||
return findAPIGatewayStore(ctx, s, makeGenericFilter(ii))
|
||||
}
|
||||
|
||||
// findAPIGatewayStore looks for the workflow in the store
|
||||
func findAPIGatewayStore(ctx context.Context, s store.Storer, gf genericFilter) (wf *types.ApigwRoute, err error) {
|
||||
if gf.id > 0 {
|
||||
wf, err = store.LookupApigwRouteByID(ctx, s, gf.id)
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if wf != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, i := range gf.identifiers {
|
||||
wf, err = store.LookupApigwRouteByEndpoint(ctx, s, i)
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if wf != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
215
server/pkg/envoy/store/api_gateway_marshal.go
Normal file
215
server/pkg/envoy/store/api_gateway_marshal.go
Normal file
@@ -0,0 +1,215 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
func newAPIGatewayFromResource(res *resource.APIGateway, cfg *EncoderConfig) resourceState {
|
||||
return &apiGateway{
|
||||
cfg: mergeConfig(cfg, res.Config()),
|
||||
|
||||
res: res,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *apiGateway) Prepare(ctx context.Context, pl *payload) (err error) {
|
||||
err = n.prepareRoute(ctx, pl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return n.prepareFilters(ctx, pl)
|
||||
}
|
||||
|
||||
func (n *apiGateway) prepareRoute(ctx context.Context, pl *payload) (err error) {
|
||||
// Reset old identifiers
|
||||
n.res.Res.ID = 0
|
||||
|
||||
// Try to get the original workflow
|
||||
n.gwr, err = findAPIGatewayStore(ctx, pl.s, makeGenericFilter(n.res.Identifiers()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if n.gwr != nil {
|
||||
n.res.Res.ID = n.gwr.ID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *apiGateway) prepareFilters(ctx context.Context, pl *payload) (err error) {
|
||||
if n.gwr == nil || n.gwr.ID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reset old identifiers
|
||||
for _, rf := range n.res.Filters {
|
||||
rf.Res.ID = 0
|
||||
}
|
||||
|
||||
// Try to find any related filters for this route
|
||||
tt, _, err := store.SearchApigwFilters(ctx, pl.s, types.ApigwFilterFilter{
|
||||
RouteID: n.gwr.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.ff = tt
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *apiGateway) Encode(ctx context.Context, pl *payload) (err error) {
|
||||
err = n.encodeRoute(ctx, pl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return n.encodeFilters(ctx, pl)
|
||||
}
|
||||
|
||||
func (n *apiGateway) encodeRoute(ctx context.Context, pl *payload) (err error) {
|
||||
res := n.res.Res
|
||||
exists := n.gwr != nil && n.gwr.ID > 0
|
||||
|
||||
// Determine the ID
|
||||
if res.ID <= 0 && exists {
|
||||
res.ID = n.gwr.ID
|
||||
}
|
||||
if res.ID <= 0 {
|
||||
res.ID = NextID()
|
||||
}
|
||||
|
||||
// Sys users
|
||||
us, err := resolveUserstamps(ctx, pl.s, pl.state.ParentResources, n.res.Userstamps())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ts := n.res.Timestamps()
|
||||
if ts != nil {
|
||||
if ts.CreatedAt != nil {
|
||||
res.CreatedAt = *ts.CreatedAt.T
|
||||
} else {
|
||||
res.CreatedAt = *now()
|
||||
}
|
||||
if ts.UpdatedAt != nil {
|
||||
res.UpdatedAt = ts.UpdatedAt.T
|
||||
}
|
||||
if ts.DeletedAt != nil {
|
||||
res.DeletedAt = ts.DeletedAt.T
|
||||
}
|
||||
}
|
||||
|
||||
res.CreatedBy = pl.invokerID
|
||||
if us != nil {
|
||||
if us.CreatedBy != nil {
|
||||
res.CreatedBy = us.CreatedBy.UserID
|
||||
}
|
||||
if us.UpdatedBy != nil {
|
||||
res.UpdatedBy = us.UpdatedBy.UserID
|
||||
}
|
||||
if us.DeletedBy != nil {
|
||||
res.DeletedBy = us.DeletedBy.UserID
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate the resource skip expression
|
||||
// @todo expand available parameters; similar implementation to automation/types/record@Dict
|
||||
if skip, err := basicSkipEval(ctx, n.cfg, !exists); err != nil {
|
||||
return err
|
||||
} else if skip {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a fresh workflow
|
||||
if !exists {
|
||||
return store.CreateApigwRoute(ctx, pl.s, res)
|
||||
}
|
||||
|
||||
// Update existing workflow
|
||||
switch n.cfg.OnExisting {
|
||||
case resource.Skip:
|
||||
return nil
|
||||
|
||||
case resource.MergeLeft:
|
||||
res = mergeAPIGateways(n.gwr, res)
|
||||
|
||||
case resource.MergeRight:
|
||||
res = mergeAPIGateways(res, n.gwr)
|
||||
}
|
||||
|
||||
err = store.UpdateApigwRoute(ctx, pl.s, res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.res.Res = res
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *apiGateway) encodeFilters(ctx context.Context, pl *payload) (err error) {
|
||||
exists := len(n.ff) > 0
|
||||
ff := make([]*types.ApigwFilter, 0, len(n.res.Filters))
|
||||
|
||||
for _, rf := range n.res.Filters {
|
||||
res := rf.Res
|
||||
res.Route = n.res.Res.ID
|
||||
res.ID = NextID()
|
||||
|
||||
// Sys users
|
||||
us, err := resolveUserstamps(ctx, pl.s, pl.state.ParentResources, rf.Userstamps())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ts := rf.Timestamps()
|
||||
if ts != nil {
|
||||
if ts.CreatedAt != nil {
|
||||
res.CreatedAt = *ts.CreatedAt.T
|
||||
} else {
|
||||
res.CreatedAt = *now()
|
||||
}
|
||||
if ts.UpdatedAt != nil {
|
||||
res.UpdatedAt = ts.UpdatedAt.T
|
||||
}
|
||||
if ts.DeletedAt != nil {
|
||||
res.DeletedAt = ts.DeletedAt.T
|
||||
}
|
||||
}
|
||||
res.CreatedBy = pl.invokerID
|
||||
if us != nil {
|
||||
if us.CreatedBy != nil {
|
||||
res.CreatedBy = us.CreatedBy.UserID
|
||||
}
|
||||
if us.UpdatedBy != nil {
|
||||
res.UpdatedBy = us.UpdatedBy.UserID
|
||||
}
|
||||
if us.DeletedBy != nil {
|
||||
res.DeletedBy = us.DeletedBy.UserID
|
||||
}
|
||||
}
|
||||
|
||||
ff = append(ff, res)
|
||||
}
|
||||
|
||||
// Create a fresh workflow
|
||||
if !exists {
|
||||
return store.CreateApigwFilter(ctx, pl.s, ff...)
|
||||
}
|
||||
|
||||
// If these filters already exist and we wish to modify them,
|
||||
// remove the old ones and create new ones
|
||||
switch n.cfg.OnExisting {
|
||||
case resource.Skip,
|
||||
resource.MergeLeft:
|
||||
return nil
|
||||
}
|
||||
|
||||
err = store.DeleteApigwFilter(ctx, pl.s, n.ff...)
|
||||
return store.CreateApigwFilter(ctx, pl.s, ff...)
|
||||
}
|
||||
30
server/pkg/envoy/store/api_gateway_unmarshal.go
Normal file
30
server/pkg/envoy/store/api_gateway_unmarshal.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
func newAPIGateway(gwr *types.ApigwRoute, ff types.ApigwFilterSet, ux *userIndex) *apiGateway {
|
||||
return &apiGateway{
|
||||
gwr: gwr,
|
||||
ff: ff,
|
||||
|
||||
ux: ux,
|
||||
}
|
||||
}
|
||||
|
||||
func (awf *apiGateway) MarshalEnvoy() ([]resource.Interface, error) {
|
||||
rs := resource.NewAPIGateway(awf.gwr)
|
||||
syncUserStamps(rs.Userstamps(), awf.ux)
|
||||
|
||||
for _, f := range awf.ff {
|
||||
rt := rs.AddGatewayFilter(f)
|
||||
syncUserStamps(rt.Userstamps(), awf.ux)
|
||||
}
|
||||
|
||||
return envoy.CollectNodes(
|
||||
rs,
|
||||
)
|
||||
}
|
||||
90
server/pkg/envoy/store/application.go
Normal file
90
server/pkg/envoy/store/application.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
application struct {
|
||||
cfg *EncoderConfig
|
||||
|
||||
res *resource.Application
|
||||
app *types.Application
|
||||
|
||||
ux *userIndex
|
||||
}
|
||||
)
|
||||
|
||||
// mergeApplications merges b into a, prioritising a
|
||||
func mergeApplications(a, b *types.Application) *types.Application {
|
||||
c := *a
|
||||
|
||||
if c.Name == "" {
|
||||
c.Name = b.Name
|
||||
}
|
||||
c.OwnerID = b.OwnerID
|
||||
|
||||
if c.CreatedAt.IsZero() {
|
||||
c.CreatedAt = b.CreatedAt
|
||||
}
|
||||
if c.UpdatedAt == nil {
|
||||
c.UpdatedAt = b.UpdatedAt
|
||||
}
|
||||
if c.DeletedAt == nil {
|
||||
c.DeletedAt = b.DeletedAt
|
||||
}
|
||||
|
||||
// I'll just compare the entire struct for now
|
||||
if c.Unify == nil || *c.Unify == (types.ApplicationUnify{}) {
|
||||
c.Unify = b.Unify
|
||||
}
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
// findApplication looks for the app in the resources & the store
|
||||
//
|
||||
// Provided resources are prioritized.
|
||||
func findApplication(ctx context.Context, s store.Storer, rr resource.InterfaceSet, ii resource.Identifiers) (ap *types.Application, err error) {
|
||||
ap = resource.FindApplication(rr, ii)
|
||||
if ap != nil {
|
||||
return ap, nil
|
||||
}
|
||||
|
||||
return findApplicationStore(ctx, s, makeGenericFilter(ii))
|
||||
}
|
||||
|
||||
// findApplicationStore looks for the app in the store
|
||||
func findApplicationStore(ctx context.Context, s store.Storer, gf genericFilter) (ap *types.Application, err error) {
|
||||
if gf.id > 0 {
|
||||
ap, err = store.LookupApplicationByID(ctx, s, gf.id)
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ap != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, i := range gf.identifiers {
|
||||
var aa types.ApplicationSet
|
||||
aa, _, err = store.SearchApplications(ctx, s, types.ApplicationFilter{Name: i})
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
if len(aa) > 1 {
|
||||
return nil, resourceErrIdentifierNotUnique(i)
|
||||
}
|
||||
if len(aa) == 1 {
|
||||
ap = aa[0]
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
108
server/pkg/envoy/store/application_marshal.go
Normal file
108
server/pkg/envoy/store/application_marshal.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
func NewApplicationFromResource(res *resource.Application, cfg *EncoderConfig) resourceState {
|
||||
return &application{
|
||||
cfg: mergeConfig(cfg, res.Config()),
|
||||
res: res,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *application) Prepare(ctx context.Context, pl *payload) (err error) {
|
||||
// Get the existing app
|
||||
n.app, err = findApplicationStore(ctx, pl.s, makeGenericFilter(n.res.Identifiers()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if n.app != nil {
|
||||
n.res.Res.ID = n.app.ID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *application) Encode(ctx context.Context, pl *payload) (err error) {
|
||||
res := n.res.Res
|
||||
exists := n.app != nil && n.app.ID > 0
|
||||
|
||||
// Determine the ID
|
||||
if res.ID <= 0 && exists {
|
||||
res.ID = n.app.ID
|
||||
}
|
||||
if res.ID <= 0 {
|
||||
res.ID = NextID()
|
||||
}
|
||||
|
||||
// Sys users
|
||||
us, err := resolveUserstamps(ctx, pl.s, pl.state.ParentResources, n.res.Userstamps())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Unify
|
||||
if res.Unify == nil {
|
||||
res.Unify = &types.ApplicationUnify{}
|
||||
}
|
||||
|
||||
ts := n.res.Timestamps()
|
||||
if ts != nil {
|
||||
if ts.CreatedAt != nil {
|
||||
res.CreatedAt = *ts.CreatedAt.T
|
||||
} else {
|
||||
res.CreatedAt = *now()
|
||||
}
|
||||
if ts.UpdatedAt != nil {
|
||||
res.UpdatedAt = ts.UpdatedAt.T
|
||||
}
|
||||
if ts.DeletedAt != nil {
|
||||
res.DeletedAt = ts.DeletedAt.T
|
||||
}
|
||||
}
|
||||
|
||||
if us != nil {
|
||||
if us.OwnedBy != nil {
|
||||
res.OwnerID = us.OwnedBy.UserID
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate the resource skip expression
|
||||
// @todo expand available parameters; similar implementation to compose/types/record@Dict
|
||||
if skip, err := basicSkipEval(ctx, n.cfg, !exists); err != nil {
|
||||
return err
|
||||
} else if skip {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create fresh application
|
||||
if !exists {
|
||||
return store.CreateApplication(ctx, pl.s, res)
|
||||
}
|
||||
|
||||
// Update existing application
|
||||
switch n.cfg.OnExisting {
|
||||
case resource.Skip:
|
||||
return nil
|
||||
|
||||
case resource.MergeLeft:
|
||||
res = mergeApplications(n.app, res)
|
||||
|
||||
case resource.MergeRight:
|
||||
res = mergeApplications(res, n.app)
|
||||
}
|
||||
|
||||
err = store.UpdateApplication(ctx, pl.s, res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.res.Res = res
|
||||
return nil
|
||||
}
|
||||
54
server/pkg/envoy/store/application_test.go
Normal file
54
server/pkg/envoy/store/application_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestApplication_Merger(t *testing.T) {
|
||||
req := require.New(t)
|
||||
|
||||
ub := &types.ApplicationUnify{
|
||||
Config: "config",
|
||||
Icon: "icon",
|
||||
}
|
||||
|
||||
now := time.Time{}
|
||||
nowP := &time.Time{}
|
||||
|
||||
empty := &types.Application{}
|
||||
full := &types.Application{
|
||||
Name: "name",
|
||||
OwnerID: 1,
|
||||
Enabled: true,
|
||||
Unify: ub,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: nowP,
|
||||
DeletedAt: nowP,
|
||||
}
|
||||
|
||||
t.Run("merge on empty", func(t *testing.T) {
|
||||
c := mergeApplications(empty, full)
|
||||
req.Equal("name", c.Name)
|
||||
req.Equal(uint64(1), c.OwnerID)
|
||||
req.False(c.Enabled)
|
||||
req.Equal(ub, c.Unify)
|
||||
req.Equal(now, c.CreatedAt)
|
||||
req.Equal(nowP, c.UpdatedAt)
|
||||
req.Equal(nowP, c.DeletedAt)
|
||||
})
|
||||
|
||||
t.Run("merge with empty", func(t *testing.T) {
|
||||
c := mergeApplications(full, empty)
|
||||
req.Equal("name", c.Name)
|
||||
req.Equal(uint64(0), c.OwnerID)
|
||||
req.True(c.Enabled)
|
||||
req.Equal(ub, c.Unify)
|
||||
req.Equal(now, c.CreatedAt)
|
||||
req.Equal(nowP, c.UpdatedAt)
|
||||
req.Equal(nowP, c.DeletedAt)
|
||||
})
|
||||
}
|
||||
23
server/pkg/envoy/store/application_unmarshal.go
Normal file
23
server/pkg/envoy/store/application_unmarshal.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
func newApplication(app *types.Application, ux *userIndex) *application {
|
||||
return &application{
|
||||
app: app,
|
||||
ux: ux,
|
||||
}
|
||||
}
|
||||
|
||||
func (app *application) MarshalEnvoy() ([]resource.Interface, error) {
|
||||
rs := resource.NewApplication(app.app)
|
||||
syncUserStamps(rs.Userstamps(), app.ux)
|
||||
|
||||
return envoy.CollectNodes(
|
||||
rs,
|
||||
)
|
||||
}
|
||||
176
server/pkg/envoy/store/automation.go
Normal file
176
server/pkg/envoy/store/automation.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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/filter"
|
||||
"github.com/cortezaproject/corteza/server/pkg/id"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
)
|
||||
|
||||
type (
|
||||
automationWorkflowFilter types.WorkflowFilter
|
||||
|
||||
automationStore interface {
|
||||
store.AutomationWorkflows
|
||||
store.AutomationTriggers
|
||||
}
|
||||
|
||||
automationDecoder struct {
|
||||
resourceID []uint64
|
||||
ux *userIndex
|
||||
}
|
||||
)
|
||||
|
||||
func newAutomationDecoder(ux *userIndex) *automationDecoder {
|
||||
return &automationDecoder{
|
||||
resourceID: make([]uint64, 0, 200),
|
||||
ux: ux,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *automationDecoder) decodeWorkflows(ctx context.Context, s automationStore, ff []*automationWorkflowFilter) *auxRsp {
|
||||
mm := make([]envoy.Marshaller, 0, 100)
|
||||
if ff == nil {
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
var nn types.WorkflowSet
|
||||
var fn types.WorkflowFilter
|
||||
var err error
|
||||
|
||||
for _, f := range ff {
|
||||
aux := *f
|
||||
|
||||
if aux.Limit == 0 {
|
||||
aux.Limit = 1000
|
||||
}
|
||||
|
||||
for {
|
||||
nn, fn, err = s.SearchAutomationWorkflows(ctx, types.WorkflowFilter(aux))
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
for _, n := range nn {
|
||||
// Index users
|
||||
err = d.ux.add(
|
||||
ctx,
|
||||
n.RunAs,
|
||||
n.OwnedBy,
|
||||
n.CreatedBy,
|
||||
n.UpdatedBy,
|
||||
n.DeletedBy,
|
||||
)
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
tt, _, err := s.SearchAutomationTriggers(ctx, types.TriggerFilter{
|
||||
WorkflowID: id.Strings(n.ID),
|
||||
Disabled: filter.StateInclusive,
|
||||
})
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
for _, t := range tt {
|
||||
// Index users
|
||||
err = d.ux.add(
|
||||
ctx,
|
||||
t.OwnedBy,
|
||||
t.CreatedBy,
|
||||
t.UpdatedBy,
|
||||
t.DeletedBy,
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
mm = append(mm, newAutomationWorkflow(n, tt, d.ux))
|
||||
d.resourceID = append(d.resourceID, n.ID)
|
||||
}
|
||||
|
||||
if fn.NextPage != nil {
|
||||
aux.PageCursor = fn.NextPage
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
func (df *DecodeFilter) automationFromResource(rr ...string) *DecodeFilter {
|
||||
for _, r := range rr {
|
||||
if !strings.HasPrefix(r, "automation") {
|
||||
continue
|
||||
}
|
||||
|
||||
id := ""
|
||||
if strings.Count(r, ":") == 2 && !strings.HasSuffix(r, "*") {
|
||||
// There is an identifier
|
||||
aux := strings.Split(r, ":")
|
||||
|
||||
id = aux[len(aux)-1]
|
||||
r = strings.Join(aux[:len(aux)-1], ":")
|
||||
}
|
||||
|
||||
switch strings.ToLower(r) {
|
||||
case "automation:workflow":
|
||||
df = df.AutomationWorkflows(&types.WorkflowFilter{
|
||||
Query: id,
|
||||
Disabled: filter.StateInclusive,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return df
|
||||
}
|
||||
|
||||
func (df *DecodeFilter) automationFromRef(rr ...*resource.Ref) *DecodeFilter {
|
||||
for _, r := range rr {
|
||||
if strings.Index(r.ResourceType, "automation") < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
switch r.ResourceType {
|
||||
case types.WorkflowResourceType:
|
||||
for _, i := range r.Identifiers.StringSlice() {
|
||||
df = df.AutomationWorkflows(&types.WorkflowFilter{
|
||||
Query: i,
|
||||
Disabled: filter.StateInclusive,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return df
|
||||
}
|
||||
|
||||
// AutomationWorkflows adds a new WorkflowFilter
|
||||
func (df *DecodeFilter) AutomationWorkflows(f *types.WorkflowFilter) *DecodeFilter {
|
||||
if df.automationWorkflow == nil {
|
||||
df.automationWorkflow = make([]*automationWorkflowFilter, 0, 1)
|
||||
}
|
||||
df.automationWorkflow = append(df.automationWorkflow, (*automationWorkflowFilter)(f))
|
||||
return df
|
||||
}
|
||||
138
server/pkg/envoy/store/automation_workflow.go
Normal file
138
server/pkg/envoy/store/automation_workflow.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/automation/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
)
|
||||
|
||||
type (
|
||||
automationWorkflow struct {
|
||||
cfg *EncoderConfig
|
||||
|
||||
res *resource.AutomationWorkflow
|
||||
wf *types.Workflow
|
||||
tt types.TriggerSet
|
||||
|
||||
ux *userIndex
|
||||
}
|
||||
automationWorkflowSet []*automationWorkflow
|
||||
|
||||
automationTrigger struct {
|
||||
cfg *EncoderConfig
|
||||
|
||||
res *resource.AutomationTrigger
|
||||
tr *types.Trigger
|
||||
}
|
||||
automationTriggerSet []*automationTrigger
|
||||
)
|
||||
|
||||
// mergeAutomationWorkflows merges b into a, prioritising a
|
||||
func mergeAutomationWorkflows(a, b *types.Workflow) *types.Workflow {
|
||||
c := a
|
||||
|
||||
if c.Handle == "" {
|
||||
c.Handle = b.Handle
|
||||
}
|
||||
if c.Meta == nil {
|
||||
c.Meta = b.Meta
|
||||
}
|
||||
|
||||
if c.Scope == nil {
|
||||
c.Scope = b.Scope
|
||||
}
|
||||
if c.Steps == nil {
|
||||
c.Steps = b.Steps
|
||||
}
|
||||
if c.Paths == nil {
|
||||
c.Paths = b.Paths
|
||||
}
|
||||
|
||||
if c.RunAs == 0 {
|
||||
c.RunAs = b.RunAs
|
||||
}
|
||||
if c.OwnedBy == 0 {
|
||||
c.OwnedBy = b.OwnedBy
|
||||
}
|
||||
if c.CreatedBy == 0 {
|
||||
c.CreatedBy = b.CreatedBy
|
||||
}
|
||||
if c.UpdatedBy == 0 {
|
||||
c.UpdatedBy = b.UpdatedBy
|
||||
}
|
||||
if c.DeletedBy == 0 {
|
||||
c.DeletedBy = b.DeletedBy
|
||||
}
|
||||
|
||||
if c.CreatedAt.IsZero() {
|
||||
c.CreatedAt = b.CreatedAt
|
||||
}
|
||||
|
||||
if c.UpdatedAt == nil {
|
||||
c.UpdatedAt = b.UpdatedAt
|
||||
}
|
||||
|
||||
if c.DeletedAt == nil {
|
||||
c.DeletedAt = b.DeletedAt
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// findAutomationWorkflow looks for the workflow in the resources & the store
|
||||
//
|
||||
// Provided resources are prioritized.
|
||||
func findAutomationWorkflow(ctx context.Context, s store.Storer, rr resource.InterfaceSet, ii resource.Identifiers) (wf *types.Workflow, err error) {
|
||||
wf = resource.FindAutomationWorkflow(rr, ii)
|
||||
if wf != nil {
|
||||
return wf, nil
|
||||
}
|
||||
|
||||
return findAutomationWorkflowStore(ctx, s, makeGenericFilter(ii))
|
||||
}
|
||||
|
||||
// findAutomationWorkflowStore looks for the workflow in the store
|
||||
func findAutomationWorkflowStore(ctx context.Context, s store.Storer, gf genericFilter) (wf *types.Workflow, err error) {
|
||||
if gf.id > 0 {
|
||||
wf, err = store.LookupAutomationWorkflowByID(ctx, s, gf.id)
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if wf != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, i := range gf.identifiers {
|
||||
wf, err = store.LookupAutomationWorkflowByHandle(ctx, s, i)
|
||||
if err == store.ErrNotFound {
|
||||
var nn types.WorkflowSet
|
||||
nn, _, err = store.SearchAutomationWorkflows(ctx, s, types.WorkflowFilter{
|
||||
Query: i,
|
||||
Paging: filter.Paging{
|
||||
Limit: 2,
|
||||
},
|
||||
})
|
||||
if len(nn) > 1 {
|
||||
return nil, resourceErrIdentifierNotUnique(i)
|
||||
}
|
||||
if len(nn) == 1 {
|
||||
wf = nn[0]
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if wf != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
239
server/pkg/envoy/store/automation_workflow_marshal.go
Normal file
239
server/pkg/envoy/store/automation_workflow_marshal.go
Normal file
@@ -0,0 +1,239 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/automation/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/cortezaproject/corteza/server/pkg/id"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
)
|
||||
|
||||
func newAutomationWorkflowFromResource(res *resource.AutomationWorkflow, cfg *EncoderConfig) resourceState {
|
||||
return &automationWorkflow{
|
||||
cfg: mergeConfig(cfg, res.Config()),
|
||||
|
||||
res: res,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *automationWorkflow) Prepare(ctx context.Context, pl *payload) (err error) {
|
||||
err = n.prepareWorkflows(ctx, pl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return n.prepareTriggers(ctx, pl)
|
||||
}
|
||||
|
||||
func (n *automationWorkflow) prepareWorkflows(ctx context.Context, pl *payload) (err error) {
|
||||
// Reset old identifiers
|
||||
n.res.Res.ID = 0
|
||||
|
||||
// Try to get the original workflow
|
||||
n.wf, err = findAutomationWorkflowStore(ctx, pl.s, makeGenericFilter(n.res.Identifiers()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if n.wf != nil {
|
||||
n.res.Res.ID = n.wf.ID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *automationWorkflow) prepareTriggers(ctx context.Context, pl *payload) (err error) {
|
||||
if n.wf == nil || n.wf.ID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reset old identifiers
|
||||
for _, t := range n.res.Triggers {
|
||||
t.Res.ID = 0
|
||||
t.Res.WorkflowID = 0
|
||||
}
|
||||
|
||||
// Try to find any related triggers for this workflow
|
||||
tt, _, err := store.SearchAutomationTriggers(ctx, pl.s, types.TriggerFilter{
|
||||
WorkflowID: id.Strings(n.wf.ID),
|
||||
Disabled: filter.StateInclusive,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.tt = tt
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *automationWorkflow) Encode(ctx context.Context, pl *payload) (err error) {
|
||||
err = n.encodeWorkflow(ctx, pl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return n.encodeTriggers(ctx, pl)
|
||||
}
|
||||
|
||||
func (n *automationWorkflow) encodeWorkflow(ctx context.Context, pl *payload) (err error) {
|
||||
res := n.res.Res
|
||||
exists := n.wf != nil && n.wf.ID > 0
|
||||
|
||||
// Determine the ID
|
||||
if res.ID <= 0 && exists {
|
||||
res.ID = n.wf.ID
|
||||
}
|
||||
if res.ID <= 0 {
|
||||
res.ID = NextID()
|
||||
}
|
||||
|
||||
// Sys users
|
||||
us, err := resolveUserstamps(ctx, pl.s, pl.state.ParentResources, n.res.Userstamps())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ts := n.res.Timestamps()
|
||||
if ts != nil {
|
||||
if ts.CreatedAt != nil {
|
||||
res.CreatedAt = *ts.CreatedAt.T
|
||||
} else {
|
||||
res.CreatedAt = *now()
|
||||
}
|
||||
if ts.UpdatedAt != nil {
|
||||
res.UpdatedAt = ts.UpdatedAt.T
|
||||
}
|
||||
if ts.DeletedAt != nil {
|
||||
res.DeletedAt = ts.DeletedAt.T
|
||||
}
|
||||
}
|
||||
|
||||
res.CreatedBy = pl.invokerID
|
||||
if us != nil {
|
||||
if us.OwnedBy != nil {
|
||||
res.OwnedBy = us.OwnedBy.UserID
|
||||
}
|
||||
if us.CreatedBy != nil {
|
||||
res.CreatedBy = us.CreatedBy.UserID
|
||||
}
|
||||
if us.UpdatedBy != nil {
|
||||
res.UpdatedBy = us.UpdatedBy.UserID
|
||||
}
|
||||
if us.DeletedBy != nil {
|
||||
res.DeletedBy = us.DeletedBy.UserID
|
||||
}
|
||||
if us.RunAs != nil {
|
||||
res.RunAs = us.RunAs.UserID
|
||||
}
|
||||
}
|
||||
|
||||
res.Steps = make(types.WorkflowStepSet, 0, 100)
|
||||
for _, sres := range n.res.Steps {
|
||||
res.Steps = append(res.Steps, sres.Res)
|
||||
}
|
||||
|
||||
res.Paths = make(types.WorkflowPathSet, 0, 100)
|
||||
for _, pres := range n.res.Paths {
|
||||
p := pres.Res
|
||||
res.Paths = append(res.Paths, p)
|
||||
}
|
||||
|
||||
// Evaluate the resource skip expression
|
||||
// @todo expand available parameters; similar implementation to automation/types/record@Dict
|
||||
if skip, err := basicSkipEval(ctx, n.cfg, !exists); err != nil {
|
||||
return err
|
||||
} else if skip {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a fresh workflow
|
||||
if !exists {
|
||||
return store.CreateAutomationWorkflow(ctx, pl.s, res)
|
||||
}
|
||||
|
||||
// Update existing workflow
|
||||
switch n.cfg.OnExisting {
|
||||
case resource.Skip:
|
||||
return nil
|
||||
|
||||
case resource.MergeLeft:
|
||||
res = mergeAutomationWorkflows(n.wf, res)
|
||||
|
||||
case resource.MergeRight:
|
||||
res = mergeAutomationWorkflows(res, n.wf)
|
||||
}
|
||||
|
||||
err = store.UpdateAutomationWorkflow(ctx, pl.s, res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.res.Res = res
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *automationWorkflow) encodeTriggers(ctx context.Context, pl *payload) (err error) {
|
||||
exists := len(n.tt) > 0
|
||||
rr := make([]*types.Trigger, 0, len(n.res.Triggers))
|
||||
|
||||
for _, tr := range n.res.Triggers {
|
||||
res := tr.Res
|
||||
res.WorkflowID = n.res.Res.ID
|
||||
res.ID = NextID()
|
||||
|
||||
// Sys users
|
||||
us, err := resolveUserstamps(ctx, pl.s, pl.state.ParentResources, tr.Userstamps())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ts := tr.Timestamps()
|
||||
if ts != nil {
|
||||
if ts.CreatedAt != nil {
|
||||
res.CreatedAt = *ts.CreatedAt.T
|
||||
} else {
|
||||
res.CreatedAt = *now()
|
||||
}
|
||||
if ts.UpdatedAt != nil {
|
||||
res.UpdatedAt = ts.UpdatedAt.T
|
||||
}
|
||||
if ts.DeletedAt != nil {
|
||||
res.DeletedAt = ts.DeletedAt.T
|
||||
}
|
||||
}
|
||||
res.CreatedBy = pl.invokerID
|
||||
if us != nil {
|
||||
if us.OwnedBy != nil {
|
||||
res.OwnedBy = us.OwnedBy.UserID
|
||||
}
|
||||
if us.CreatedBy != nil {
|
||||
res.CreatedBy = us.CreatedBy.UserID
|
||||
}
|
||||
if us.UpdatedBy != nil {
|
||||
res.UpdatedBy = us.UpdatedBy.UserID
|
||||
}
|
||||
if us.DeletedBy != nil {
|
||||
res.DeletedBy = us.DeletedBy.UserID
|
||||
}
|
||||
}
|
||||
|
||||
rr = append(rr, res)
|
||||
}
|
||||
|
||||
// Create a fresh workflow
|
||||
if !exists {
|
||||
return store.CreateAutomationTrigger(ctx, pl.s, rr...)
|
||||
}
|
||||
|
||||
// If these triggers already exist and we wish to modify them,
|
||||
// remove the old ones and create new ones
|
||||
switch n.cfg.OnExisting {
|
||||
case resource.Skip,
|
||||
resource.MergeLeft:
|
||||
return nil
|
||||
}
|
||||
|
||||
err = store.DeleteAutomationTrigger(ctx, pl.s, n.tt...)
|
||||
return store.CreateAutomationTrigger(ctx, pl.s, rr...)
|
||||
}
|
||||
38
server/pkg/envoy/store/automation_workflow_unmarshal.go
Normal file
38
server/pkg/envoy/store/automation_workflow_unmarshal.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/automation/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
)
|
||||
|
||||
func newAutomationWorkflow(wf *types.Workflow, tt types.TriggerSet, ux *userIndex) *automationWorkflow {
|
||||
return &automationWorkflow{
|
||||
wf: wf,
|
||||
tt: tt,
|
||||
|
||||
ux: ux,
|
||||
}
|
||||
}
|
||||
|
||||
func (awf *automationWorkflow) MarshalEnvoy() ([]resource.Interface, error) {
|
||||
rs := resource.NewAutomationWorkflow(awf.wf)
|
||||
syncUserStamps(rs.Userstamps(), awf.ux)
|
||||
|
||||
for _, t := range awf.tt {
|
||||
rt := rs.AddAutomationTrigger(t)
|
||||
syncUserStamps(rt.Userstamps(), awf.ux)
|
||||
}
|
||||
|
||||
for _, s := range awf.wf.Steps {
|
||||
rs.AddAutomationWorkflowStep(s)
|
||||
}
|
||||
|
||||
for _, p := range awf.wf.Paths {
|
||||
rs.AddAutomationWorkflowPath(p)
|
||||
}
|
||||
|
||||
return envoy.CollectNodes(
|
||||
rs,
|
||||
)
|
||||
}
|
||||
579
server/pkg/envoy/store/compose.go
Normal file
579
server/pkg/envoy/store/compose.go
Normal file
@@ -0,0 +1,579 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/dalutils"
|
||||
"github.com/cortezaproject/corteza/server/compose/service"
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/dal"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
stypes "github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
composeNamespaceFilter types.NamespaceFilter
|
||||
composeModuleFilter types.ModuleFilter
|
||||
composeRecordFilter types.RecordFilter
|
||||
composePageFilter types.PageFilter
|
||||
composeChartFilter types.ChartFilter
|
||||
|
||||
composeStore interface {
|
||||
store.ComposeAttachments
|
||||
store.ComposeCharts
|
||||
store.ComposeModuleFields
|
||||
store.ComposeModules
|
||||
store.ComposeNamespaces
|
||||
store.ComposePages
|
||||
}
|
||||
|
||||
dalService interface {
|
||||
Create(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, vv ...dal.ValueGetter) error
|
||||
Search(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, f filter.Filter) (dal.Iterator, error)
|
||||
Delete(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, pkv ...dal.ValueGetter) (err error)
|
||||
Update(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, pkv ...dal.ValueGetter) (err error)
|
||||
|
||||
ReplaceModel(ctx context.Context, currentAlts []*dal.Alteration, model *dal.Model) (newAlts []*dal.Alteration, err error)
|
||||
GetConnectionByID(uint64) *dal.ConnectionWrap
|
||||
}
|
||||
|
||||
composeDecoder struct {
|
||||
resourceID []uint64
|
||||
namespaceID []uint64
|
||||
|
||||
dal dalService
|
||||
}
|
||||
)
|
||||
|
||||
func newComposeDecoder(dal dalService) *composeDecoder {
|
||||
return &composeDecoder{
|
||||
resourceID: make([]uint64, 0, 200),
|
||||
namespaceID: make([]uint64, 0, 200),
|
||||
dal: dal,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *composeDecoder) decodeComposeNamespace(ctx context.Context, s composeStore, ff []*composeNamespaceFilter) *auxRsp {
|
||||
mm := make([]envoy.Marshaller, 0, 100)
|
||||
if ff == nil {
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
var nn types.NamespaceSet
|
||||
var fn types.NamespaceFilter
|
||||
var err error
|
||||
for _, f := range ff {
|
||||
aux := *f
|
||||
|
||||
if aux.Limit == 0 {
|
||||
aux.Limit = 1000
|
||||
}
|
||||
|
||||
for {
|
||||
nn, fn, err = s.SearchComposeNamespaces(ctx, types.NamespaceFilter(aux))
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
for _, n := range nn {
|
||||
d.namespaceID = append(d.namespaceID, n.ID)
|
||||
d.resourceID = append(d.resourceID, n.ID)
|
||||
|
||||
mm = append(mm, newComposeNamespace(n))
|
||||
}
|
||||
|
||||
if f.NextPage != nil {
|
||||
aux.PageCursor = fn.NextPage
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *composeDecoder) decodeComposeModule(ctx context.Context, s composeStore, ff []*composeModuleFilter) *auxRsp {
|
||||
mm := make([]envoy.Marshaller, 0, 100)
|
||||
if ff == nil {
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
if len(d.namespaceID) > 0 {
|
||||
ffNs := make([]*composeModuleFilter, 0, len(ff)+len(d.namespaceID))
|
||||
for _, nsID := range d.namespaceID {
|
||||
for _, f := range ff {
|
||||
fNs := *f
|
||||
fNs.NamespaceID = nsID
|
||||
ffNs = append(ffNs, &fNs)
|
||||
}
|
||||
}
|
||||
ff = ffNs
|
||||
}
|
||||
|
||||
var nn types.ModuleSet
|
||||
var fn types.ModuleFilter
|
||||
var err error
|
||||
for _, f := range ff {
|
||||
aux := *f
|
||||
|
||||
if aux.Limit == 0 {
|
||||
aux.Limit = 1000
|
||||
}
|
||||
|
||||
for {
|
||||
nn, fn, err = s.SearchComposeModules(ctx, types.ModuleFilter(aux))
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
for _, n := range nn {
|
||||
d.resourceID = append(d.resourceID, n.ID)
|
||||
|
||||
n.Fields, _, err = s.SearchComposeModuleFields(ctx, types.ModuleFieldFilter{
|
||||
ModuleID: []uint64{n.ID},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
mm = append(mm, newComposeModule(n))
|
||||
}
|
||||
|
||||
if f.NextPage != nil {
|
||||
aux.PageCursor = fn.NextPage
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *composeDecoder) decodeComposeRecord(ctx context.Context, s store.Storer, ff []*composeRecordFilter) *auxRsp {
|
||||
mm := make([]envoy.Marshaller, 0, 100)
|
||||
if ff == nil {
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
ac := service.AccessControl(s)
|
||||
|
||||
if len(d.namespaceID) > 0 {
|
||||
ffNs := make([]*composeRecordFilter, 0, len(ff)+len(d.namespaceID))
|
||||
for _, nsID := range d.namespaceID {
|
||||
for _, f := range ff {
|
||||
nn, _, err := s.SearchComposeModules(ctx, types.ModuleFilter{
|
||||
NamespaceID: nsID,
|
||||
})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, n := range nn {
|
||||
fNs := *f
|
||||
fNs.NamespaceID = nsID
|
||||
fNs.ModuleID = n.ID
|
||||
ffNs = append(ffNs, &fNs)
|
||||
}
|
||||
}
|
||||
}
|
||||
ff = ffNs
|
||||
}
|
||||
|
||||
// When decoding large amounts of records (milions) we can probably assume
|
||||
// that each system user exists somewhere in the record set.
|
||||
//
|
||||
// That said, it's probably cheeper to simply say that we probably need all of the
|
||||
// references to hold.
|
||||
// If a user is not there in the preparation step, we shouldn't fail straight away.
|
||||
//
|
||||
// @todo use some heuristic to determine when to just list all of the users
|
||||
// and when to preprocess the records to find out what users to get.
|
||||
|
||||
relUsers := make(resource.UserstampIndex)
|
||||
uu, _, err := store.SearchUsers(ctx, s, stypes.UserFilter{
|
||||
Paging: filter.Paging{
|
||||
Limit: 0,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
relUsers.Add(uu...)
|
||||
|
||||
mapValues := func(r *types.Record) map[string]resource.ComposeRecordRawValue {
|
||||
mod := r.GetModule()
|
||||
out := make(map[string]resource.ComposeRecordRawValue, len(r.Values))
|
||||
|
||||
for _, v := range r.Values {
|
||||
f := mod.Fields.FindByName(v.Name)
|
||||
if f == nil || !f.Multi {
|
||||
out[v.Name] = resource.ComposeRecordRawValue{
|
||||
Name: v.Name,
|
||||
Value: v.Value,
|
||||
}
|
||||
} else {
|
||||
if _, ok := out[v.Name]; !ok {
|
||||
out[v.Name] = resource.ComposeRecordRawValue{
|
||||
Name: v.Name,
|
||||
IsMulti: true,
|
||||
}
|
||||
}
|
||||
|
||||
vx := out[v.Name]
|
||||
vx.Values = append(vx.Values, v.Value)
|
||||
out[v.Name] = vx
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// Prepare a series of resource.ComposeRecord instances; one for each provided filter
|
||||
for _, f := range ff {
|
||||
aux := *f
|
||||
|
||||
if aux.Limit == 0 {
|
||||
aux.Limit = 1000
|
||||
}
|
||||
|
||||
mod, err := store.LookupComposeModuleByID(ctx, s, f.ModuleID)
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
ff, _, err := store.SearchComposeModuleFields(ctx, s, types.ModuleFieldFilter{ModuleID: []uint64{mod.ID}})
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
_ = ff.Walk(func(f *types.ModuleField) error {
|
||||
f.NamespaceID = mod.NamespaceID
|
||||
return nil
|
||||
})
|
||||
mod.Fields = ff
|
||||
|
||||
aux.Check = service.ComposeRecordFilterChecker(ctx, ac, mod)
|
||||
|
||||
// Refs
|
||||
auxRecord := &composeRecordAux{
|
||||
refMod: strconv.FormatUint(f.ModuleID, 10),
|
||||
relMod: mod,
|
||||
refNs: strconv.FormatUint(f.NamespaceID, 10),
|
||||
relUsers: relUsers,
|
||||
}
|
||||
|
||||
// Walker
|
||||
auxRecord.walker = func(cb func(r *resource.ComposeRecordRaw) error) error {
|
||||
var rr types.RecordSet
|
||||
var fn types.RecordFilter
|
||||
var err error
|
||||
|
||||
for {
|
||||
rr, fn, err = dalutils.ComposeRecordsList(ctx, d.dal, mod, types.RecordFilter(aux))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service.ComposeRecordFilterAC(ctx, ac, mod, rr...)
|
||||
|
||||
for _, n := range rr {
|
||||
// Create a raw record
|
||||
r := &resource.ComposeRecordRaw{
|
||||
ID: strconv.FormatUint(n.ID, 10),
|
||||
Values: mapValues(n),
|
||||
Ts: resource.MakeTimestampsCUDA(&n.CreatedAt, n.UpdatedAt, n.DeletedAt, nil),
|
||||
Us: resource.MakeUserstampsCUDO(n.CreatedBy, n.UpdatedBy, n.DeletedBy, n.OwnedBy),
|
||||
}
|
||||
|
||||
err = cb(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if fn.NextPage != nil {
|
||||
aux.PageCursor = fn.NextPage
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
mm = append(mm, newComposeRecordFromAux(auxRecord))
|
||||
|
||||
}
|
||||
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *composeDecoder) decodeComposePage(ctx context.Context, s composeStore, ff []*composePageFilter) *auxRsp {
|
||||
mm := make([]envoy.Marshaller, 0, 100)
|
||||
if ff == nil {
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
if len(d.namespaceID) > 0 {
|
||||
ffNs := make([]*composePageFilter, 0, len(ff)+len(d.namespaceID))
|
||||
for _, nsID := range d.namespaceID {
|
||||
for _, f := range ff {
|
||||
fNs := *f
|
||||
fNs.NamespaceID = nsID
|
||||
ffNs = append(ffNs, &fNs)
|
||||
}
|
||||
}
|
||||
ff = ffNs
|
||||
}
|
||||
|
||||
var nn types.PageSet
|
||||
var fn types.PageFilter
|
||||
var err error
|
||||
for _, f := range ff {
|
||||
aux := *f
|
||||
|
||||
if aux.Limit == 0 {
|
||||
aux.Limit = 1000
|
||||
}
|
||||
|
||||
for {
|
||||
nn, fn, err = s.SearchComposePages(ctx, types.PageFilter(aux))
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
for _, n := range nn {
|
||||
d.resourceID = append(d.resourceID, n.ID)
|
||||
|
||||
mm = append(mm, newComposePage(n))
|
||||
}
|
||||
|
||||
if fn.NextPage != nil {
|
||||
aux.PageCursor = fn.NextPage
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *composeDecoder) decodeComposeChart(ctx context.Context, s composeStore, ff []*composeChartFilter) *auxRsp {
|
||||
mm := make([]envoy.Marshaller, 0, 100)
|
||||
if ff == nil {
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
if len(d.namespaceID) > 0 {
|
||||
ffNs := make([]*composeChartFilter, 0, len(ff)+len(d.namespaceID))
|
||||
for _, nsID := range d.namespaceID {
|
||||
for _, f := range ff {
|
||||
fNs := *f
|
||||
fNs.NamespaceID = nsID
|
||||
ffNs = append(ffNs, &fNs)
|
||||
}
|
||||
}
|
||||
ff = ffNs
|
||||
}
|
||||
|
||||
var nn types.ChartSet
|
||||
var fn types.ChartFilter
|
||||
var err error
|
||||
for _, f := range ff {
|
||||
aux := *f
|
||||
|
||||
if aux.Limit == 0 {
|
||||
aux.Limit = 1000
|
||||
}
|
||||
|
||||
for {
|
||||
nn, fn, err = s.SearchComposeCharts(ctx, types.ChartFilter(aux))
|
||||
if err != nil {
|
||||
return &auxRsp{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
for _, n := range nn {
|
||||
d.resourceID = append(d.resourceID, n.ID)
|
||||
|
||||
mm = append(mm, newComposeChart(n))
|
||||
}
|
||||
|
||||
if f.Limit > 0 {
|
||||
break
|
||||
} else if fn.NextPage != nil {
|
||||
aux.PageCursor = fn.NextPage
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &auxRsp{
|
||||
mm: mm,
|
||||
}
|
||||
}
|
||||
|
||||
func (df *DecodeFilter) composeFromResource(rr ...string) *DecodeFilter {
|
||||
for _, r := range rr {
|
||||
if !strings.HasPrefix(r, "compose") {
|
||||
continue
|
||||
}
|
||||
|
||||
id := ""
|
||||
if strings.Count(r, ":") == 2 && !strings.HasSuffix(r, "*") {
|
||||
// There is an identifier
|
||||
aux := strings.Split(r, ":")
|
||||
|
||||
id = aux[len(aux)-1]
|
||||
r = strings.Join(aux[:len(aux)-1], ":")
|
||||
}
|
||||
|
||||
switch strings.ToLower(r) {
|
||||
|
||||
case "compose:namespace":
|
||||
df = df.ComposeNamespace(&types.NamespaceFilter{
|
||||
Query: id,
|
||||
})
|
||||
case "compose:module":
|
||||
df = df.ComposeModule(&types.ModuleFilter{
|
||||
Query: id,
|
||||
})
|
||||
case "compose:record":
|
||||
df = df.ComposeRecord(&types.RecordFilter{
|
||||
Query: id,
|
||||
})
|
||||
case "compose:page":
|
||||
df = df.ComposePage(&types.PageFilter{
|
||||
Query: id,
|
||||
})
|
||||
case "compose:chart":
|
||||
df = df.ComposeChart(&types.ChartFilter{
|
||||
Query: id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return df
|
||||
}
|
||||
|
||||
func (df *DecodeFilter) composeFromRef(rr ...*resource.Ref) *DecodeFilter {
|
||||
for _, r := range rr {
|
||||
if strings.Index(r.ResourceType, "compose") < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
switch r.ResourceType {
|
||||
case types.NamespaceResourceType:
|
||||
for _, i := range r.Identifiers.StringSlice() {
|
||||
df = df.ComposeNamespace(&types.NamespaceFilter{
|
||||
Query: i,
|
||||
})
|
||||
}
|
||||
case types.ModuleResourceType:
|
||||
for _, i := range r.Identifiers.StringSlice() {
|
||||
df = df.ComposeModule(&types.ModuleFilter{
|
||||
Query: i,
|
||||
})
|
||||
}
|
||||
case types.PageResourceType:
|
||||
for _, i := range r.Identifiers.StringSlice() {
|
||||
df = df.ComposePage(&types.PageFilter{
|
||||
Query: i,
|
||||
})
|
||||
}
|
||||
case types.ChartResourceType:
|
||||
for _, i := range r.Identifiers.StringSlice() {
|
||||
df = df.ComposeChart(&types.ChartFilter{
|
||||
Query: i,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return df
|
||||
}
|
||||
|
||||
// ComposeNamespace adds a new compose NamespaceFilter
|
||||
func (df *DecodeFilter) ComposeNamespace(f *types.NamespaceFilter) *DecodeFilter {
|
||||
if df.composeNamespace == nil {
|
||||
df.composeNamespace = make([]*composeNamespaceFilter, 0, 1)
|
||||
}
|
||||
df.composeNamespace = append(df.composeNamespace, (*composeNamespaceFilter)(f))
|
||||
return df
|
||||
}
|
||||
|
||||
// ComposeModule adds a new compose ModuleFilter
|
||||
func (df *DecodeFilter) ComposeModule(f *types.ModuleFilter) *DecodeFilter {
|
||||
if df.composeModule == nil {
|
||||
df.composeModule = make([]*composeModuleFilter, 0, 1)
|
||||
}
|
||||
df.composeModule = append(df.composeModule, (*composeModuleFilter)(f))
|
||||
return df
|
||||
}
|
||||
|
||||
// ComposeRecord adds a new compose RecordFilter
|
||||
func (df *DecodeFilter) ComposeRecord(f *types.RecordFilter) *DecodeFilter {
|
||||
if df.composeRecord == nil {
|
||||
df.composeRecord = make([]*composeRecordFilter, 0, 1)
|
||||
}
|
||||
df.composeRecord = append(df.composeRecord, (*composeRecordFilter)(f))
|
||||
return df
|
||||
}
|
||||
|
||||
// ComposePage adds a new compose PageFilter
|
||||
func (df *DecodeFilter) ComposePage(f *types.PageFilter) *DecodeFilter {
|
||||
if df.composePage == nil {
|
||||
df.composePage = make([]*composePageFilter, 0, 1)
|
||||
}
|
||||
df.composePage = append(df.composePage, (*composePageFilter)(f))
|
||||
return df
|
||||
}
|
||||
|
||||
// ComposeChart adds a new compose ChartFilter
|
||||
func (df *DecodeFilter) ComposeChart(f *types.ChartFilter) *DecodeFilter {
|
||||
if df.composeChart == nil {
|
||||
df.composeChart = make([]*composeChartFilter, 0, 1)
|
||||
}
|
||||
df.composeChart = append(df.composeChart, (*composeChartFilter)(f))
|
||||
return df
|
||||
}
|
||||
118
server/pkg/envoy/store/compose_chart.go
Normal file
118
server/pkg/envoy/store/compose_chart.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
)
|
||||
|
||||
type (
|
||||
composeChart struct {
|
||||
cfg *EncoderConfig
|
||||
|
||||
res *resource.ComposeChart
|
||||
chr *types.Chart
|
||||
|
||||
relNS *types.Namespace
|
||||
relMods types.ModuleSet
|
||||
}
|
||||
)
|
||||
|
||||
// mergeComposeChart merges b into a, prioritising a
|
||||
func mergeComposeChart(a, b *types.Chart) *types.Chart {
|
||||
c := *a
|
||||
|
||||
if c.Handle == "" {
|
||||
c.Handle = b.Handle
|
||||
}
|
||||
if c.Name == "" {
|
||||
c.Name = b.Name
|
||||
}
|
||||
c.NamespaceID = b.NamespaceID
|
||||
if c.CreatedAt.IsZero() {
|
||||
c.CreatedAt = b.CreatedAt
|
||||
}
|
||||
if c.UpdatedAt == nil {
|
||||
c.UpdatedAt = b.UpdatedAt
|
||||
}
|
||||
if c.DeletedAt == nil {
|
||||
c.DeletedAt = b.DeletedAt
|
||||
}
|
||||
|
||||
if len(c.Config.Reports) <= 0 {
|
||||
c.Config.Reports = b.Config.Reports
|
||||
}
|
||||
if c.Config.ColorScheme == "" {
|
||||
c.Config.ColorScheme = b.Config.ColorScheme
|
||||
}
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
// findComposeChart looks for the chart in the resources & the store
|
||||
//
|
||||
// Provided resources are prioritized.
|
||||
func findComposeChart(ctx context.Context, s store.Storer, nsID uint64, rr resource.InterfaceSet, ii resource.Identifiers) (ch *types.Chart, err error) {
|
||||
ch = resource.FindComposeChart(rr, ii)
|
||||
if ch != nil {
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
if nsID <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Go in the store
|
||||
return findComposeChartStore(ctx, s, nsID, makeGenericFilter(ii))
|
||||
}
|
||||
|
||||
// findComposeChartStore looks for the chart in the store
|
||||
func findComposeChartStore(ctx context.Context, s store.Storer, nsID uint64, gf genericFilter) (ch *types.Chart, err error) {
|
||||
if nsID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if gf.id > 0 {
|
||||
ch, err = store.LookupComposeChartByID(ctx, s, gf.id)
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ch != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, i := range gf.identifiers {
|
||||
ch, err = store.LookupComposeChartByNamespaceIDHandle(ctx, s, nsID, i)
|
||||
if err == store.ErrNotFound {
|
||||
var cc types.ChartSet
|
||||
cc, _, err = store.SearchComposeCharts(ctx, s, types.ChartFilter{
|
||||
NamespaceID: nsID,
|
||||
Name: i,
|
||||
Paging: filter.Paging{
|
||||
Limit: 2,
|
||||
},
|
||||
})
|
||||
if len(cc) > 1 {
|
||||
return nil, resourceErrIdentifierNotUnique(i)
|
||||
}
|
||||
if len(cc) == 1 {
|
||||
ch = cc[0]
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ch != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
140
server/pkg/envoy/store/compose_chart_marshal.go
Normal file
140
server/pkg/envoy/store/compose_chart_marshal.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
)
|
||||
|
||||
func newComposeChartFromResource(res *resource.ComposeChart, cfg *EncoderConfig) resourceState {
|
||||
return &composeChart{
|
||||
cfg: mergeConfig(cfg, res.Config()),
|
||||
|
||||
res: res,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *composeChart) Prepare(ctx context.Context, pl *payload) (err error) {
|
||||
// Reset old identifiers
|
||||
n.res.Res.ID = 0
|
||||
n.res.Res.NamespaceID = 0
|
||||
|
||||
// Get related namespace
|
||||
n.relNS, err = findComposeNamespace(ctx, pl.s, pl.state.ParentResources, n.res.RefNs.Identifiers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n.relNS == nil {
|
||||
return resource.ComposeNamespaceErrUnresolved(n.res.RefNs.Identifiers)
|
||||
}
|
||||
|
||||
// Get related modules
|
||||
n.relMods = make(types.ModuleSet, len(n.res.RefMods))
|
||||
for i, rMod := range n.res.RefMods {
|
||||
n.relMods[i], err = findComposeModule(ctx, pl.s, n.relNS.ID, pl.state.ParentResources, rMod.Identifiers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n.relMods[i] == nil {
|
||||
return resource.ComposeModuleErrUnresolved(rMod.Identifiers)
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get the original chart
|
||||
n.chr, err = findComposeChartStore(ctx, pl.s, n.relNS.ID, makeGenericFilter(n.res.Identifiers()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if n.chr != nil {
|
||||
n.res.Res.ID = n.chr.ID
|
||||
n.res.Res.NamespaceID = n.chr.NamespaceID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *composeChart) Encode(ctx context.Context, pl *payload) (err error) {
|
||||
res := n.res.Res
|
||||
exists := n.chr != nil && n.chr.ID > 0
|
||||
|
||||
// Determine the ID
|
||||
if res.ID <= 0 && exists {
|
||||
res.ID = n.chr.ID
|
||||
}
|
||||
if res.ID <= 0 {
|
||||
res.ID = NextID()
|
||||
}
|
||||
|
||||
ts := n.res.Timestamps()
|
||||
if ts != nil {
|
||||
if ts.CreatedAt != nil {
|
||||
res.CreatedAt = *ts.CreatedAt.T
|
||||
} else {
|
||||
res.CreatedAt = *now()
|
||||
}
|
||||
if ts.UpdatedAt != nil {
|
||||
res.UpdatedAt = ts.UpdatedAt.T
|
||||
}
|
||||
if ts.DeletedAt != nil {
|
||||
res.DeletedAt = ts.DeletedAt.T
|
||||
}
|
||||
}
|
||||
|
||||
// Namespace
|
||||
res.NamespaceID = n.relNS.ID
|
||||
if res.NamespaceID <= 0 {
|
||||
ns := resource.FindComposeNamespace(pl.state.ParentResources, n.res.RefNs.Identifiers)
|
||||
res.NamespaceID = ns.ID
|
||||
}
|
||||
if res.NamespaceID <= 0 {
|
||||
return resource.ComposeNamespaceErrUnresolved(n.res.RefNs.Identifiers)
|
||||
}
|
||||
|
||||
// Report modules
|
||||
for i, r := range res.Config.Reports {
|
||||
relMod := n.relMods[i]
|
||||
if relMod == nil {
|
||||
relMod = resource.FindComposeModule(pl.state.ParentResources, n.res.RefMods[i].Identifiers)
|
||||
}
|
||||
if relMod == nil || relMod.ID <= 0 {
|
||||
return resource.ComposeModuleErrUnresolved(n.res.RefMods[i].Identifiers)
|
||||
}
|
||||
|
||||
r.ModuleID = relMod.ID
|
||||
}
|
||||
|
||||
// Evaluate the resource skip expression
|
||||
// @todo expand available parameters; similar implementation to compose/types/record@Dict
|
||||
if skip, err := basicSkipEval(ctx, n.cfg, !exists); err != nil {
|
||||
return err
|
||||
} else if skip {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create fresh chart
|
||||
if !exists {
|
||||
return store.CreateComposeChart(ctx, pl.s, res)
|
||||
}
|
||||
|
||||
// Update existing chart
|
||||
switch n.cfg.OnExisting {
|
||||
case resource.Skip:
|
||||
return nil
|
||||
|
||||
case resource.MergeLeft:
|
||||
res = mergeComposeChart(n.chr, res)
|
||||
|
||||
case resource.MergeRight:
|
||||
res = mergeComposeChart(res, n.chr)
|
||||
}
|
||||
|
||||
err = store.UpdateComposeChart(ctx, pl.s, res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.res.Res = res
|
||||
return nil
|
||||
}
|
||||
54
server/pkg/envoy/store/compose_chart_test.go
Normal file
54
server/pkg/envoy/store/compose_chart_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestComposeChart_Merger(t *testing.T) {
|
||||
req := require.New(t)
|
||||
|
||||
now := time.Time{}
|
||||
nowP := &time.Time{}
|
||||
|
||||
empty := &types.Chart{}
|
||||
full := &types.Chart{
|
||||
Handle: "handle",
|
||||
Name: "name",
|
||||
Config: types.ChartConfig{
|
||||
Reports: []*types.ChartConfigReport{&types.ChartConfigReport{}},
|
||||
ColorScheme: "colorScheme",
|
||||
},
|
||||
NamespaceID: 1,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: nowP,
|
||||
DeletedAt: nowP,
|
||||
}
|
||||
|
||||
t.Run("merge on empty", func(t *testing.T) {
|
||||
c := mergeComposeChart(empty, full)
|
||||
req.Equal("name", c.Name)
|
||||
req.Equal("handle", c.Handle)
|
||||
req.Len(c.Config.Reports, 1)
|
||||
req.Equal("colorScheme", c.Config.ColorScheme)
|
||||
req.Equal(uint64(1), c.NamespaceID)
|
||||
req.Equal(now, c.CreatedAt)
|
||||
req.Equal(nowP, c.UpdatedAt)
|
||||
req.Equal(nowP, c.DeletedAt)
|
||||
})
|
||||
|
||||
t.Run("merge with empty", func(t *testing.T) {
|
||||
c := mergeComposeChart(full, empty)
|
||||
req.Equal("name", c.Name)
|
||||
req.Equal("handle", c.Handle)
|
||||
req.Len(c.Config.Reports, 1)
|
||||
req.Equal("colorScheme", c.Config.ColorScheme)
|
||||
req.Equal(uint64(0), c.NamespaceID)
|
||||
req.Equal(now, c.CreatedAt)
|
||||
req.Equal(nowP, c.UpdatedAt)
|
||||
req.Equal(nowP, c.DeletedAt)
|
||||
})
|
||||
}
|
||||
25
server/pkg/envoy/store/compose_chart_unmarshal.go
Normal file
25
server/pkg/envoy/store/compose_chart_unmarshal.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
)
|
||||
|
||||
func newComposeChart(chr *types.Chart) *composeChart {
|
||||
return &composeChart{
|
||||
chr: chr,
|
||||
}
|
||||
}
|
||||
|
||||
func (chr *composeChart) MarshalEnvoy() ([]resource.Interface, error) {
|
||||
refNs := resource.MakeNamespaceRef(chr.chr.NamespaceID, "", "")
|
||||
refMods := make(resource.RefSet, 0, 2)
|
||||
for _, r := range chr.chr.Config.Reports {
|
||||
refMods = append(refMods, resource.MakeModuleRef(r.ModuleID, "", ""))
|
||||
}
|
||||
|
||||
return envoy.CollectNodes(
|
||||
resource.NewComposeChart(chr.chr, refNs, refMods),
|
||||
)
|
||||
}
|
||||
210
server/pkg/envoy/store/compose_module.go
Normal file
210
server/pkg/envoy/store/compose_module.go
Normal file
@@ -0,0 +1,210 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
)
|
||||
|
||||
type (
|
||||
composeModule struct {
|
||||
cfg *EncoderConfig
|
||||
|
||||
res *resource.ComposeModule
|
||||
mod *types.Module
|
||||
|
||||
relNS *types.Namespace
|
||||
recFields map[string]uint64
|
||||
userFields map[string]uint64
|
||||
}
|
||||
)
|
||||
|
||||
// mergeComposeModuleFields merges b into a, prioritising a
|
||||
func mergeComposeModuleFields(a, b types.ModuleFieldSet) types.ModuleFieldSet {
|
||||
ff := a.Clone()
|
||||
missing := make(types.ModuleFieldSet, 0, len(b))
|
||||
|
||||
for _, fb := range b {
|
||||
for _, fa := range ff {
|
||||
if fb.ID != fa.ID && fb.Name != fa.Name {
|
||||
continue
|
||||
}
|
||||
|
||||
fa.ModuleID = fb.ModuleID
|
||||
fa.Place = fb.Place
|
||||
if fa.Kind == "" {
|
||||
fa.Kind = fb.Kind
|
||||
}
|
||||
if fa.Name == "" {
|
||||
fa.Name = fb.Name
|
||||
}
|
||||
if fa.Label == "" {
|
||||
fa.Label = fb.Label
|
||||
}
|
||||
if fa.Options == nil {
|
||||
fa.Options = fb.Options
|
||||
}
|
||||
if fa.DefaultValue == nil {
|
||||
fa.DefaultValue = fb.DefaultValue
|
||||
}
|
||||
if fa.Expressions.ValueExpr == "" {
|
||||
fa.Expressions.ValueExpr = fb.Expressions.ValueExpr
|
||||
}
|
||||
if len(fa.Expressions.Sanitizers) == 0 {
|
||||
fa.Expressions.Sanitizers = fb.Expressions.Sanitizers
|
||||
}
|
||||
if len(fa.Expressions.Validators) == 0 {
|
||||
fa.Expressions.Validators = fb.Expressions.Validators
|
||||
}
|
||||
if len(fa.Expressions.Formatters) == 0 {
|
||||
fa.Expressions.Formatters = fb.Expressions.Formatters
|
||||
}
|
||||
|
||||
if fa.CreatedAt.IsZero() {
|
||||
fa.CreatedAt = fb.CreatedAt
|
||||
}
|
||||
if fa.UpdatedAt == nil {
|
||||
fa.UpdatedAt = fb.UpdatedAt
|
||||
}
|
||||
if fa.DeletedAt == nil {
|
||||
fa.DeletedAt = fb.DeletedAt
|
||||
}
|
||||
|
||||
goto out
|
||||
}
|
||||
missing = append(missing, fb)
|
||||
out:
|
||||
}
|
||||
|
||||
ff = append(ff, missing...)
|
||||
|
||||
return ff
|
||||
}
|
||||
|
||||
// mergeComposeModule merges b into a, prioritising a
|
||||
func mergeComposeModule(a, b *types.Module) *types.Module {
|
||||
c := a.Clone()
|
||||
|
||||
if c.Handle == "" {
|
||||
c.Handle = b.Handle
|
||||
}
|
||||
if c.Name == "" {
|
||||
c.Name = b.Name
|
||||
}
|
||||
c.NamespaceID = b.NamespaceID
|
||||
|
||||
if c.CreatedAt.IsZero() {
|
||||
c.CreatedAt = b.CreatedAt
|
||||
}
|
||||
if c.UpdatedAt == nil {
|
||||
c.UpdatedAt = b.UpdatedAt
|
||||
}
|
||||
if c.DeletedAt == nil {
|
||||
c.DeletedAt = b.DeletedAt
|
||||
}
|
||||
|
||||
// I'll just compare the entire struct for now
|
||||
if c.Meta == nil {
|
||||
c.Meta = b.Meta
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// findComposeModule looks for the chart in the resources & the store
|
||||
//
|
||||
// Provided resources are prioritized.
|
||||
func findComposeModule(ctx context.Context, s store.Storer, nsID uint64, rr resource.InterfaceSet, ii resource.Identifiers) (mod *types.Module, err error) {
|
||||
mod = resource.FindComposeModule(rr, ii)
|
||||
if mod != nil {
|
||||
return mod, nil
|
||||
}
|
||||
|
||||
if nsID <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Go in the store
|
||||
return findComposeModuleStore(ctx, s, nsID, makeGenericFilter(ii))
|
||||
}
|
||||
|
||||
// findComposeModuleStore looks for the module in the store
|
||||
func findComposeModuleStore(ctx context.Context, s store.Storer, nsID uint64, gf genericFilter) (mod *types.Module, err error) {
|
||||
if nsID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if gf.id > 0 {
|
||||
mod, err = store.LookupComposeModuleByID(ctx, s, gf.id)
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if mod != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, i := range gf.identifiers {
|
||||
mod, err = store.LookupComposeModuleByNamespaceIDHandle(ctx, s, nsID, i)
|
||||
if err == store.ErrNotFound {
|
||||
var mm types.ModuleSet
|
||||
mm, _, err = store.SearchComposeModules(ctx, s, types.ModuleFilter{
|
||||
NamespaceID: nsID,
|
||||
Name: i,
|
||||
Paging: filter.Paging{
|
||||
Limit: 2,
|
||||
},
|
||||
})
|
||||
if len(mm) > 1 {
|
||||
return nil, resourceErrIdentifierNotUnique(i)
|
||||
}
|
||||
if len(mm) == 1 {
|
||||
mod = mm[0]
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if mod != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// findComposeModuleFieldsStore looks for the module fields in the store
|
||||
func findComposeModuleFieldsStore(ctx context.Context, s store.Storer, mod *types.Module) (types.ModuleFieldSet, error) {
|
||||
if mod.ID <= 0 {
|
||||
return mod.Fields, nil
|
||||
}
|
||||
|
||||
ff, _, err := store.SearchComposeModuleFields(ctx, s, types.ModuleFieldFilter{
|
||||
ModuleID: []uint64{mod.ID},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_ = ff.Walk(func(f *types.ModuleField) error {
|
||||
f.NamespaceID = mod.NamespaceID
|
||||
return nil
|
||||
})
|
||||
|
||||
return ff, nil
|
||||
}
|
||||
|
||||
func composeModuleErrUnresolvedRecordField(ii resource.Identifiers) error {
|
||||
return fmt.Errorf("record module field unresolved %v", ii.StringSlice())
|
||||
}
|
||||
|
||||
func composeModuleErrUnresolvedUserField(ii resource.Identifiers) error {
|
||||
return fmt.Errorf("user module field unresolved %v", ii.StringSlice())
|
||||
}
|
||||
356
server/pkg/envoy/store/compose_module_marshal.go
Normal file
356
server/pkg/envoy/store/compose_module_marshal.go
Normal file
@@ -0,0 +1,356 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/service"
|
||||
"github.com/cortezaproject/corteza/server/pkg/dal"
|
||||
"github.com/cortezaproject/corteza/server/pkg/revisions"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
systemTypes "github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
func NewComposeModuleFromResource(res *resource.ComposeModule, cfg *EncoderConfig) resourceState {
|
||||
return &composeModule{
|
||||
cfg: mergeConfig(cfg, res.Config()),
|
||||
|
||||
res: res,
|
||||
|
||||
recFields: make(map[string]uint64),
|
||||
userFields: make(map[string]uint64),
|
||||
}
|
||||
}
|
||||
|
||||
func (n *composeModule) Prepare(ctx context.Context, pl *payload) (err error) {
|
||||
// Reset old identifiers
|
||||
n.res.Res.ID = 0
|
||||
n.res.Res.NamespaceID = 0
|
||||
for _, rf := range n.res.ResFields {
|
||||
rf.Res.ID = 0
|
||||
rf.Res.ModuleID = 0
|
||||
rf.Res.NamespaceID = 0
|
||||
}
|
||||
|
||||
// Get related namespace
|
||||
n.relNS, err = findComposeNamespace(ctx, pl.s, pl.state.ParentResources, n.res.RefNs.Identifiers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n.relNS == nil {
|
||||
return resource.ComposeNamespaceErrUnresolved(n.res.RefNs.Identifiers)
|
||||
}
|
||||
|
||||
n.res.Res.NamespaceID = n.relNS.ID
|
||||
|
||||
// Get related record field modules
|
||||
for _, refMod := range n.res.RefMods {
|
||||
var mod *types.Module
|
||||
if n.relNS.ID > 0 {
|
||||
mod, err = findComposeModuleStore(ctx, pl.s, n.relNS.ID, makeGenericFilter(refMod.Identifiers))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if mod == nil {
|
||||
mod = resource.FindComposeModule(pl.state.ParentResources, refMod.Identifiers)
|
||||
}
|
||||
if mod == nil {
|
||||
return composeModuleErrUnresolvedRecordField(refMod.Identifiers)
|
||||
}
|
||||
|
||||
for _, i := range refMod.Identifiers {
|
||||
n.recFields[i] = mod.ID
|
||||
}
|
||||
}
|
||||
|
||||
// Get related user field roles
|
||||
for _, refRole := range n.res.RefRoles {
|
||||
var rl *systemTypes.Role
|
||||
rl, err = findRoleStore(ctx, pl.s, makeGenericFilter(refRole.Identifiers))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rl == nil {
|
||||
rl = resource.FindRole(pl.state.ParentResources, refRole.Identifiers)
|
||||
}
|
||||
if rl == nil {
|
||||
return composeModuleErrUnresolvedUserField(refRole.Identifiers)
|
||||
}
|
||||
|
||||
for _, i := range refRole.Identifiers {
|
||||
n.userFields[i] = rl.ID
|
||||
}
|
||||
}
|
||||
|
||||
// Can't do anything else, since the NS doesn't yet exist
|
||||
if n.relNS.ID <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try to get the original module
|
||||
n.mod, err = findComposeModuleStore(ctx, pl.s, n.relNS.ID, makeGenericFilter(n.res.Identifiers()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Nothing else to do
|
||||
if n.mod == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get the original module fields
|
||||
// These are used later for some merging logic
|
||||
n.mod.Fields, err = findComposeModuleFieldsStore(ctx, pl.s, n.mod)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if n.mod != nil {
|
||||
n.res.Res.ID = n.mod.ID
|
||||
n.res.Res.NamespaceID = n.mod.NamespaceID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *composeModule) Encode(ctx context.Context, pl *payload) (err error) {
|
||||
res := n.res.Res
|
||||
exists := n.mod != nil && n.mod.ID > 0
|
||||
|
||||
// Determine the ID
|
||||
if res.ID <= 0 && exists {
|
||||
res.ID = n.mod.ID
|
||||
}
|
||||
if res.ID <= 0 {
|
||||
res.ID = NextID()
|
||||
}
|
||||
|
||||
ns := n.relNS
|
||||
|
||||
res.NamespaceID = ns.ID
|
||||
if res.NamespaceID <= 0 {
|
||||
ns = resource.FindComposeNamespace(pl.state.ParentResources, n.res.RefNs.Identifiers)
|
||||
res.NamespaceID = ns.ID
|
||||
}
|
||||
if res.NamespaceID <= 0 {
|
||||
return resource.ComposeNamespaceErrUnresolved(n.res.RefNs.Identifiers)
|
||||
}
|
||||
|
||||
if pl.state.Conflicting {
|
||||
return nil
|
||||
}
|
||||
|
||||
ts := n.res.Timestamps()
|
||||
if ts != nil {
|
||||
if ts.CreatedAt != nil {
|
||||
res.CreatedAt = *ts.CreatedAt.T
|
||||
} else {
|
||||
res.CreatedAt = *now()
|
||||
}
|
||||
if ts.UpdatedAt != nil {
|
||||
res.UpdatedAt = ts.UpdatedAt.T
|
||||
}
|
||||
if ts.DeletedAt != nil {
|
||||
res.DeletedAt = ts.DeletedAt.T
|
||||
}
|
||||
}
|
||||
|
||||
// Fields
|
||||
var originalFields types.ModuleFieldSet
|
||||
if n.mod != nil && n.mod.Fields != nil {
|
||||
originalFields = n.mod.Fields
|
||||
} else {
|
||||
originalFields = make(types.ModuleFieldSet, 0)
|
||||
}
|
||||
|
||||
// Get max validatorID for later use
|
||||
vvID := make([]uint64, len(res.Fields))
|
||||
for i, f := range res.Fields {
|
||||
for _, v := range f.Expressions.Validators {
|
||||
if vvID[i] < v.ValidatorID {
|
||||
vvID[i] = v.ValidatorID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i, f := range res.Fields {
|
||||
of := originalFields.FindByName(f.Name)
|
||||
if of != nil {
|
||||
f.ID = of.ID
|
||||
} else {
|
||||
f.ID = NextID()
|
||||
}
|
||||
f.ModuleID = res.ID
|
||||
f.Place = i
|
||||
f.DeletedAt = nil
|
||||
f.CreatedAt = *now()
|
||||
|
||||
// Assure validatorIDs
|
||||
for j, v := range f.Expressions.Validators {
|
||||
if v.ValidatorID == 0 {
|
||||
vvID[i] += 1
|
||||
v.ValidatorID = vvID[i]
|
||||
|
||||
f.Expressions.Validators[j] = v
|
||||
}
|
||||
}
|
||||
|
||||
if f.Options != nil && f.Kind == "Record" {
|
||||
refMod := f.Options.String("module")
|
||||
if refMod == "" {
|
||||
refMod = f.Options.String("moduleID")
|
||||
}
|
||||
modID := n.recFields[refMod]
|
||||
if modID <= 0 {
|
||||
ii := resource.MakeIdentifiers(refMod)
|
||||
mod := resource.FindComposeModule(pl.state.ParentResources, ii)
|
||||
if mod == nil || mod.ID <= 0 {
|
||||
return composeModuleErrUnresolvedRecordField(ii)
|
||||
}
|
||||
modID = mod.ID
|
||||
}
|
||||
|
||||
f.Options["moduleID"] = strconv.FormatUint(modID, 10)
|
||||
delete(f.Options, "module")
|
||||
}
|
||||
|
||||
if f.Options != nil && f.Kind == "User" {
|
||||
roles := resource.ComposeModuleFieldExtractUserFieldRoles(f.Options["roles"])
|
||||
if len(roles) == 0 {
|
||||
roles = resource.ComposeModuleFieldExtractUserFieldRoles(f.Options["role"])
|
||||
}
|
||||
if len(roles) == 0 {
|
||||
roles = resource.ComposeModuleFieldExtractUserFieldRoles(f.Options["roleID"])
|
||||
}
|
||||
|
||||
var out []string
|
||||
for _, r := range roles {
|
||||
roleID := n.userFields[r]
|
||||
if roleID <= 0 {
|
||||
ii := resource.MakeIdentifiers(r)
|
||||
role := resource.FindRole(pl.state.ParentResources, ii)
|
||||
if role == nil || role.ID == 0 {
|
||||
return composeModuleErrUnresolvedUserField(ii)
|
||||
}
|
||||
roleID = role.ID
|
||||
}
|
||||
|
||||
out = append(out, strconv.FormatUint(roleID, 10))
|
||||
}
|
||||
|
||||
f.Options["roles"] = out
|
||||
delete(f.Options, "role")
|
||||
delete(f.Options, "roleID")
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate the resource skip expression
|
||||
// @todo expand available parameters; similar implementation to compose/types/record@Dict
|
||||
if skip, err := basicSkipEval(ctx, n.cfg, !exists); err != nil {
|
||||
return err
|
||||
} else if skip {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a fresh module
|
||||
if !exists {
|
||||
err = store.CreateComposeModule(ctx, pl.s, res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = store.CreateComposeModuleField(ctx, pl.s, res.Fields...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// Update existing module
|
||||
switch n.cfg.OnExisting {
|
||||
case resource.Skip:
|
||||
return nil
|
||||
|
||||
case resource.MergeLeft:
|
||||
res = mergeComposeModule(n.mod, res)
|
||||
res.Fields = mergeComposeModuleFields(n.mod.Fields, res.Fields)
|
||||
|
||||
case resource.MergeRight:
|
||||
res = mergeComposeModule(res, n.mod)
|
||||
res.Fields = mergeComposeModuleFields(res.Fields, n.mod.Fields)
|
||||
}
|
||||
|
||||
err = store.UpdateComposeModule(ctx, pl.s, res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = store.DeleteComposeModuleField(ctx, pl.s, n.mod.Fields...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = store.CreateComposeModuleField(ctx, pl.s, res.Fields...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.res.Res = res
|
||||
}
|
||||
|
||||
var (
|
||||
model *dal.Model
|
||||
con = pl.dal.GetConnectionByID(0)
|
||||
)
|
||||
|
||||
// convert module to model and assume compose_record for default ident
|
||||
if model, err = service.ModuleToModel(ns, res, "compose_record"); err != nil {
|
||||
return
|
||||
}
|
||||
model.ConnectionID = con.ID
|
||||
|
||||
// @note code copied from the service/module.go
|
||||
|
||||
// Set base constraints
|
||||
if model.Ident == "compose_record" {
|
||||
model.Constraints = map[string][]any{
|
||||
"moduleID": {res.ID},
|
||||
"namespaceID": {res.NamespaceID},
|
||||
}
|
||||
}
|
||||
|
||||
// replace all partition replacement pairs
|
||||
rpl := strings.NewReplacer("{{module}}", res.Handle, "{{namespace}}", ns.Slug)
|
||||
model.Ident = rpl.Replace(model.Ident)
|
||||
|
||||
// @todo validate ident with connection's ident validator
|
||||
|
||||
// Create the revisions model if enabled
|
||||
if res.Config.RecordRevisions.Enabled {
|
||||
rModel := revisions.Model()
|
||||
|
||||
// reuse the connection from the module
|
||||
rModel.ConnectionID = con.ID
|
||||
rModel.Resource = model.Resource
|
||||
rModel.ResourceID = NextID()
|
||||
|
||||
if rModel.Ident = res.Config.RecordRevisions.Ident; rModel.Ident == "" {
|
||||
rModel.Ident = "compose_record_revisions"
|
||||
}
|
||||
|
||||
rModel.Ident = rpl.Replace(rModel.Ident)
|
||||
|
||||
// @todo validate ident with connection's ident validator
|
||||
|
||||
if _, err = pl.dal.ReplaceModel(ctx, nil, rModel); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if _, err = pl.dal.ReplaceModel(ctx, nil, model); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
111
server/pkg/envoy/store/compose_module_test.go
Normal file
111
server/pkg/envoy/store/compose_module_test.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
st "github.com/jmoiron/sqlx/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestComposeModule_Merger(t *testing.T) {
|
||||
req := require.New(t)
|
||||
|
||||
now := time.Time{}
|
||||
nowP := &time.Time{}
|
||||
|
||||
empty := &types.Module{}
|
||||
full := &types.Module{
|
||||
Handle: "handle",
|
||||
Name: "name",
|
||||
Meta: st.JSONText{},
|
||||
NamespaceID: 1,
|
||||
|
||||
CreatedAt: now,
|
||||
UpdatedAt: nowP,
|
||||
DeletedAt: nowP,
|
||||
}
|
||||
|
||||
t.Run("merge on empty", func(t *testing.T) {
|
||||
c := mergeComposeModule(empty, full)
|
||||
req.Equal("name", c.Name)
|
||||
req.Equal("handle", c.Handle)
|
||||
req.NotNil(c.Meta)
|
||||
req.Equal(uint64(1), c.NamespaceID)
|
||||
req.Equal(now, c.CreatedAt)
|
||||
req.Equal(nowP, c.UpdatedAt)
|
||||
req.Equal(nowP, c.DeletedAt)
|
||||
})
|
||||
|
||||
t.Run("merge with empty", func(t *testing.T) {
|
||||
c := mergeComposeModule(full, empty)
|
||||
req.Equal("name", c.Name)
|
||||
req.Equal("handle", c.Handle)
|
||||
req.NotNil(c.Meta)
|
||||
req.Equal(uint64(0), c.NamespaceID)
|
||||
req.Equal(now, c.CreatedAt)
|
||||
req.Equal(nowP, c.UpdatedAt)
|
||||
req.Equal(nowP, c.DeletedAt)
|
||||
})
|
||||
}
|
||||
|
||||
func TestComposeModuleField_Merger(t *testing.T) {
|
||||
req := require.New(t)
|
||||
|
||||
now := time.Time{}
|
||||
nowP := &time.Time{}
|
||||
|
||||
empty := types.ModuleFieldSet{}
|
||||
full := types.ModuleFieldSet{
|
||||
&types.ModuleField{
|
||||
ModuleID: 1,
|
||||
Place: 2,
|
||||
Kind: "kind",
|
||||
Name: "name",
|
||||
Label: "label",
|
||||
Options: types.ModuleFieldOptions{},
|
||||
DefaultValue: types.RecordValueSet{},
|
||||
Expressions: types.ModuleFieldExpr{},
|
||||
CreatedAt: now,
|
||||
UpdatedAt: nowP,
|
||||
DeletedAt: nowP,
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("merge on empty", func(t *testing.T) {
|
||||
cc := mergeComposeModuleFields(empty, full)
|
||||
req.Len(cc, 1)
|
||||
c := cc[0]
|
||||
|
||||
req.Equal(uint64(1), c.ModuleID)
|
||||
req.Equal(2, c.Place)
|
||||
req.Equal("kind", c.Kind)
|
||||
req.Equal("name", c.Name)
|
||||
req.Equal("label", c.Label)
|
||||
req.NotNil(c.Options)
|
||||
req.NotNil(c.DefaultValue)
|
||||
req.NotNil(c.Expressions)
|
||||
req.Equal(now, c.CreatedAt)
|
||||
req.Equal(nowP, c.UpdatedAt)
|
||||
req.Equal(nowP, c.DeletedAt)
|
||||
})
|
||||
|
||||
t.Run("merge with empty", func(t *testing.T) {
|
||||
cc := mergeComposeModuleFields(full, empty)
|
||||
req.Len(cc, 1)
|
||||
c := cc[0]
|
||||
|
||||
req.Equal(uint64(1), c.ModuleID)
|
||||
req.Equal(2, c.Place)
|
||||
req.Equal("kind", c.Kind)
|
||||
req.Equal("name", c.Name)
|
||||
req.Equal("label", c.Label)
|
||||
req.NotNil(c.Options)
|
||||
req.NotNil(c.DefaultValue)
|
||||
req.NotNil(c.Expressions)
|
||||
req.Equal(now, c.CreatedAt)
|
||||
req.Equal(nowP, c.UpdatedAt)
|
||||
req.Equal(nowP, c.DeletedAt)
|
||||
})
|
||||
}
|
||||
28
server/pkg/envoy/store/compose_module_unmarshal.go
Normal file
28
server/pkg/envoy/store/compose_module_unmarshal.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
)
|
||||
|
||||
func newComposeModule(mod *types.Module) *composeModule {
|
||||
return &composeModule{
|
||||
mod: mod,
|
||||
}
|
||||
}
|
||||
|
||||
func (mod *composeModule) MarshalEnvoy() ([]resource.Interface, error) {
|
||||
refNs := resource.MakeNamespaceRef(mod.mod.NamespaceID, "", "")
|
||||
refMod := resource.MakeModuleRef(mod.mod.ID, mod.mod.Handle, mod.mod.Name)
|
||||
|
||||
rMod := resource.NewComposeModule(mod.mod, refNs)
|
||||
for _, f := range mod.mod.Fields {
|
||||
r := resource.NewComposeModuleField(f, refNs, refMod)
|
||||
rMod.AddField(r)
|
||||
}
|
||||
|
||||
return envoy.CollectNodes(
|
||||
rMod,
|
||||
)
|
||||
}
|
||||
105
server/pkg/envoy/store/compose_namespace.go
Normal file
105
server/pkg/envoy/store/compose_namespace.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
)
|
||||
|
||||
type (
|
||||
composeNamespace struct {
|
||||
cfg *EncoderConfig
|
||||
|
||||
res *resource.ComposeNamespace
|
||||
ns *types.Namespace
|
||||
}
|
||||
|
||||
composeNamespaceSet []*composeNamespace
|
||||
)
|
||||
|
||||
// mergeComposeNamespaces merges b into a, prioritising a
|
||||
func mergeComposeNamespaces(a, b *types.Namespace) *types.Namespace {
|
||||
c := a.Clone()
|
||||
|
||||
if c.Name == "" {
|
||||
c.Name = b.Name
|
||||
}
|
||||
if c.Slug == "" {
|
||||
c.Slug = b.Slug
|
||||
}
|
||||
|
||||
if c.CreatedAt.IsZero() {
|
||||
c.CreatedAt = b.CreatedAt
|
||||
}
|
||||
if c.UpdatedAt == nil {
|
||||
c.UpdatedAt = b.UpdatedAt
|
||||
}
|
||||
if c.DeletedAt == nil {
|
||||
c.DeletedAt = b.DeletedAt
|
||||
}
|
||||
|
||||
// I'll just compare the entire struct for now
|
||||
if c.Meta == (types.NamespaceMeta{}) {
|
||||
c.Meta = b.Meta
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// findComposeNamespace looks for the namespace in the resources & the store
|
||||
//
|
||||
// Provided resources are prioritized.
|
||||
func findComposeNamespace(ctx context.Context, s store.Storer, rr resource.InterfaceSet, ii resource.Identifiers) (ns *types.Namespace, err error) {
|
||||
ns = resource.FindComposeNamespace(rr, ii)
|
||||
if ns != nil {
|
||||
return ns, nil
|
||||
}
|
||||
|
||||
return findComposeNamespaceStore(ctx, s, makeGenericFilter(ii))
|
||||
}
|
||||
|
||||
// findComposeNamespaceStore looks for the namespace in the store
|
||||
func findComposeNamespaceStore(ctx context.Context, s store.Storer, gf genericFilter) (ns *types.Namespace, err error) {
|
||||
if gf.id > 0 {
|
||||
ns, err = store.LookupComposeNamespaceByID(ctx, s, gf.id)
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ns != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, i := range gf.identifiers {
|
||||
ns, err = store.LookupComposeNamespaceBySlug(ctx, s, i)
|
||||
if err == store.ErrNotFound {
|
||||
var nn types.NamespaceSet
|
||||
nn, _, err = store.SearchComposeNamespaces(ctx, s, types.NamespaceFilter{
|
||||
Name: i,
|
||||
Paging: filter.Paging{
|
||||
Limit: 2,
|
||||
},
|
||||
})
|
||||
if len(nn) > 1 {
|
||||
return nil, resourceErrIdentifierNotUnique(i)
|
||||
}
|
||||
if len(nn) == 1 {
|
||||
ns = nn[0]
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ns != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
93
server/pkg/envoy/store/compose_namespace_marshal.go
Normal file
93
server/pkg/envoy/store/compose_namespace_marshal.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
)
|
||||
|
||||
func newComposeNamespaceFromResource(res *resource.ComposeNamespace, cfg *EncoderConfig) resourceState {
|
||||
return &composeNamespace{
|
||||
cfg: mergeConfig(cfg, res.Config()),
|
||||
|
||||
res: res,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *composeNamespace) Prepare(ctx context.Context, pl *payload) (err error) {
|
||||
// Reset old identifiers
|
||||
n.res.Res.ID = 0
|
||||
|
||||
// Try to get the original namespace
|
||||
n.ns, err = findComposeNamespaceStore(ctx, pl.s, makeGenericFilter(n.res.Identifiers()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if n.ns != nil {
|
||||
n.res.Res.ID = n.ns.ID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *composeNamespace) Encode(ctx context.Context, pl *payload) (err error) {
|
||||
res := n.res.Res
|
||||
exists := n.ns != nil && n.ns.ID > 0
|
||||
|
||||
// Determine the ID
|
||||
if res.ID <= 0 && exists {
|
||||
res.ID = n.ns.ID
|
||||
}
|
||||
if res.ID <= 0 {
|
||||
res.ID = NextID()
|
||||
}
|
||||
|
||||
ts := n.res.Timestamps()
|
||||
if ts != nil {
|
||||
if ts.CreatedAt != nil {
|
||||
res.CreatedAt = *ts.CreatedAt.T
|
||||
} else {
|
||||
res.CreatedAt = *now()
|
||||
}
|
||||
if ts.UpdatedAt != nil {
|
||||
res.UpdatedAt = ts.UpdatedAt.T
|
||||
}
|
||||
if ts.DeletedAt != nil {
|
||||
res.DeletedAt = ts.DeletedAt.T
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate the resource skip expression
|
||||
// @todo expand available parameters; similar implementation to compose/types/record@Dict
|
||||
if skip, err := basicSkipEval(ctx, n.cfg, !exists); err != nil {
|
||||
return err
|
||||
} else if skip {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a fresh namespace
|
||||
if !exists {
|
||||
return store.CreateComposeNamespace(ctx, pl.s, res)
|
||||
}
|
||||
|
||||
// Update existing namespace
|
||||
switch n.cfg.OnExisting {
|
||||
case resource.Skip:
|
||||
return nil
|
||||
|
||||
case resource.MergeLeft:
|
||||
res = mergeComposeNamespaces(n.ns, res)
|
||||
|
||||
case resource.MergeRight:
|
||||
res = mergeComposeNamespaces(res, n.ns)
|
||||
}
|
||||
|
||||
err = store.UpdateComposeNamespace(ctx, pl.s, res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.res.Res = res
|
||||
return nil
|
||||
}
|
||||
53
server/pkg/envoy/store/compose_namespace_test.go
Normal file
53
server/pkg/envoy/store/compose_namespace_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestComposeNamespace_Merger(t *testing.T) {
|
||||
req := require.New(t)
|
||||
|
||||
now := time.Time{}
|
||||
nowP := &time.Time{}
|
||||
|
||||
empty := &types.Namespace{}
|
||||
full := &types.Namespace{
|
||||
Name: "name",
|
||||
Slug: "slug",
|
||||
Enabled: false,
|
||||
Meta: types.NamespaceMeta{
|
||||
Description: "dsc",
|
||||
Subtitle: "sub",
|
||||
},
|
||||
|
||||
CreatedAt: now,
|
||||
UpdatedAt: nowP,
|
||||
DeletedAt: nowP,
|
||||
}
|
||||
|
||||
t.Run("merge on empty", func(t *testing.T) {
|
||||
c := mergeComposeNamespaces(empty, full)
|
||||
req.Equal("name", c.Name)
|
||||
req.Equal("slug", c.Slug)
|
||||
req.Equal("dsc", c.Meta.Description)
|
||||
req.Equal("sub", c.Meta.Subtitle)
|
||||
req.Equal(now, c.CreatedAt)
|
||||
req.Equal(nowP, c.UpdatedAt)
|
||||
req.Equal(nowP, c.DeletedAt)
|
||||
})
|
||||
|
||||
t.Run("merge with empty", func(t *testing.T) {
|
||||
c := mergeComposeNamespaces(full, empty)
|
||||
req.Equal("name", c.Name)
|
||||
req.Equal("slug", c.Slug)
|
||||
req.Equal("dsc", c.Meta.Description)
|
||||
req.Equal("sub", c.Meta.Subtitle)
|
||||
req.Equal(now, c.CreatedAt)
|
||||
req.Equal(nowP, c.UpdatedAt)
|
||||
req.Equal(nowP, c.DeletedAt)
|
||||
})
|
||||
}
|
||||
19
server/pkg/envoy/store/compose_namespace_unmarshal.go
Normal file
19
server/pkg/envoy/store/compose_namespace_unmarshal.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
)
|
||||
|
||||
func newComposeNamespace(ns *types.Namespace) *composeNamespace {
|
||||
return &composeNamespace{
|
||||
ns: ns,
|
||||
}
|
||||
}
|
||||
|
||||
func (ns *composeNamespace) MarshalEnvoy() ([]resource.Interface, error) {
|
||||
return envoy.CollectNodes(
|
||||
resource.NewComposeNamespace(ns.ns),
|
||||
)
|
||||
}
|
||||
130
server/pkg/envoy/store/compose_page.go
Normal file
130
server/pkg/envoy/store/compose_page.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
atypes "github.com/cortezaproject/corteza/server/automation/types"
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
)
|
||||
|
||||
type (
|
||||
composePage struct {
|
||||
cfg *EncoderConfig
|
||||
|
||||
res *resource.ComposePage
|
||||
pg *types.Page
|
||||
|
||||
relNS *types.Namespace
|
||||
relMod *types.Module
|
||||
relParent *types.Page
|
||||
|
||||
relWfs map[string]*atypes.Workflow
|
||||
relMods map[string]*types.Module
|
||||
relCharts map[string]*types.Chart
|
||||
}
|
||||
)
|
||||
|
||||
// mergeComposePage merges b into a, prioritising a
|
||||
func mergeComposePage(a, b *types.Page) *types.Page {
|
||||
c := a.Clone()
|
||||
|
||||
c.SelfID = b.SelfID
|
||||
c.NamespaceID = b.NamespaceID
|
||||
c.ModuleID = b.ModuleID
|
||||
c.Weight = b.Weight
|
||||
if c.Handle == "" {
|
||||
c.Handle = b.Handle
|
||||
}
|
||||
if c.Title == "" {
|
||||
c.Title = b.Title
|
||||
}
|
||||
if c.Description == "" {
|
||||
c.Description = b.Description
|
||||
}
|
||||
if len(c.Blocks) <= 0 {
|
||||
c.Blocks = b.Blocks
|
||||
}
|
||||
if len(c.Children) <= 0 {
|
||||
c.Children = b.Children
|
||||
}
|
||||
|
||||
if c.CreatedAt.IsZero() {
|
||||
c.CreatedAt = b.CreatedAt
|
||||
}
|
||||
if c.UpdatedAt == nil {
|
||||
c.UpdatedAt = b.UpdatedAt
|
||||
}
|
||||
if c.DeletedAt == nil {
|
||||
c.DeletedAt = b.DeletedAt
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// findComposePage looks for the page in the resources & the store
|
||||
//
|
||||
// Provided resources are prioritized.
|
||||
func findComposePage(ctx context.Context, s store.Storer, nsID uint64, rr resource.InterfaceSet, ii resource.Identifiers) (pg *types.Page, err error) {
|
||||
pg = resource.FindComposePage(rr, ii)
|
||||
if pg != nil {
|
||||
return pg, nil
|
||||
}
|
||||
|
||||
if nsID <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Go in the store
|
||||
return findComposePageStore(ctx, s, nsID, makeGenericFilter(ii))
|
||||
}
|
||||
|
||||
// findComposePageStore looks for the page in the store
|
||||
func findComposePageStore(ctx context.Context, s store.Storer, nsID uint64, gf genericFilter) (pg *types.Page, err error) {
|
||||
if nsID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if gf.id > 0 {
|
||||
pg, err = store.LookupComposePageByID(ctx, s, gf.id)
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if pg != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, i := range gf.identifiers {
|
||||
pg, err = store.LookupComposePageByNamespaceIDHandle(ctx, s, nsID, i)
|
||||
if err == store.ErrNotFound {
|
||||
var pp types.PageSet
|
||||
pp, _, err = store.SearchComposePages(ctx, s, types.PageFilter{
|
||||
NamespaceID: nsID,
|
||||
Title: i,
|
||||
Paging: filter.Paging{
|
||||
Limit: 2,
|
||||
},
|
||||
})
|
||||
if len(pp) > 1 {
|
||||
return nil, resourceErrIdentifierNotUnique(i)
|
||||
}
|
||||
if len(pp) == 1 {
|
||||
pg = pp[0]
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if pg != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
369
server/pkg/envoy/store/compose_page_marshal.go
Normal file
369
server/pkg/envoy/store/compose_page_marshal.go
Normal file
@@ -0,0 +1,369 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
atypes "github.com/cortezaproject/corteza/server/automation/types"
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
)
|
||||
|
||||
func newComposePageFromResource(res *resource.ComposePage, cfg *EncoderConfig) resourceState {
|
||||
return &composePage{
|
||||
cfg: mergeConfig(cfg, res.Config()),
|
||||
|
||||
res: res,
|
||||
|
||||
relWfs: make(map[string]*atypes.Workflow),
|
||||
relMods: make(map[string]*types.Module),
|
||||
relCharts: make(map[string]*types.Chart),
|
||||
}
|
||||
}
|
||||
|
||||
func (n *composePage) Prepare(ctx context.Context, pl *payload) (err error) {
|
||||
// Reset old identifiers
|
||||
n.res.Res.ID = 0
|
||||
n.res.Res.SelfID = 0
|
||||
n.res.Res.NamespaceID = 0
|
||||
n.res.Res.ModuleID = 0
|
||||
|
||||
// Get related namespace
|
||||
n.relNS, err = findComposeNamespace(ctx, pl.s, pl.state.ParentResources, n.res.RefNs.Identifiers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n.relNS == nil {
|
||||
return resource.ComposeNamespaceErrUnresolved(n.res.RefNs.Identifiers)
|
||||
}
|
||||
|
||||
// Get related module
|
||||
// If this isn't a record page, there is no related module
|
||||
if n.res.RefMod != nil {
|
||||
n.relMod, err = findComposeModule(ctx, pl.s, n.relNS.ID, pl.state.ParentResources, n.res.RefMod.Identifiers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n.relMod == nil {
|
||||
return resource.ComposeModuleErrUnresolved(n.res.RefMod.Identifiers)
|
||||
}
|
||||
}
|
||||
|
||||
// Get parent page
|
||||
if n.res.RefParent != nil {
|
||||
n.relParent, err = findComposePage(ctx, pl.s, n.relNS.ID, pl.state.ParentResources, n.res.RefParent.Identifiers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n.relParent == nil {
|
||||
return resource.ComposePageErrUnresolved(n.res.RefParent.Identifiers)
|
||||
}
|
||||
}
|
||||
|
||||
// Get related workflows
|
||||
for _, wfr := range n.res.WfRefs {
|
||||
wf, err := findAutomationWorkflow(ctx, pl.s, pl.state.ParentResources, wfr.Identifiers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if wf == nil {
|
||||
return resource.AutomationWorkflowErrUnresolved(wfr.Identifiers)
|
||||
}
|
||||
for _, id := range wfr.Identifiers {
|
||||
n.relWfs[id] = wf
|
||||
}
|
||||
}
|
||||
|
||||
// Get other related modules
|
||||
for _, mr := range n.res.ModRefs {
|
||||
mod, err := findComposeModule(ctx, pl.s, n.relNS.ID, pl.state.ParentResources, mr.Identifiers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if mod == nil {
|
||||
return resource.ComposeModuleErrUnresolved(mr.Identifiers)
|
||||
}
|
||||
for _, id := range mr.Identifiers {
|
||||
n.relMods[id] = mod
|
||||
}
|
||||
}
|
||||
|
||||
// Get related charts
|
||||
for _, refChart := range n.res.RefCharts {
|
||||
chr, err := findComposeChart(ctx, pl.s, n.relNS.ID, pl.state.ParentResources, refChart.Identifiers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if chr == nil {
|
||||
return resource.ComposeChartErrUnresolved(refChart.Identifiers)
|
||||
}
|
||||
for _, id := range refChart.Identifiers {
|
||||
n.relCharts[id] = chr
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get the original page
|
||||
n.pg, err = findComposePageStore(ctx, pl.s, n.relNS.ID, makeGenericFilter(n.res.Identifiers()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if n.pg != nil {
|
||||
n.res.Res.ID = n.pg.ID
|
||||
n.res.Res.NamespaceID = n.pg.NamespaceID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *composePage) Encode(ctx context.Context, pl *payload) (err error) {
|
||||
res := n.res.Res
|
||||
exists := n.pg != nil && n.pg.ID > 0
|
||||
|
||||
// Determine the ID
|
||||
if res.ID <= 0 && exists {
|
||||
res.ID = n.pg.ID
|
||||
}
|
||||
if res.ID <= 0 {
|
||||
res.ID = NextID()
|
||||
}
|
||||
|
||||
ts := n.res.Timestamps()
|
||||
if ts != nil {
|
||||
if ts.CreatedAt != nil {
|
||||
res.CreatedAt = *ts.CreatedAt.T
|
||||
} else {
|
||||
res.CreatedAt = *now()
|
||||
}
|
||||
if ts.UpdatedAt != nil {
|
||||
res.UpdatedAt = ts.UpdatedAt.T
|
||||
}
|
||||
if ts.DeletedAt != nil {
|
||||
res.DeletedAt = ts.DeletedAt.T
|
||||
}
|
||||
}
|
||||
|
||||
// Namespace
|
||||
res.NamespaceID = n.relNS.ID
|
||||
if res.NamespaceID <= 0 {
|
||||
ns := resource.FindComposeNamespace(pl.state.ParentResources, n.res.RefNs.Identifiers)
|
||||
res.NamespaceID = ns.ID
|
||||
}
|
||||
|
||||
if res.NamespaceID <= 0 {
|
||||
return resource.ComposeNamespaceErrUnresolved(n.res.RefNs.Identifiers)
|
||||
}
|
||||
|
||||
// Module?
|
||||
if n.res.RefMod != nil {
|
||||
res.ModuleID = n.relMod.ID
|
||||
if res.ModuleID <= 0 {
|
||||
mod := resource.FindComposeModule(pl.state.ParentResources, n.res.RefMod.Identifiers)
|
||||
res.ModuleID = mod.ID
|
||||
}
|
||||
}
|
||||
|
||||
// Parent?
|
||||
if n.res.RefParent != nil {
|
||||
res.SelfID = n.relParent.ID
|
||||
if res.SelfID <= 0 {
|
||||
mod := resource.FindComposePage(pl.state.ParentResources, n.res.RefParent.Identifiers)
|
||||
res.SelfID = mod.ID
|
||||
}
|
||||
}
|
||||
|
||||
// Blocks
|
||||
getModID := func(id string) uint64 {
|
||||
mod := n.relMods[id]
|
||||
if mod == nil || mod.ID <= 0 {
|
||||
mod = resource.FindComposeModule(pl.state.ParentResources, resource.MakeIdentifiers(id))
|
||||
if mod == nil || mod.ID <= 0 {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
return mod.ID
|
||||
}
|
||||
getWfID := func(id string) uint64 {
|
||||
wf := n.relWfs[id]
|
||||
if wf == nil || wf.ID <= 0 {
|
||||
wf = resource.FindAutomationWorkflow(pl.state.ParentResources, resource.MakeIdentifiers(id))
|
||||
if wf == nil || wf.ID <= 0 {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
return wf.ID
|
||||
}
|
||||
|
||||
getChartID := func(id string) uint64 {
|
||||
chr := n.relCharts[id]
|
||||
if chr == nil || chr.ID <= 0 {
|
||||
chr = resource.FindComposeChart(pl.state.ParentResources, resource.MakeIdentifiers(id))
|
||||
if chr == nil || chr.ID <= 0 {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
return chr.ID
|
||||
}
|
||||
|
||||
// Quick utility to extract references from options
|
||||
ss := func(m map[string]interface{}, kk ...string) string {
|
||||
for _, k := range kk {
|
||||
if vr, has := m[k]; has {
|
||||
v, _ := vr.(string)
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Get max blockID for later use
|
||||
blockID := uint64(0)
|
||||
for _, b := range res.Blocks {
|
||||
if b.BlockID > blockID {
|
||||
blockID = b.BlockID
|
||||
}
|
||||
}
|
||||
|
||||
for i, b := range res.Blocks {
|
||||
|
||||
// Assure blockIDs
|
||||
if b.BlockID == 0 {
|
||||
blockID++
|
||||
b.BlockID = blockID
|
||||
res.Blocks[i] = b
|
||||
}
|
||||
|
||||
switch b.Kind {
|
||||
case "RecordList":
|
||||
id := ss(b.Options, "module", "moduleID")
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
mID := getModID(id)
|
||||
if mID <= 0 {
|
||||
return resource.ComposeModuleErrUnresolved(resource.MakeIdentifiers(id))
|
||||
}
|
||||
b.Options["moduleID"] = strconv.FormatUint(mID, 10)
|
||||
delete(b.Options, "module")
|
||||
|
||||
case "Automation":
|
||||
bb, _ := b.Options["buttons"].([]interface{})
|
||||
for _, b := range bb {
|
||||
button, _ := b.(map[string]interface{})
|
||||
id := ss(button, "workflow", "workflowID")
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
wfID := getWfID(id)
|
||||
if wfID <= 0 {
|
||||
return resource.AutomationWorkflowErrUnresolved(resource.MakeIdentifiers(id))
|
||||
}
|
||||
button["workflowID"] = strconv.FormatUint(wfID, 10)
|
||||
delete(button, "workflow")
|
||||
}
|
||||
|
||||
case "RecordOrganizer":
|
||||
id := ss(b.Options, "module", "moduleID")
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
mID := getModID(id)
|
||||
if mID <= 0 {
|
||||
return resource.ComposeModuleErrUnresolved(resource.MakeIdentifiers(id))
|
||||
}
|
||||
b.Options["moduleID"] = strconv.FormatUint(mID, 10)
|
||||
delete(b.Options, "module")
|
||||
|
||||
case "Calendar":
|
||||
ff, _ := b.Options["feeds"].([]interface{})
|
||||
for _, f := range ff {
|
||||
feed, _ := f.(map[string]interface{})
|
||||
fOpts, _ := (feed["options"]).(map[string]interface{})
|
||||
id := ss(fOpts, "module", "moduleID")
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
mID := getModID(id)
|
||||
if mID <= 0 {
|
||||
return resource.ComposeModuleErrUnresolved(resource.MakeIdentifiers(id))
|
||||
}
|
||||
fOpts["moduleID"] = strconv.FormatUint(mID, 10)
|
||||
delete(fOpts, "module")
|
||||
}
|
||||
|
||||
case "Chart":
|
||||
id := ss(b.Options, "chart", "chartID")
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
chrID := getChartID(id)
|
||||
if chrID == 0 {
|
||||
return resource.ComposeChartErrUnresolved(resource.MakeIdentifiers(id))
|
||||
}
|
||||
b.Options["chartID"] = strconv.FormatUint(chrID, 10)
|
||||
delete(b.Options, "chart")
|
||||
|
||||
case "Metric":
|
||||
mm, _ := b.Options["metrics"].([]interface{})
|
||||
for _, m := range mm {
|
||||
mops, _ := m.(map[string]interface{})
|
||||
id := ss(mops, "module", "moduleID")
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
mID := getModID(id)
|
||||
if mID <= 0 {
|
||||
return resource.ComposeModuleErrUnresolved(resource.MakeIdentifiers(id))
|
||||
}
|
||||
mops["moduleID"] = strconv.FormatUint(mID, 10)
|
||||
delete(mops, "module")
|
||||
|
||||
}
|
||||
|
||||
case "Comment":
|
||||
id := ss(b.Options, "module", "moduleID")
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
mID := getModID(id)
|
||||
if mID <= 0 {
|
||||
return resource.ComposeModuleErrUnresolved(resource.MakeIdentifiers(id))
|
||||
}
|
||||
b.Options["moduleID"] = strconv.FormatUint(mID, 10)
|
||||
delete(b.Options, "module")
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate the resource skip expression
|
||||
// @todo expand available parameters; similar implementation to compose/types/record@Dict
|
||||
if skip, err := basicSkipEval(ctx, n.cfg, !exists); err != nil {
|
||||
return err
|
||||
} else if skip {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a fresh page
|
||||
if !exists {
|
||||
return store.CreateComposePage(ctx, pl.s, res)
|
||||
}
|
||||
|
||||
// Update existing page
|
||||
switch n.cfg.OnExisting {
|
||||
case resource.Skip:
|
||||
return nil
|
||||
|
||||
case resource.MergeLeft:
|
||||
res = mergeComposePage(n.pg, res)
|
||||
|
||||
case resource.MergeRight:
|
||||
res = mergeComposePage(res, n.pg)
|
||||
}
|
||||
|
||||
err = store.UpdateComposePage(ctx, pl.s, res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.res.Res = res
|
||||
return nil
|
||||
}
|
||||
69
server/pkg/envoy/store/compose_page_test.go
Normal file
69
server/pkg/envoy/store/compose_page_test.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestComposePage_Merger(t *testing.T) {
|
||||
req := require.New(t)
|
||||
|
||||
now := time.Time{}
|
||||
nowP := &time.Time{}
|
||||
|
||||
empty := &types.Page{}
|
||||
full := &types.Page{
|
||||
SelfID: 1,
|
||||
NamespaceID: 2,
|
||||
ModuleID: 3,
|
||||
Handle: "handle",
|
||||
Title: "title",
|
||||
Description: "description",
|
||||
Blocks: types.PageBlocks{types.PageBlock{}},
|
||||
Children: types.PageSet{&types.Page{}},
|
||||
Weight: 4,
|
||||
|
||||
CreatedAt: now,
|
||||
UpdatedAt: nowP,
|
||||
DeletedAt: nowP,
|
||||
}
|
||||
|
||||
t.Run("merge on empty", func(t *testing.T) {
|
||||
c := mergeComposePage(empty, full)
|
||||
req.Equal(uint64(1), c.SelfID)
|
||||
req.Equal(uint64(2), c.NamespaceID)
|
||||
req.Equal(uint64(3), c.ModuleID)
|
||||
|
||||
req.Equal("handle", c.Handle)
|
||||
req.Equal("title", c.Title)
|
||||
req.Equal("description", c.Description)
|
||||
req.Len(c.Blocks, 1)
|
||||
req.Len(c.Children, 1)
|
||||
req.Equal(4, c.Weight)
|
||||
|
||||
req.Equal(now, c.CreatedAt)
|
||||
req.Equal(nowP, c.UpdatedAt)
|
||||
req.Equal(nowP, c.DeletedAt)
|
||||
})
|
||||
|
||||
t.Run("merge with empty", func(t *testing.T) {
|
||||
c := mergeComposePage(full, empty)
|
||||
req.Equal(uint64(0), c.SelfID)
|
||||
req.Equal(uint64(0), c.NamespaceID)
|
||||
req.Equal(uint64(0), c.ModuleID)
|
||||
|
||||
req.Equal("handle", c.Handle)
|
||||
req.Equal("title", c.Title)
|
||||
req.Equal("description", c.Description)
|
||||
req.Len(c.Blocks, 1)
|
||||
req.Len(c.Children, 1)
|
||||
req.Equal(0, c.Weight)
|
||||
|
||||
req.Equal(now, c.CreatedAt)
|
||||
req.Equal(nowP, c.UpdatedAt)
|
||||
req.Equal(nowP, c.DeletedAt)
|
||||
})
|
||||
}
|
||||
31
server/pkg/envoy/store/compose_page_unmarshal.go
Normal file
31
server/pkg/envoy/store/compose_page_unmarshal.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
)
|
||||
|
||||
func newComposePage(pg *types.Page) *composePage {
|
||||
return &composePage{
|
||||
pg: pg,
|
||||
}
|
||||
}
|
||||
|
||||
func (pg *composePage) MarshalEnvoy() ([]resource.Interface, error) {
|
||||
var (
|
||||
refNs = resource.MakeNamespaceRef(pg.pg.NamespaceID, "", "")
|
||||
refMod *resource.Ref
|
||||
refParent *resource.Ref
|
||||
)
|
||||
if pg.pg.ModuleID > 0 {
|
||||
refMod = resource.MakeModuleRef(pg.pg.ModuleID, "", "")
|
||||
}
|
||||
if pg.pg.SelfID > 0 {
|
||||
refParent = resource.MakePageRef(pg.pg.SelfID, "", "")
|
||||
}
|
||||
|
||||
return envoy.CollectNodes(
|
||||
resource.NewComposePage(pg.pg, refNs, refMod, refParent),
|
||||
)
|
||||
}
|
||||
35
server/pkg/envoy/store/compose_record.go
Normal file
35
server/pkg/envoy/store/compose_record.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
)
|
||||
|
||||
type (
|
||||
composeRecordAux struct {
|
||||
refMod string
|
||||
relMod *types.Module
|
||||
|
||||
refNs string
|
||||
relUsers resource.UserstampIndex
|
||||
walker resource.CrsWalker
|
||||
}
|
||||
|
||||
composeRecord struct {
|
||||
cfg *EncoderConfig
|
||||
|
||||
res *resource.ComposeRecord
|
||||
rec *composeRecordAux
|
||||
|
||||
relNS *types.Namespace
|
||||
relMod *types.Module
|
||||
|
||||
fieldModRef map[string]resource.Identifiers
|
||||
// module identifier -> record identifier -> recordID
|
||||
externalRef map[string]map[string]uint64
|
||||
recMap map[string]*types.Record
|
||||
|
||||
// Little helper flag for conditional encoding
|
||||
missing bool
|
||||
}
|
||||
)
|
||||
471
server/pkg/envoy/store/compose_record_marshal.go
Normal file
471
server/pkg/envoy/store/compose_record_marshal.go
Normal file
@@ -0,0 +1,471 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/dalutils"
|
||||
"github.com/cortezaproject/corteza/server/compose/service"
|
||||
"github.com/cortezaproject/corteza/server/compose/service/values"
|
||||
composeTypes "github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
systemTypes "github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
var (
|
||||
rvSanitizer = values.Sanitizer()
|
||||
rvValidator = values.Validator()
|
||||
rvFormatter = values.Formatter()
|
||||
)
|
||||
|
||||
func NewComposeRecordFromResource(res *resource.ComposeRecord, cfg *EncoderConfig) resourceState {
|
||||
return &composeRecord{
|
||||
cfg: cfg,
|
||||
fieldModRef: make(map[string]resource.Identifiers),
|
||||
externalRef: make(map[string]map[string]uint64),
|
||||
recMap: make(map[string]*composeTypes.Record),
|
||||
|
||||
res: res,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *composeRecord) Prepare(ctx context.Context, pl *payload) (err error) {
|
||||
// Get related namespace
|
||||
n.relNS, err = findComposeNamespace(ctx, pl.s, pl.state.ParentResources, n.res.RefNs.Identifiers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n.relNS == nil {
|
||||
return resource.ComposeNamespaceErrUnresolved(n.res.RefNs.Identifiers)
|
||||
}
|
||||
|
||||
n.missing = true
|
||||
n.relMod = resource.FindComposeModule(pl.state.ParentResources, n.res.RefMod.Identifiers)
|
||||
if n.relMod == nil && n.relNS.ID > 0 {
|
||||
n.relMod, err = findComposeModuleStore(ctx, pl.s, n.relNS.ID, makeGenericFilter(n.res.RefMod.Identifiers))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n.relMod != nil {
|
||||
// Preload existing fields
|
||||
n.relMod.Fields, err = findComposeModuleFieldsStore(ctx, pl.s, n.relMod)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if n.relMod == nil {
|
||||
return resource.ComposeModuleErrUnresolved(n.res.RefMod.Identifiers)
|
||||
}
|
||||
|
||||
// Preload sys users
|
||||
if n.res.UserFlakes == nil {
|
||||
n.res.UserFlakes = make(resource.UserstampIndex)
|
||||
}
|
||||
if len(n.res.UserFlakes) == 0 {
|
||||
// No users provided, let's try to fetch them
|
||||
uu, _, err := store.SearchUsers(ctx, pl.s, systemTypes.UserFilter{
|
||||
Paging: filter.Paging{
|
||||
Limit: 0,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n.res.UserFlakes.Add(uu...)
|
||||
}
|
||||
|
||||
// Add missing refs
|
||||
preloadRefs := make(resource.RefSet, 0, int(len(n.relMod.Fields)/2)+1)
|
||||
for _, f := range n.relMod.Fields {
|
||||
switch f.Kind {
|
||||
case "Record":
|
||||
refM := f.Options.String("module")
|
||||
if refM == "" {
|
||||
refM = f.Options.String("moduleID")
|
||||
}
|
||||
if refM != "" && refM != "0" {
|
||||
// Make a reference with that module's records
|
||||
ref := n.res.AddRef(composeTypes.RecordResourceType, refM).Constraint(n.res.RefNs)
|
||||
|
||||
n.fieldModRef[f.Name] = ref.Identifiers
|
||||
preloadRefs = append(preloadRefs, ref)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Can't do anything else, since the NS doesn't yet exist
|
||||
if n.relNS.ID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Preload potential references
|
||||
//
|
||||
// This is a fairly primitive approach, try to think of something a bit nicer
|
||||
for _, ref := range preloadRefs {
|
||||
mod, err := findComposeModuleStore(ctx, pl.s, n.relNS.ID, makeGenericFilter(ref.Identifiers))
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return err
|
||||
}
|
||||
auxMap := make(map[string]uint64)
|
||||
for _, i := range ref.Identifiers {
|
||||
n.externalRef[i] = auxMap
|
||||
}
|
||||
|
||||
// Preload all records
|
||||
rr, _, err := dalutils.ComposeRecordsList(ctx, pl.dal, mod, composeTypes.RecordFilter{
|
||||
ModuleID: mod.ID,
|
||||
NamespaceID: mod.NamespaceID,
|
||||
Paging: filter.Paging{
|
||||
Limit: 0,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, r := range rr {
|
||||
auxMap[strconv.FormatUint(r.ID, 10)] = r.ID
|
||||
}
|
||||
}
|
||||
|
||||
// Can't work with own record because the module doesn't yet exist
|
||||
if n.relMod.ID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Preload own records
|
||||
rr, _, err := dalutils.ComposeRecordsList(ctx, pl.dal, n.relMod, composeTypes.RecordFilter{
|
||||
ModuleID: n.relMod.ID,
|
||||
NamespaceID: n.relNS.ID,
|
||||
Paging: filter.Paging{
|
||||
Limit: 0,
|
||||
},
|
||||
})
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return err
|
||||
}
|
||||
n.missing = len(rr) == 0
|
||||
|
||||
// Map existing records so we can perform updates
|
||||
// Map to xref map for easier use later
|
||||
auxMap := make(map[string]uint64)
|
||||
for _, i := range n.res.RefMod.Identifiers {
|
||||
n.externalRef[i] = auxMap
|
||||
}
|
||||
for _, r := range rr {
|
||||
key := strconv.FormatUint(r.ID, 10)
|
||||
n.recMap[key] = r
|
||||
|
||||
// Map IDs to xref map
|
||||
auxMap[key] = r.ID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// @note composeRecord.Encode can raise an error in case of unresolved user dependencies.
|
||||
func (n *composeRecord) Encode(ctx context.Context, pl *payload) (err error) {
|
||||
// Namespace
|
||||
nsID := n.relNS.ID
|
||||
if nsID <= 0 {
|
||||
ns := resource.FindComposeNamespace(pl.state.ParentResources, n.res.RefNs.Identifiers)
|
||||
nsID = ns.ID
|
||||
}
|
||||
|
||||
// Module
|
||||
mod := n.relMod
|
||||
if mod.ID <= 0 {
|
||||
m := resource.FindComposeModule(pl.state.ParentResources, n.res.RefMod.Identifiers)
|
||||
mod.ID = m.ID
|
||||
}
|
||||
|
||||
// Aggregate all of the available users
|
||||
// ux will map { identifier: userID }
|
||||
ux := make(map[string]uint64)
|
||||
// Firstly the users provided by the record pl.state itself
|
||||
for _, ur := range n.res.UserFlakes {
|
||||
u := ur.U
|
||||
|
||||
ux[strconv.FormatUint(u.ID, 10)] = u.ID
|
||||
if u.Handle != "" {
|
||||
ux[u.Handle] = u.ID
|
||||
}
|
||||
if u.Name != "" {
|
||||
ux[u.Name] = u.ID
|
||||
}
|
||||
if u.Email != "" {
|
||||
ux[u.Email] = u.ID
|
||||
}
|
||||
}
|
||||
// Next all of the encoded users.
|
||||
// If identifiers overwrite eachother, that's fine.
|
||||
for _, ref := range pl.state.ParentResources {
|
||||
if ref.ResourceType() == systemTypes.UserResourceType {
|
||||
refUsr := ref.(*resource.User)
|
||||
for _, i := range refUsr.Identifiers() {
|
||||
ux[i] = refUsr.SysID()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Some pointing
|
||||
rm := n.recMap
|
||||
im := n.res.IDMap
|
||||
|
||||
getKey := func(i int, k string) string {
|
||||
if k == "" {
|
||||
return strconv.FormatInt(int64(i), 10)
|
||||
}
|
||||
|
||||
return k
|
||||
}
|
||||
|
||||
checkXRef := func(ii resource.Identifiers, ref string) (uint64, error) {
|
||||
var auxMap map[string]uint64
|
||||
for _, ri := range ii {
|
||||
if mp, ok := n.externalRef[ri]; ok {
|
||||
auxMap = mp
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if auxMap == nil || len(auxMap) == 0 {
|
||||
return 0, fmt.Errorf("referenced record not resolved: %s", resource.ComposeRecordErrUnresolved(resource.MakeIdentifiers(ref)))
|
||||
}
|
||||
|
||||
return auxMap[ref], nil
|
||||
}
|
||||
|
||||
i := -1
|
||||
return n.res.Walker(func(r *resource.ComposeRecordRaw) error {
|
||||
i++
|
||||
|
||||
// So we don't have to worry about nil
|
||||
cfg := r.Config
|
||||
if cfg == nil {
|
||||
cfg = &resource.EnvoyConfig{}
|
||||
}
|
||||
|
||||
if cfg.SkipIf != "" {
|
||||
evl, err := exprP.NewEvaluable(cfg.SkipIf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// @todo expand this
|
||||
skip, err := evl.EvalBool(ctx, map[string]interface{}{
|
||||
"missing": n.missing,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if skip {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
rec := &composeTypes.Record{
|
||||
ID: im[getKey(i, r.ID)],
|
||||
NamespaceID: nsID,
|
||||
ModuleID: mod.ID,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
exists := false
|
||||
var old *composeTypes.Record
|
||||
if r.ID != "" {
|
||||
old = rm[r.ID]
|
||||
exists = old != nil
|
||||
}
|
||||
|
||||
if rec.ID == 0 && exists {
|
||||
rec.ID = rm[r.ID].ID
|
||||
}
|
||||
if rec.ID == 0 {
|
||||
rec.ID = NextID()
|
||||
}
|
||||
|
||||
im[getKey(i, r.ID)] = rec.ID
|
||||
|
||||
if pl.state.Conflicting {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := func() error {
|
||||
// Timestamps
|
||||
if r.Ts != nil {
|
||||
if r.Ts.CreatedAt != nil {
|
||||
rec.CreatedAt = *r.Ts.CreatedAt.T
|
||||
} else {
|
||||
rec.CreatedAt = *now()
|
||||
}
|
||||
if r.Ts.UpdatedAt != nil {
|
||||
rec.UpdatedAt = r.Ts.UpdatedAt.T
|
||||
}
|
||||
if r.Ts.DeletedAt != nil {
|
||||
rec.DeletedAt = r.Ts.DeletedAt.T
|
||||
}
|
||||
}
|
||||
|
||||
// Userstamps
|
||||
rec.CreatedBy = pl.invokerID
|
||||
if r.Us != nil {
|
||||
if r.Us.CreatedBy != nil {
|
||||
rec.CreatedBy = ux[r.Us.CreatedBy.Ref]
|
||||
}
|
||||
if r.Us.UpdatedBy != nil {
|
||||
rec.UpdatedBy = ux[r.Us.UpdatedBy.Ref]
|
||||
}
|
||||
if r.Us.DeletedBy != nil {
|
||||
rec.DeletedBy = ux[r.Us.DeletedBy.Ref]
|
||||
}
|
||||
if r.Us.OwnedBy != nil {
|
||||
rec.OwnedBy = ux[r.Us.OwnedBy.Ref]
|
||||
}
|
||||
}
|
||||
|
||||
rec.OwnedBy = service.CalcRecordOwner(0, rec.OwnedBy, pl.invokerID)
|
||||
|
||||
rvs := make(composeTypes.RecordValueSet, 0, len(r.Values))
|
||||
for k, rr := range r.Values {
|
||||
v := rr.Value
|
||||
|
||||
rv := &composeTypes.RecordValue{
|
||||
RecordID: rec.ID,
|
||||
Name: k,
|
||||
Value: v,
|
||||
Updated: true,
|
||||
}
|
||||
|
||||
f := mod.Fields.FindByName(k)
|
||||
if f != nil && v != "" {
|
||||
switch f.Kind {
|
||||
case "User":
|
||||
uID := ux[v]
|
||||
if uID == 0 {
|
||||
return resource.UserErrUnresolved(resource.MakeIdentifiers(v))
|
||||
}
|
||||
rv.Value = strconv.FormatUint(uID, 10)
|
||||
rv.Ref = uID
|
||||
|
||||
case "Record":
|
||||
refIdentifiers, ok := n.fieldModRef[f.Name]
|
||||
if !ok {
|
||||
return fmt.Errorf("module field record reference not resoled: %s", f.Name)
|
||||
}
|
||||
|
||||
// if self...
|
||||
if n.res.RefMod.Identifiers.HasAny(refIdentifiers) {
|
||||
rID := im[v]
|
||||
|
||||
// Check if its in the store
|
||||
if rID == 0 {
|
||||
// Check if we have an xref
|
||||
rID, err = checkXRef(refIdentifiers, v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if rID == 0 {
|
||||
return resource.ComposeRecordErrUnresolved(resource.MakeIdentifiers(v))
|
||||
}
|
||||
rv.Value = strconv.FormatUint(rID, 10)
|
||||
rv.Ref = rID
|
||||
} else {
|
||||
// not self...
|
||||
rID := uint64(0)
|
||||
refRes := resource.FindComposeRecordResource(pl.state.ParentResources, refIdentifiers)
|
||||
|
||||
if refRes != nil {
|
||||
// check if parent has it
|
||||
rID = refRes.IDMap[v]
|
||||
}
|
||||
|
||||
if rID == 0 {
|
||||
// Check if we have an xref
|
||||
rID, err = checkXRef(refIdentifiers, v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if rID == 0 {
|
||||
return fmt.Errorf("referenced record not resolved: %s", resource.ComposeRecordErrUnresolved(resource.MakeIdentifiers(v)))
|
||||
}
|
||||
|
||||
rv.Value = strconv.FormatUint(rID, 10)
|
||||
rv.Ref = rID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rvs = append(rvs, rv)
|
||||
}
|
||||
|
||||
if err = service.RecordValueSanitization(mod, rvs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rec.Values = rvs
|
||||
rec.Values.SetUpdatedFlag(true)
|
||||
|
||||
// @todo owner?
|
||||
var (
|
||||
rve *composeTypes.RecordValueErrorSet
|
||||
|
||||
canAccessField = func(f *composeTypes.ModuleField) bool {
|
||||
return true
|
||||
}
|
||||
)
|
||||
|
||||
if old != nil {
|
||||
rec.Values = old.Values.Merge(mod.Fields, rec.Values, canAccessField)
|
||||
}
|
||||
|
||||
rec.Values, _ = rec.Values.Filter(func(v *composeTypes.RecordValue) (bool, error) {
|
||||
return mod.Fields.HasName(v.Name), nil
|
||||
})
|
||||
|
||||
rve = service.RecordValueUpdateOpCheck(ctx, nil, mod, rec.Values)
|
||||
if !rve.IsValid() {
|
||||
return rve
|
||||
}
|
||||
|
||||
rve = service.RecordPreparer(ctx, pl.s, rvSanitizer, rvValidator, rvFormatter, mod, rec)
|
||||
if !rve.IsValid() {
|
||||
return rve
|
||||
}
|
||||
|
||||
// Create a new record
|
||||
if !exists {
|
||||
err = dalutils.ComposeRecordCreate(ctx, pl.dal, mod, rec)
|
||||
return err
|
||||
}
|
||||
|
||||
// Update existing
|
||||
err = dalutils.ComposeRecordUpdate(ctx, pl.dal, mod, rec)
|
||||
return err
|
||||
}()
|
||||
|
||||
if n.cfg.Defer != nil {
|
||||
n.cfg.Defer()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if n.cfg.DeferNok != nil {
|
||||
return n.cfg.DeferNok(err)
|
||||
}
|
||||
return err
|
||||
} else if n.cfg.DeferOk != nil {
|
||||
n.cfg.DeferOk()
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
23
server/pkg/envoy/store/compose_record_unmarshal.go
Normal file
23
server/pkg/envoy/store/compose_record_unmarshal.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
)
|
||||
|
||||
func newComposeRecordFromAux(rec *composeRecordAux) *composeRecord {
|
||||
return &composeRecord{
|
||||
rec: rec,
|
||||
relMod: rec.relMod,
|
||||
}
|
||||
}
|
||||
|
||||
func (rec *composeRecord) MarshalEnvoy() ([]resource.Interface, error) {
|
||||
rr := resource.NewComposeRecordSet(rec.rec.walker, rec.rec.refNs, rec.rec.refMod)
|
||||
rr.SetUserFlakes(rec.rec.relUsers)
|
||||
rr.RelMod = rec.relMod
|
||||
|
||||
return envoy.CollectNodes(
|
||||
rr,
|
||||
)
|
||||
}
|
||||
211
server/pkg/envoy/store/decoder.go
Normal file
211
server/pkg/envoy/store/decoder.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/pkg/id"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
decoder struct {
|
||||
ux *userIndex
|
||||
}
|
||||
|
||||
DecodeFilter struct {
|
||||
|
||||
// Compose stuff
|
||||
composeNamespace []*composeNamespaceFilter
|
||||
composeModule []*composeModuleFilter
|
||||
composeRecord []*composeRecordFilter
|
||||
composePage []*composePageFilter
|
||||
composeChart []*composeChartFilter
|
||||
|
||||
// System stuff
|
||||
roles []*roleFilter
|
||||
users []*userFilter
|
||||
templates []*templateFilter
|
||||
applications []*applicationFilter
|
||||
apiGwRoutes []*apiGwRouteFilter
|
||||
reports []*reportFilter
|
||||
|
||||
settings []*settingFilter
|
||||
rbac []*rbacFilter
|
||||
resourceTranslations []*resourceTranslationFilter
|
||||
|
||||
// Automation stuff
|
||||
automationWorkflow []*automationWorkflowFilter
|
||||
}
|
||||
|
||||
// These two aux things let us simplify the code a little bit
|
||||
auxMarshaller []envoy.Marshaller
|
||||
auxRsp struct {
|
||||
mm []envoy.Marshaller
|
||||
err error
|
||||
}
|
||||
|
||||
// We'll use the userIndex to hold required users
|
||||
userIndex struct {
|
||||
users map[uint64]*types.User
|
||||
s store.Users
|
||||
}
|
||||
)
|
||||
|
||||
func Decoder() *decoder {
|
||||
return &decoder{}
|
||||
}
|
||||
|
||||
func NewDecodeFilter() *DecodeFilter {
|
||||
return &DecodeFilter{}
|
||||
}
|
||||
|
||||
func (df *DecodeFilter) FromResource(rr ...string) *DecodeFilter {
|
||||
df = df.systemFromResource(rr...)
|
||||
df = df.automationFromResource(rr...)
|
||||
df = df.composeFromResource(rr...)
|
||||
|
||||
return df
|
||||
}
|
||||
|
||||
func (df *DecodeFilter) FromRef(rr ...*resource.Ref) *DecodeFilter {
|
||||
df = df.systemFromRef(rr...)
|
||||
df = df.automationFromRef(rr...)
|
||||
df = df.composeFromRef(rr...)
|
||||
|
||||
return df
|
||||
}
|
||||
|
||||
func (aum auxMarshaller) MarshalEnvoy() ([]resource.Interface, error) {
|
||||
ii := make([]resource.Interface, 0, len(aum))
|
||||
for _, m := range aum {
|
||||
tmp, err := m.MarshalEnvoy()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Default locales for applicable resources
|
||||
for _, t := range tmp {
|
||||
if li, ok := t.(resource.LocaleInterface); ok {
|
||||
ll, err := li.EncodeTranslations()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, l := range ll {
|
||||
l.MarkDefault()
|
||||
ii = append(ii, l)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ii = append(ii, tmp...)
|
||||
}
|
||||
|
||||
return ii, nil
|
||||
}
|
||||
|
||||
// Decode decodes all of the things in the provided store
|
||||
func (d *decoder) Decode(ctx context.Context, s store.Storer, dal dalService, f *DecodeFilter) ([]resource.Interface, error) {
|
||||
mm := make(auxMarshaller, 0, 100)
|
||||
|
||||
if d.ux == nil {
|
||||
d.ux = &userIndex{
|
||||
users: make(map[uint64]*types.User),
|
||||
}
|
||||
}
|
||||
d.ux.s = s
|
||||
|
||||
pof := func(rr ...*auxRsp) (auxMarshaller, error) {
|
||||
mm := make(auxMarshaller, 0, 200)
|
||||
|
||||
for _, r := range rr {
|
||||
if r == nil {
|
||||
continue
|
||||
}
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
|
||||
mm = append(mm, r.mm...)
|
||||
}
|
||||
|
||||
return mm, nil
|
||||
}
|
||||
|
||||
compose := newComposeDecoder(dal)
|
||||
system := newSystemDecoder(d.ux)
|
||||
automation := newAutomationDecoder(d.ux)
|
||||
|
||||
mm, err := pof(
|
||||
compose.decodeComposeNamespace(ctx, s, f.composeNamespace),
|
||||
compose.decodeComposeModule(ctx, s, f.composeModule),
|
||||
compose.decodeComposeRecord(ctx, s, f.composeRecord),
|
||||
compose.decodeComposePage(ctx, s, f.composePage),
|
||||
compose.decodeComposeChart(ctx, s, f.composeChart),
|
||||
|
||||
system.decodeRoles(ctx, s, f.roles),
|
||||
system.decodeUsers(ctx, s, f.users),
|
||||
system.decodeTemplates(ctx, s, f.templates),
|
||||
system.decodeApplications(ctx, s, f.applications),
|
||||
system.decodeAPIGWRoutes(ctx, s, f.apiGwRoutes),
|
||||
system.decodeReports(ctx, s, f.reports),
|
||||
system.decodeSettings(ctx, s, f.settings),
|
||||
system.decodeResourceTranslation(ctx, s, f.resourceTranslations),
|
||||
|
||||
automation.decodeWorkflows(ctx, s, f.automationWorkflow),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f.allowRbacResource(compose.resourceID...)
|
||||
f.allowRbacResource(system.resourceID...)
|
||||
f.allowRbacResource(automation.resourceID...)
|
||||
rr, err := pof(
|
||||
system.decodeRbac(ctx, s, f.rbac),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return append(rr, mm...).MarshalEnvoy()
|
||||
}
|
||||
|
||||
func (ux *userIndex) add(ctx context.Context, uu ...uint64) error {
|
||||
// List of filtered users
|
||||
filtered := make([]uint64, 0, len(uu))
|
||||
|
||||
for _, u := range uu {
|
||||
// not defined, we don't need
|
||||
if u == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// we have, no need
|
||||
if ux.users[u] != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
filtered = append(filtered, u)
|
||||
}
|
||||
|
||||
if len(filtered) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
users, _, err := store.SearchUsers(ctx, ux.s, types.UserFilter{
|
||||
UserID: id.Strings(filtered...),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, u := range users {
|
||||
ux.users[u.ID] = u
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
229
server/pkg/envoy/store/encoder.go
Normal file
229
server/pkg/envoy/store/encoder.go
Normal file
@@ -0,0 +1,229 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/auth"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/pkg/rbac"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
)
|
||||
|
||||
type (
|
||||
storeEncoder struct {
|
||||
s store.Storer
|
||||
dal dalService
|
||||
cfg *EncoderConfig
|
||||
|
||||
// Each resource should define its own state that is used when encoding the resource.
|
||||
// Such approach removes the need for a janky generic global state.
|
||||
// This also simplifies any slight deviations between resource complexities.
|
||||
state map[resource.Interface]resourceState
|
||||
}
|
||||
|
||||
// EncoderConfig allows us to configure the resource encoding process
|
||||
EncoderConfig struct {
|
||||
// OnExisting defines what to do if the resource exists
|
||||
OnExisting resource.MergeAlg
|
||||
// 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
|
||||
}
|
||||
|
||||
accessControlRBACServicer interface {
|
||||
Can([]uint64, string, rbac.Resource) bool
|
||||
}
|
||||
|
||||
composeAccessController interface {
|
||||
composeRecordValueAccessController
|
||||
composeRecordAccessController
|
||||
}
|
||||
composeRecordValueAccessController interface {
|
||||
CanReadRecordValueOnModuleField(context.Context, *types.ModuleField) bool
|
||||
CanUpdateRecordValueOnModuleField(context.Context, *types.ModuleField) bool
|
||||
}
|
||||
|
||||
composeRecordAccessController interface {
|
||||
CanCreateRecordOnModule(context.Context, *types.Module) bool
|
||||
CanUpdateRecord(context.Context, *types.Record) bool
|
||||
CanDeleteRecord(context.Context, *types.Record) bool
|
||||
}
|
||||
|
||||
payload struct {
|
||||
s store.Storer
|
||||
dal dalService
|
||||
state *envoy.ResourceState
|
||||
|
||||
invokerID uint64
|
||||
}
|
||||
|
||||
// resourceState allows each conforming struct to be initialized and encoded
|
||||
// by the store encoder
|
||||
resourceState interface {
|
||||
Prepare(context.Context, *payload) (err error)
|
||||
Encode(context.Context, *payload) (err error)
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnknownResource = fmt.Errorf("unknown resource")
|
||||
ErrResourceStateUndefined = fmt.Errorf("undefined resource state")
|
||||
)
|
||||
|
||||
// NewStoreEncoder initializes a fresh store encoder
|
||||
//
|
||||
// If no config is provided, it uses Skip as the default merge alg.
|
||||
func NewStoreEncoder(s store.Storer, dal dalService, cfg *EncoderConfig) envoy.PrepareEncoder {
|
||||
if cfg == nil {
|
||||
cfg = &EncoderConfig{
|
||||
OnExisting: resource.Skip,
|
||||
}
|
||||
}
|
||||
|
||||
return &storeEncoder{
|
||||
s: s,
|
||||
dal: dal,
|
||||
cfg: cfg,
|
||||
|
||||
state: 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 (se *storeEncoder) Prepare(ctx context.Context, ee ...*envoy.ResourceState) (err error) {
|
||||
f := func(rs resourceState, ers *envoy.ResourceState) error {
|
||||
if rs == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = rs.Prepare(ctx, se.makePayload(ctx, se.s, se.dal, ers))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
se.state[ers.Res] = rs
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, ers := range ee {
|
||||
// Skip placeholders
|
||||
if ers.Res.Placeholder() {
|
||||
continue
|
||||
}
|
||||
|
||||
switch res := ers.Res.(type) {
|
||||
// Compose resources
|
||||
case *resource.ComposeNamespace:
|
||||
err = f(newComposeNamespaceFromResource(res, se.cfg), ers)
|
||||
case *resource.ComposeModule:
|
||||
err = f(NewComposeModuleFromResource(res, se.cfg), ers)
|
||||
case *resource.ComposeRecord:
|
||||
err = f(NewComposeRecordFromResource(res, se.cfg), ers)
|
||||
case *resource.ComposeChart:
|
||||
err = f(newComposeChartFromResource(res, se.cfg), ers)
|
||||
case *resource.ComposePage:
|
||||
err = f(newComposePageFromResource(res, se.cfg), ers)
|
||||
|
||||
// System resources
|
||||
case *resource.User:
|
||||
err = f(NewUserFromResource(res, se.cfg), ers)
|
||||
case *resource.Template:
|
||||
err = f(NewTemplateFromResource(res, se.cfg), ers)
|
||||
case *resource.Role:
|
||||
err = f(NewRoleFromResource(res, se.cfg), ers)
|
||||
case *resource.Application:
|
||||
err = f(NewApplicationFromResource(res, se.cfg), ers)
|
||||
case *resource.APIGateway:
|
||||
err = f(newAPIGatewayFromResource(res, se.cfg), ers)
|
||||
case *resource.Report:
|
||||
err = f(newReportFromResource(res, se.cfg), ers)
|
||||
case *resource.Setting:
|
||||
err = f(NewSettingFromResource(res, se.cfg), ers)
|
||||
case *resource.RbacRule:
|
||||
err = f(newRbacRuleFromResource(res, se.cfg), ers)
|
||||
case *resource.ResourceTranslation:
|
||||
err = f(newResourceTranslationFromResource(res, se.cfg), ers)
|
||||
|
||||
// Automation resources
|
||||
case *resource.AutomationWorkflow:
|
||||
err = f(newAutomationWorkflowFromResource(res, se.cfg), ers)
|
||||
|
||||
default:
|
||||
err = ErrUnknownResource
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return se.WrapError("prepare", ers.Res, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Encode encodes available resource states using the given store encoder
|
||||
func (se *storeEncoder) Encode(ctx context.Context, p envoy.Provider) error {
|
||||
var ers *envoy.ResourceState
|
||||
return store.Tx(ctx, se.s, func(ctx context.Context, s store.Storer) (err error) {
|
||||
for {
|
||||
ers, err = p.NextInverted(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ers == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Skip placeholders
|
||||
if ers.Res.Placeholder() {
|
||||
continue
|
||||
}
|
||||
|
||||
state := se.state[ers.Res]
|
||||
if state == nil {
|
||||
err = ErrResourceStateUndefined
|
||||
} else {
|
||||
if state != nil {
|
||||
err = state.Encode(ctx, se.makePayload(ctx, s, se.dal, ers))
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return se.WrapError("encode", ers.Res, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (se *storeEncoder) makePayload(ctx context.Context, s store.Storer, dal dalService, ers *envoy.ResourceState) *payload {
|
||||
return &payload{
|
||||
s: s,
|
||||
dal: dal,
|
||||
state: ers,
|
||||
invokerID: auth.GetIdentityFromContext(ctx).Identity(),
|
||||
}
|
||||
}
|
||||
|
||||
func (se *storeEncoder) WrapError(act string, res resource.Interface, err error) error {
|
||||
if ww, ok := res.(resource.ErrorWrapper); ok {
|
||||
err = ww.WrapError(act, err)
|
||||
} else {
|
||||
err = fmt.Errorf("%s %s %v: %s", act, res.ResourceType(), res.Identifiers().StringSlice(), err)
|
||||
}
|
||||
|
||||
return fmt.Errorf("store encoder %s", err)
|
||||
}
|
||||
|
||||
func resourceErrIdentifierNotUnique(i string) error {
|
||||
return fmt.Errorf("bad resource identifier %v: not unique", i)
|
||||
}
|
||||
30
server/pkg/envoy/store/rbac_rule.go
Normal file
30
server/pkg/envoy/store/rbac_rule.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"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 {
|
||||
cfg *EncoderConfig
|
||||
|
||||
rule *rbac.Rule
|
||||
|
||||
// point to the rbac rule
|
||||
refRbacResource string
|
||||
refRbacRes *resource.Ref
|
||||
|
||||
refPathRes []*resource.Ref
|
||||
|
||||
refRole *resource.Ref
|
||||
role *types.Role
|
||||
}
|
||||
)
|
||||
|
||||
func rbacResourceErrUnidentifiable(ii resource.Identifiers) error {
|
||||
return fmt.Errorf("rbac resource unidentifiable %v", ii.StringSlice())
|
||||
}
|
||||
397
server/pkg/envoy/store/rbac_rule_marshal.go
Normal file
397
server/pkg/envoy/store/rbac_rule_marshal.go
Normal file
@@ -0,0 +1,397 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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/resource"
|
||||
"github.com/cortezaproject/corteza/server/pkg/rbac"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
systemTypes "github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
var (
|
||||
gRbacRules map[string]bool
|
||||
)
|
||||
|
||||
func newRbacRuleFromResource(res *resource.RbacRule, cfg *EncoderConfig) resourceState {
|
||||
return &rbacRule{
|
||||
cfg: mergeConfig(cfg, res.Config()),
|
||||
|
||||
rule: res.Res,
|
||||
|
||||
refRbacResource: res.RefResource,
|
||||
refRbacRes: res.RefRes,
|
||||
|
||||
refPathRes: res.RefPath,
|
||||
|
||||
refRole: res.RefRole,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *rbacRule) Prepare(ctx context.Context, pl *payload) (err error) {
|
||||
// Init global state
|
||||
if gRbacRules == nil {
|
||||
err = n.initGlobalIndex(ctx, pl.s)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Related role
|
||||
n.role, err = findRole(ctx, pl.s, pl.state.ParentResources, n.refRole.Identifiers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n.role == nil {
|
||||
return resource.RoleErrUnresolved(n.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(pl.state.ParentResources); !found {
|
||||
return resource.RbacResourceErrNotFound(refRes.Identifiers)
|
||||
}
|
||||
|
||||
case composeTypes.ModuleFieldResourceType:
|
||||
if found = n.handleComposeModuleField(pl.state.ParentResources); !found {
|
||||
return resource.RbacResourceErrNotFound(refRes.Identifiers)
|
||||
}
|
||||
|
||||
default:
|
||||
for _, r := range pl.state.ParentResources {
|
||||
if refRes.ResourceType == r.ResourceType() && r.Identifiers().HasAny(refRes.Identifiers) {
|
||||
return
|
||||
}
|
||||
}
|
||||
// We couldn't find it...
|
||||
return resource.RbacResourceErrNotFound(refRes.Identifiers)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (n *rbacRule) Encode(ctx context.Context, pl *payload) (err error) {
|
||||
// Assure correct resource
|
||||
n.rule.Resource, err = n.makeRBACResource(pl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Assure correct role
|
||||
n.rule.RoleID = n.role.ID
|
||||
if n.rule.RoleID == 0 {
|
||||
rl := resource.FindRole(pl.state.ParentResources, n.refRole.Identifiers)
|
||||
n.rule.RoleID = rl.ID
|
||||
}
|
||||
if n.rule.RoleID == 0 {
|
||||
return resource.RoleErrUnresolved(n.refRole.Identifiers)
|
||||
}
|
||||
|
||||
// Upsert
|
||||
if _, exists := gRbacRules[n.rbacRuleIndex((n.rule))]; !exists {
|
||||
return store.CreateRbacRule(ctx, pl.s, n.rule)
|
||||
}
|
||||
|
||||
// On existing rbac rule, replace/merge right basically overwrites the existing rule;
|
||||
// otherwise, the new rule is ignored.
|
||||
switch n.cfg.OnExisting {
|
||||
case resource.Replace,
|
||||
resource.MergeRight:
|
||||
return store.UpdateRbacRule(ctx, pl.s, n.rule)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *rbacRule) rbacRuleIndex(r *rbac.Rule) string {
|
||||
return fmt.Sprintf("%d_%s_%s", r.RoleID, r.Resource, r.Operation)
|
||||
}
|
||||
|
||||
func (n *rbacRule) initGlobalIndex(ctx context.Context, s store.Storer) error {
|
||||
gRbacRules = make(map[string]bool)
|
||||
|
||||
rr, _, err := store.SearchRbacRules(ctx, s, rbac.RuleFilter{})
|
||||
if err != store.ErrNotFound && err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, r := range rr {
|
||||
gRbacRules[n.rbacRuleIndex(r)] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *rbacRule) makeRBACResource(pl *payload) (string, error) {
|
||||
p0ID := uint64(0)
|
||||
p1ID := uint64(0)
|
||||
p2ID := uint64(0)
|
||||
|
||||
rt := strings.Split(n.refRbacResource, "/")[0]
|
||||
|
||||
// Component level stuff
|
||||
switch rt {
|
||||
case composeTypes.ComponentResourceType:
|
||||
return composeTypes.ComponentRbacResource(), nil
|
||||
case systemTypes.ComponentResourceType:
|
||||
return systemTypes.ComponentRbacResource(), nil
|
||||
case automationTypes.ComponentResourceType:
|
||||
return automationTypes.ComponentRbacResource(), nil
|
||||
case federationTypes.ComponentResourceType:
|
||||
return federationTypes.ComponentRbacResource(), nil
|
||||
}
|
||||
|
||||
// Component resource stuff
|
||||
switch rt {
|
||||
case automationTypes.WorkflowResourceType:
|
||||
if n.refRbacRes != nil {
|
||||
p1 := resource.FindAutomationWorkflow(pl.state.ParentResources, n.refRbacRes.Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.AutomationWorkflowErrUnresolved(n.refRbacRes.Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
}
|
||||
return automationTypes.WorkflowRbacResource(p1ID), nil
|
||||
|
||||
case composeTypes.NamespaceResourceType:
|
||||
if n.refRbacRes != nil {
|
||||
p1 := resource.FindComposeNamespace(pl.state.ParentResources, n.refRbacRes.Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.ComposeNamespaceErrUnresolved(n.refRbacRes.Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
}
|
||||
return composeTypes.NamespaceRbacResource(p1ID), nil
|
||||
|
||||
case composeTypes.ModuleResourceType:
|
||||
if len(n.refPathRes) > 0 {
|
||||
p0 := resource.FindComposeNamespace(pl.state.ParentResources, n.refPathRes[0].Identifiers)
|
||||
if p0 == nil {
|
||||
return "", resource.ComposeNamespaceErrUnresolved(n.refPathRes[0].Identifiers)
|
||||
}
|
||||
p0ID = p0.ID
|
||||
}
|
||||
|
||||
if n.refRbacRes != nil {
|
||||
p1 := resource.FindComposeModule(pl.state.ParentResources, n.refRbacRes.Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.ComposeModuleErrUnresolved(n.refRbacRes.Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
}
|
||||
return composeTypes.ModuleRbacResource(p0ID, p1ID), nil
|
||||
|
||||
case composeTypes.ChartResourceType:
|
||||
if len(n.refPathRes) > 0 {
|
||||
p0 := resource.FindComposeNamespace(pl.state.ParentResources, n.refPathRes[0].Identifiers)
|
||||
if p0 == nil {
|
||||
return "", resource.ComposeNamespaceErrUnresolved(n.refPathRes[0].Identifiers)
|
||||
}
|
||||
p0ID = p0.ID
|
||||
}
|
||||
|
||||
if n.refRbacRes != nil {
|
||||
p1 := resource.FindComposeChart(pl.state.ParentResources, n.refRbacRes.Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.ComposeChartErrUnresolved(n.refRbacRes.Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
}
|
||||
return composeTypes.ChartRbacResource(p0ID, p1ID), nil
|
||||
|
||||
case composeTypes.PageResourceType:
|
||||
if len(n.refPathRes) > 0 {
|
||||
p0 := resource.FindComposeNamespace(pl.state.ParentResources, n.refPathRes[0].Identifiers)
|
||||
if p0 == nil {
|
||||
return "", resource.ComposeNamespaceErrUnresolved(n.refPathRes[0].Identifiers)
|
||||
}
|
||||
p0ID = p0.ID
|
||||
}
|
||||
|
||||
if n.refRbacRes != nil {
|
||||
p1 := resource.FindComposePage(pl.state.ParentResources, n.refRbacRes.Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.ComposePageErrUnresolved(n.refRbacRes.Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
}
|
||||
return composeTypes.PageRbacResource(p0ID, p1ID), nil
|
||||
|
||||
case composeTypes.RecordResourceType:
|
||||
if len(n.refPathRes) > 0 {
|
||||
p0 := resource.FindComposeNamespace(pl.state.ParentResources, n.refPathRes[0].Identifiers)
|
||||
if p0 == nil {
|
||||
return "", resource.ComposeNamespaceErrUnresolved(n.refPathRes[0].Identifiers)
|
||||
}
|
||||
p0ID = p0.ID
|
||||
}
|
||||
if len(n.refPathRes) > 1 {
|
||||
p1 := resource.FindComposeModule(pl.state.ParentResources, n.refPathRes[1].Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.ComposeModuleErrUnresolved(n.refPathRes[1].Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
}
|
||||
if n.refRbacRes != nil {
|
||||
ref := n.refPathRes[1]
|
||||
|
||||
p2 := resource.FindComposeRecordResource(pl.state.ParentResources, ref.Identifiers)
|
||||
if p2 == nil {
|
||||
return "", resource.ComposeRecordErrUnresolved(n.refRbacRes.Identifiers)
|
||||
}
|
||||
|
||||
for _, i := range n.refRbacRes.Identifiers {
|
||||
if p2ID = p2.IDMap[i]; p2ID > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if p2ID == 0 {
|
||||
return "", resource.ComposeRecordErrUnresolved(n.refRbacRes.Identifiers)
|
||||
}
|
||||
}
|
||||
return composeTypes.RecordRbacResource(p0ID, p1ID, p2ID), nil
|
||||
|
||||
case composeTypes.ModuleFieldResourceType:
|
||||
if len(n.refPathRes) > 0 {
|
||||
p0 := resource.FindComposeNamespace(pl.state.ParentResources, n.refPathRes[0].Identifiers)
|
||||
if p0 == nil {
|
||||
return "", resource.ComposeNamespaceErrUnresolved(n.refPathRes[0].Identifiers)
|
||||
}
|
||||
p0ID = p0.ID
|
||||
}
|
||||
if len(n.refPathRes) > 1 {
|
||||
p1 := resource.FindComposeModule(pl.state.ParentResources, n.refPathRes[1].Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.ComposeModuleErrUnresolved(n.refPathRes[1].Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
}
|
||||
|
||||
if n.refRbacRes != nil {
|
||||
modRef := n.refPathRes[1]
|
||||
p2 := resource.FindComposeModuleField(pl.state.ParentResources, modRef.Identifiers, n.refRbacRes.Identifiers)
|
||||
if p2 == nil {
|
||||
return "", resource.ComposeModuleFieldErrUnresolved(n.refRbacRes.Identifiers)
|
||||
}
|
||||
|
||||
p2ID = p2.ID
|
||||
}
|
||||
|
||||
return composeTypes.ModuleFieldRbacResource(p0ID, p1ID, p2ID), nil
|
||||
|
||||
case systemTypes.UserResourceType:
|
||||
if n.refRbacRes != nil {
|
||||
p1 := resource.FindUser(pl.state.ParentResources, n.refRbacRes.Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.UserErrUnresolved(n.refRbacRes.Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
}
|
||||
return systemTypes.UserRbacResource(p1ID), nil
|
||||
|
||||
case systemTypes.RoleResourceType:
|
||||
if n.refRbacRes != nil {
|
||||
p1 := resource.FindRole(pl.state.ParentResources, n.refRbacRes.Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.RoleErrUnresolved(n.refRbacRes.Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
}
|
||||
return systemTypes.RoleRbacResource(p1ID), nil
|
||||
|
||||
case systemTypes.ApplicationResourceType:
|
||||
if n.refRbacRes != nil {
|
||||
p1 := resource.FindApplication(pl.state.ParentResources, n.refRbacRes.Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.ApplicationErrUnresolved(n.refRbacRes.Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
}
|
||||
return systemTypes.ApplicationRbacResource(p1ID), nil
|
||||
|
||||
case systemTypes.ApigwRouteResourceType:
|
||||
if n.refRbacRes != nil {
|
||||
p1 := resource.FindAPIGateway(pl.state.ParentResources, n.refRbacRes.Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.APIGatewayErrUnresolved(n.refRbacRes.Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
}
|
||||
return systemTypes.ApigwRouteRbacResource(p1ID), nil
|
||||
|
||||
case systemTypes.AuthClientResourceType:
|
||||
// @todo add support for importing rbac rules for specific client
|
||||
return systemTypes.AuthClientRbacResource(p1ID), nil
|
||||
|
||||
case systemTypes.TemplateResourceType:
|
||||
if n.refRbacRes != nil {
|
||||
p1 := resource.FindTemplate(pl.state.ParentResources, n.refRbacRes.Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.TemplateErrUnresolved(n.refRbacRes.Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
}
|
||||
return systemTypes.TemplateRbacResource(p1ID), nil
|
||||
|
||||
case systemTypes.ReportResourceType:
|
||||
if n.refRbacRes != nil {
|
||||
p1 := resource.FindReport(pl.state.ParentResources, n.refRbacRes.Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.ReportErrUnresolved(n.refRbacRes.Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
}
|
||||
return systemTypes.ReportRbacResource(p1ID), nil
|
||||
|
||||
case systemTypes.QueueResourceType:
|
||||
// @todo add support for importing rbac rules for specific queue
|
||||
return systemTypes.QueueRbacResource(p1ID), nil
|
||||
|
||||
case systemTypes.DataPrivacyRequestResourceType:
|
||||
return systemTypes.DataPrivacyRequestRbacResource(p1ID), nil
|
||||
|
||||
case federationTypes.NodeResourceType:
|
||||
return federationTypes.NodeRbacResource(p1ID), nil
|
||||
case federationTypes.SharedModuleResourceType:
|
||||
return federationTypes.SharedModuleRbacResource(p0ID, p1ID), nil
|
||||
case federationTypes.ExposedModuleResourceType:
|
||||
return federationTypes.ExposedModuleRbacResource(p0ID, p1ID), nil
|
||||
}
|
||||
|
||||
// @todo if we wish to support rbac for external stuff, this needs to pass through.
|
||||
// this also requires some tweaks in the path ID thing.
|
||||
return "", fmt.Errorf("unsupported resource type '%s' for RBAC store encode", rt)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
32
server/pkg/envoy/store/rbac_rule_unmarshal.go
Normal file
32
server/pkg/envoy/store/rbac_rule_unmarshal.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/pkg/rbac"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
func newRbacRule(rl *rbac.Rule) (*rbacRule, error) {
|
||||
res := rl.Resource
|
||||
_, ref, pp, err := resource.ParseRule(res)
|
||||
|
||||
return &rbacRule{
|
||||
rule: rl,
|
||||
|
||||
refRbacResource: res,
|
||||
refRbacRes: ref,
|
||||
|
||||
refPathRes: pp,
|
||||
|
||||
refRole: resource.MakeRef(types.RoleResourceType, resource.MakeIdentifiers(strconv.FormatUint(rl.RoleID, 10))),
|
||||
}, err
|
||||
}
|
||||
|
||||
func (rl *rbacRule) MarshalEnvoy() ([]resource.Interface, error) {
|
||||
return envoy.CollectNodes(
|
||||
resource.NewRbacRule(rl.rule, rl.refRole, rl.refRbacRes, rl.refRbacResource, rl.refPathRes...),
|
||||
)
|
||||
}
|
||||
115
server/pkg/envoy/store/report.go
Normal file
115
server/pkg/envoy/store/report.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
report struct {
|
||||
cfg *EncoderConfig
|
||||
|
||||
res *resource.Report
|
||||
rp *types.Report
|
||||
ss types.ReportDataSourceSet
|
||||
pp types.ReportBlockSet
|
||||
|
||||
ux *userIndex
|
||||
}
|
||||
reportSet []*report
|
||||
|
||||
reportSource struct {
|
||||
cfg *EncoderConfig
|
||||
|
||||
res *resource.ReportSource
|
||||
tr *types.ReportDataSource
|
||||
}
|
||||
reportSourceSet []*reportSource
|
||||
)
|
||||
|
||||
// mergeReports merges b into a, prioritising a
|
||||
func mergeReports(a, b *types.Report) *types.Report {
|
||||
c := a
|
||||
|
||||
if c.Handle == "" {
|
||||
c.Handle = b.Handle
|
||||
}
|
||||
if c.Meta == nil {
|
||||
c.Meta = b.Meta
|
||||
}
|
||||
if c.Sources == nil {
|
||||
c.Sources = b.Sources
|
||||
}
|
||||
if c.Blocks == nil {
|
||||
c.Blocks = b.Blocks
|
||||
}
|
||||
|
||||
if c.OwnedBy == 0 {
|
||||
c.OwnedBy = b.OwnedBy
|
||||
}
|
||||
if c.CreatedBy == 0 {
|
||||
c.CreatedBy = b.CreatedBy
|
||||
}
|
||||
if c.UpdatedBy == 0 {
|
||||
c.UpdatedBy = b.UpdatedBy
|
||||
}
|
||||
if c.DeletedBy == 0 {
|
||||
c.DeletedBy = b.DeletedBy
|
||||
}
|
||||
|
||||
if c.CreatedAt.IsZero() {
|
||||
c.CreatedAt = b.CreatedAt
|
||||
}
|
||||
|
||||
if c.UpdatedAt == nil {
|
||||
c.UpdatedAt = b.UpdatedAt
|
||||
}
|
||||
|
||||
if c.DeletedAt == nil {
|
||||
c.DeletedAt = b.DeletedAt
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// findReport looks for the report in the resources & the store
|
||||
//
|
||||
// Provided resources are prioritized.
|
||||
func findReport(ctx context.Context, s store.Storer, rr resource.InterfaceSet, ii resource.Identifiers) (wf *types.Report, err error) {
|
||||
wf = resource.FindReport(rr, ii)
|
||||
if wf != nil {
|
||||
return wf, nil
|
||||
}
|
||||
|
||||
return findReportStore(ctx, s, makeGenericFilter(ii))
|
||||
}
|
||||
|
||||
// findReportStore looks for the report in the store
|
||||
func findReportStore(ctx context.Context, s store.Storer, gf genericFilter) (wf *types.Report, err error) {
|
||||
if gf.id > 0 {
|
||||
wf, err = store.LookupReportByID(ctx, s, gf.id)
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if wf != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, i := range gf.identifiers {
|
||||
wf, err = store.LookupReportByHandle(ctx, s, i)
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if wf != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
126
server/pkg/envoy/store/report_marshal.go
Normal file
126
server/pkg/envoy/store/report_marshal.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
func newReportFromResource(res *resource.Report, cfg *EncoderConfig) resourceState {
|
||||
return &report{
|
||||
cfg: mergeConfig(cfg, res.Config()),
|
||||
|
||||
res: res,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *report) Prepare(ctx context.Context, pl *payload) (err error) {
|
||||
// Reset old identifiers
|
||||
n.res.Res.ID = 0
|
||||
|
||||
// Try to get the original report
|
||||
n.rp, err = findReportStore(ctx, pl.s, makeGenericFilter(n.res.Identifiers()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if n.rp != nil {
|
||||
n.res.Res.ID = n.rp.ID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *report) Encode(ctx context.Context, pl *payload) (err error) {
|
||||
res := n.res.Res
|
||||
exists := n.rp != nil && n.rp.ID > 0
|
||||
|
||||
// Determine the ID
|
||||
if res.ID <= 0 && exists {
|
||||
res.ID = n.rp.ID
|
||||
}
|
||||
if res.ID <= 0 {
|
||||
res.ID = NextID()
|
||||
}
|
||||
|
||||
// Sys users
|
||||
us, err := resolveUserstamps(ctx, pl.s, pl.state.ParentResources, n.res.Userstamps())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ts := n.res.Timestamps()
|
||||
if ts != nil {
|
||||
if ts.CreatedAt != nil {
|
||||
res.CreatedAt = *ts.CreatedAt.T
|
||||
} else {
|
||||
res.CreatedAt = *now()
|
||||
}
|
||||
if ts.UpdatedAt != nil {
|
||||
res.UpdatedAt = ts.UpdatedAt.T
|
||||
}
|
||||
if ts.DeletedAt != nil {
|
||||
res.DeletedAt = ts.DeletedAt.T
|
||||
}
|
||||
}
|
||||
|
||||
res.CreatedBy = pl.invokerID
|
||||
if us != nil {
|
||||
if us.OwnedBy != nil {
|
||||
res.OwnedBy = us.OwnedBy.UserID
|
||||
}
|
||||
if us.CreatedBy != nil {
|
||||
res.CreatedBy = us.CreatedBy.UserID
|
||||
}
|
||||
if us.UpdatedBy != nil {
|
||||
res.UpdatedBy = us.UpdatedBy.UserID
|
||||
}
|
||||
if us.DeletedBy != nil {
|
||||
res.DeletedBy = us.DeletedBy.UserID
|
||||
}
|
||||
}
|
||||
|
||||
res.Sources = make(types.ReportDataSourceSet, 0, 10)
|
||||
for _, rp := range n.res.Sources {
|
||||
res.Sources = append(res.Sources, rp.Res)
|
||||
}
|
||||
|
||||
res.Blocks = make(types.ReportBlockSet, 0, 10)
|
||||
for _, rp := range n.res.Blocks {
|
||||
res.Blocks = append(res.Blocks, rp.Res)
|
||||
}
|
||||
|
||||
// Evaluate the resource skip expression
|
||||
// @todo expand available parameters; similar implementation to automation/types/record@Dict
|
||||
if skip, err := basicSkipEval(ctx, n.cfg, !exists); err != nil {
|
||||
return err
|
||||
} else if skip {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a fresh report
|
||||
if !exists {
|
||||
return store.CreateReport(ctx, pl.s, res)
|
||||
}
|
||||
|
||||
// Update existing report
|
||||
switch n.cfg.OnExisting {
|
||||
case resource.Skip:
|
||||
return nil
|
||||
|
||||
case resource.MergeLeft:
|
||||
res = mergeReports(n.rp, res)
|
||||
|
||||
case resource.MergeRight:
|
||||
res = mergeReports(res, n.rp)
|
||||
}
|
||||
|
||||
err = store.UpdateReport(ctx, pl.s, res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.res.Res = res
|
||||
return nil
|
||||
}
|
||||
34
server/pkg/envoy/store/report_unmarshal.go
Normal file
34
server/pkg/envoy/store/report_unmarshal.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
func newReport(wf *types.Report, ux *userIndex) *report {
|
||||
return &report{
|
||||
rp: wf,
|
||||
ss: wf.Sources,
|
||||
pp: wf.Blocks,
|
||||
|
||||
ux: ux,
|
||||
}
|
||||
}
|
||||
|
||||
func (awf *report) MarshalEnvoy() ([]resource.Interface, error) {
|
||||
rs := resource.NewReport(awf.rp)
|
||||
syncUserStamps(rs.Userstamps(), awf.ux)
|
||||
|
||||
for _, s := range awf.ss {
|
||||
rs.AddReportSource(s)
|
||||
}
|
||||
|
||||
for _, p := range awf.pp {
|
||||
rs.AddReportBlock(p)
|
||||
}
|
||||
|
||||
return envoy.CollectNodes(
|
||||
rs,
|
||||
)
|
||||
}
|
||||
26
server/pkg/envoy/store/resource_translation.go
Normal file
26
server/pkg/envoy/store/resource_translation.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
resourceTranslation struct {
|
||||
cfg *EncoderConfig
|
||||
|
||||
locales types.ResourceTranslationSet
|
||||
|
||||
// point to the resource translation
|
||||
refResourceTranslation string
|
||||
refLocaleRes *resource.Ref
|
||||
|
||||
refPathRes []*resource.Ref
|
||||
}
|
||||
)
|
||||
|
||||
func resourceTranslationErrUnidentifiable(ii resource.Identifiers) error {
|
||||
return fmt.Errorf("resource translation unidentifiable %v", ii.StringSlice())
|
||||
}
|
||||
250
server/pkg/envoy/store/resource_translation_marshal.go
Normal file
250
server/pkg/envoy/store/resource_translation_marshal.go
Normal file
@@ -0,0 +1,250 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
composeTypes "github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
var (
|
||||
gResourceTranslations map[language.Tag]map[string]*types.ResourceTranslation
|
||||
|
||||
resourceTranslationIndex = func(l *types.ResourceTranslation) string {
|
||||
return fmt.Sprintf("%s/%s", l.Resource, l.K)
|
||||
}
|
||||
)
|
||||
|
||||
func newResourceTranslationFromResource(res *resource.ResourceTranslation, cfg *EncoderConfig) resourceState {
|
||||
return &resourceTranslation{
|
||||
cfg: mergeConfig(cfg, res.Config()),
|
||||
|
||||
locales: res.Res,
|
||||
|
||||
refResourceTranslation: res.Res[0].Resource,
|
||||
refLocaleRes: res.RefRes,
|
||||
|
||||
refPathRes: res.RefPath,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *resourceTranslation) Prepare(ctx context.Context, pl *payload) (err error) {
|
||||
// Init global state
|
||||
if gResourceTranslations == nil {
|
||||
err = n.initGlobalIndex(ctx, pl.s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// For now we will only allow resource specific locale resources if that resource is
|
||||
// also present.
|
||||
// Here we check if we can find it in case we're handling a resource specific rule.
|
||||
refRes := n.refLocaleRes
|
||||
if refRes.ResourceType == composeTypes.ModuleFieldResourceType {
|
||||
for _, r := range pl.state.ParentResources {
|
||||
if r.ResourceType() == composeTypes.ModuleResourceType && r.Identifiers().HasAny(n.refPathRes[1].Identifiers) {
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, r := range pl.state.ParentResources {
|
||||
if refRes.ResourceType == r.ResourceType() && r.Identifiers().HasAny(refRes.Identifiers) {
|
||||
return
|
||||
}
|
||||
}
|
||||
// We couldn't find it...
|
||||
return resource.ResourceTranslationErrNotFound(refRes.Identifiers)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (n *resourceTranslation) Encode(ctx context.Context, pl *payload) (err error) {
|
||||
|
||||
// @todo move out of the encoding logic
|
||||
if n == nil {
|
||||
return
|
||||
}
|
||||
|
||||
localeRes, err := n.makeResourceTranslation(pl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Assure correct resources
|
||||
for _, l := range n.locales {
|
||||
l.Resource = localeRes
|
||||
}
|
||||
|
||||
ll := make(types.ResourceTranslationSet, 0, len(n.locales))
|
||||
for _, l := range n.locales {
|
||||
if l.Message == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
old, ok := gResourceTranslations[l.Lang.Tag][resourceTranslationIndex(l)]
|
||||
|
||||
// not exist
|
||||
if !ok {
|
||||
l.ID = NextID()
|
||||
l.CreatedAt = *now()
|
||||
l.CreatedBy = pl.invokerID
|
||||
|
||||
ll = append(ll, l)
|
||||
continue
|
||||
}
|
||||
|
||||
// exists
|
||||
switch n.cfg.OnExisting {
|
||||
case resource.Skip,
|
||||
resource.MergeLeft:
|
||||
continue
|
||||
|
||||
case resource.MergeRight,
|
||||
resource.Default:
|
||||
old.Message = l.Message
|
||||
|
||||
ll = append(ll, old)
|
||||
}
|
||||
}
|
||||
|
||||
// Update global index for following runs
|
||||
n.updateGlobalIndex(ll)
|
||||
|
||||
return store.UpsertResourceTranslation(ctx, pl.s, ll...)
|
||||
}
|
||||
|
||||
func (n *resourceTranslation) makeResourceTranslation(pl *payload) (string, error) {
|
||||
p0ID := uint64(0)
|
||||
p1ID := uint64(0)
|
||||
p2ID := uint64(0)
|
||||
|
||||
localeRes := ""
|
||||
_ = localeRes
|
||||
|
||||
switch n.refLocaleRes.ResourceType {
|
||||
// case automationTypes.WorkflowResourceType:
|
||||
// p1 := resource.FindAutomationWorkflow(pl.state.ParentResources, n.refLocaleRes.Identifiers)
|
||||
// if p1 == nil {
|
||||
// return "", resource.AutomationWorkflowErrUnresolved(n.refLocaleRes.Identifiers)
|
||||
// }
|
||||
// p1ID = p1.ID
|
||||
|
||||
// return automationTypes.WorkflowResourceTranslation(p1ID), nil
|
||||
|
||||
case composeTypes.NamespaceResourceType:
|
||||
p1 := resource.FindComposeNamespace(pl.state.ParentResources, n.refLocaleRes.Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.ComposeNamespaceErrUnresolved(n.refLocaleRes.Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
|
||||
return composeTypes.NamespaceResourceTranslation(p1ID), nil
|
||||
|
||||
case composeTypes.ModuleResourceType:
|
||||
p0 := resource.FindComposeNamespace(pl.state.ParentResources, n.refPathRes[0].Identifiers)
|
||||
if p0 == nil {
|
||||
return "", resource.ComposeNamespaceErrUnresolved(n.refPathRes[0].Identifiers)
|
||||
}
|
||||
p0ID = p0.ID
|
||||
|
||||
p1 := resource.FindComposeModule(pl.state.ParentResources, n.refLocaleRes.Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.ComposeModuleErrUnresolved(n.refLocaleRes.Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
|
||||
return composeTypes.ModuleResourceTranslation(p0ID, p1ID), nil
|
||||
|
||||
case composeTypes.PageResourceType:
|
||||
p0 := resource.FindComposeNamespace(pl.state.ParentResources, n.refPathRes[0].Identifiers)
|
||||
if p0 == nil {
|
||||
return "", resource.ComposeNamespaceErrUnresolved(n.refPathRes[0].Identifiers)
|
||||
}
|
||||
p0ID = p0.ID
|
||||
|
||||
p1 := resource.FindComposePage(pl.state.ParentResources, n.refLocaleRes.Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.ComposePageErrUnresolved(n.refLocaleRes.Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
|
||||
return composeTypes.PageResourceTranslation(p0ID, p1ID), nil
|
||||
|
||||
case composeTypes.ChartResourceType:
|
||||
p0 := resource.FindComposeNamespace(pl.state.ParentResources, n.refPathRes[0].Identifiers)
|
||||
if p0 == nil {
|
||||
return "", resource.ComposeNamespaceErrUnresolved(n.refPathRes[0].Identifiers)
|
||||
}
|
||||
p0ID = p0.ID
|
||||
|
||||
p1 := resource.FindComposeChart(pl.state.ParentResources, n.refLocaleRes.Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.ComposeChartErrUnresolved(n.refLocaleRes.Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
|
||||
return composeTypes.ChartResourceTranslation(p0ID, p1ID), nil
|
||||
|
||||
case composeTypes.ModuleFieldResourceType:
|
||||
p0 := resource.FindComposeNamespace(pl.state.ParentResources, n.refPathRes[0].Identifiers)
|
||||
if p0 == nil {
|
||||
return "", resource.ComposeNamespaceErrUnresolved(n.refPathRes[0].Identifiers)
|
||||
}
|
||||
p0ID = p0.ID
|
||||
|
||||
p1 := resource.FindComposeModule(pl.state.ParentResources, n.refPathRes[1].Identifiers)
|
||||
if p1 == nil {
|
||||
return "", resource.ComposeModuleErrUnresolved(n.refPathRes[1].Identifiers)
|
||||
}
|
||||
p1ID = p1.ID
|
||||
|
||||
// field
|
||||
f := resource.FindComposeModuleField(pl.state.ParentResources, n.refPathRes[1].Identifiers, n.refLocaleRes.Identifiers)
|
||||
if f == nil {
|
||||
return "", resource.ComposeModuleFieldErrUnresolved(n.refLocaleRes.Identifiers)
|
||||
}
|
||||
p2ID = f.ID
|
||||
|
||||
return composeTypes.ModuleFieldResourceTranslation(p0ID, p1ID, p2ID), nil
|
||||
|
||||
// case types.ReportResourceType:
|
||||
// p1 := resource.FindReport(pl.state.ParentResources, n.refLocaleRes.Identifiers)
|
||||
// if p1 == nil {
|
||||
// return "", resource.ReportErrUnresolved(n.refLocaleRes.Identifiers)
|
||||
// }
|
||||
// p1ID = p1.ID
|
||||
|
||||
// return types.ReportResourceTranslation(p1ID), nil
|
||||
|
||||
default:
|
||||
// @todo if we wish to support res. trans. for external stuff, this needs to pass through.
|
||||
// this also requires some tweaks in the path ID thing.
|
||||
return "", fmt.Errorf("unsupported resource type '%s' for resource translation store encode", n.refLocaleRes.ResourceType)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *resourceTranslation) initGlobalIndex(ctx context.Context, s store.Storer) (err error) {
|
||||
gResourceTranslations = make(map[language.Tag]map[string]*types.ResourceTranslation)
|
||||
ll, _, err := store.SearchResourceTranslations(ctx, s, types.ResourceTranslationFilter{})
|
||||
if err != store.ErrNotFound && err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.updateGlobalIndex(ll)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *resourceTranslation) updateGlobalIndex(ll types.ResourceTranslationSet) {
|
||||
for _, l := range ll {
|
||||
if _, ok := gResourceTranslations[l.Lang.Tag]; !ok {
|
||||
gResourceTranslations[l.Lang.Tag] = make(map[string]*types.ResourceTranslation)
|
||||
}
|
||||
gResourceTranslations[l.Lang.Tag][resourceTranslationIndex(l)] = l
|
||||
}
|
||||
}
|
||||
27
server/pkg/envoy/store/resource_translation_unmarshal.go
Normal file
27
server/pkg/envoy/store/resource_translation_unmarshal.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
func newResourceTranslation(l types.ResourceTranslationSet) (*resourceTranslation, error) {
|
||||
res := l[0].Resource
|
||||
_, ref, pp, err := resource.ParseResourceTranslation(res)
|
||||
|
||||
return &resourceTranslation{
|
||||
locales: l,
|
||||
|
||||
refResourceTranslation: res,
|
||||
refLocaleRes: ref,
|
||||
|
||||
refPathRes: pp,
|
||||
}, err
|
||||
}
|
||||
|
||||
func (lr *resourceTranslation) MarshalEnvoy() ([]resource.Interface, error) {
|
||||
return envoy.CollectNodes(
|
||||
resource.NewResourceTranslation(lr.locales, lr.refResourceTranslation, lr.refLocaleRes, lr.refPathRes...),
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user