From 69b1ac9d01c3f00207e950fde043d18139b33459 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=C5=BE=20Jerman?= Date: Fri, 12 Mar 2021 11:45:06 +0100 Subject: [PATCH] Add support for workflow import/export --- pkg/envoy/json/util_encode.go | 1 + pkg/envoy/resource/automation_workflow.go | 139 +++++++++ pkg/envoy/resource/base.go | 7 +- pkg/envoy/resource/types.go | 26 +- pkg/envoy/store/application.go | 2 + pkg/envoy/store/application_unmarshal.go | 8 +- pkg/envoy/store/automation.go | 153 ++++++++++ pkg/envoy/store/automation_workflow.go | 191 ++++++++++++ .../store/automation_workflow_marshal.go | 236 ++++++++++++++ .../store/automation_workflow_unmarshal.go | 39 +++ pkg/envoy/store/decoder.go | 73 ++++- pkg/envoy/store/encoder.go | 2 + pkg/envoy/store/setting.go | 2 + pkg/envoy/store/setting_unmarshal.go | 9 +- pkg/envoy/store/system.go | 60 +++- pkg/envoy/store/util.go | 19 ++ pkg/envoy/yaml/automation.go | 64 ++++ pkg/envoy/yaml/automation_workflow.go | 63 ++++ pkg/envoy/yaml/automation_workflow_marshal.go | 155 ++++++++++ pkg/envoy/yaml/automation_workflow_test.go | 39 +++ .../yaml/automation_workflow_unmarshal.go | 289 ++++++++++++++++++ pkg/envoy/yaml/compose_record_marshal.go | 9 +- pkg/envoy/yaml/document.go | 33 +- pkg/envoy/yaml/encoder.go | 3 + pkg/envoy/yaml/stamps.go | 4 + pkg/envoy/yaml/testdata/workflow_1.yaml | 75 +++++ pkg/envoy/yaml/util_encode.go | 1 + tests/envoy/automation.go | 88 ++++++ tests/envoy/main.go | 3 + tests/envoy/store_yaml_base_test.go | 75 +++-- tests/envoy/testdata/base/workflows.yaml | 103 +++++++ tests/envoy/yaml_store_base_test.go | 38 ++- 32 files changed, 1958 insertions(+), 51 deletions(-) create mode 100644 pkg/envoy/resource/automation_workflow.go create mode 100644 pkg/envoy/store/automation.go create mode 100644 pkg/envoy/store/automation_workflow.go create mode 100644 pkg/envoy/store/automation_workflow_marshal.go create mode 100644 pkg/envoy/store/automation_workflow_unmarshal.go create mode 100644 pkg/envoy/yaml/automation.go create mode 100644 pkg/envoy/yaml/automation_workflow.go create mode 100644 pkg/envoy/yaml/automation_workflow_marshal.go create mode 100644 pkg/envoy/yaml/automation_workflow_test.go create mode 100644 pkg/envoy/yaml/automation_workflow_unmarshal.go create mode 100644 pkg/envoy/yaml/testdata/workflow_1.yaml create mode 100644 tests/envoy/automation.go create mode 100644 tests/envoy/testdata/base/workflows.yaml diff --git a/pkg/envoy/json/util_encode.go b/pkg/envoy/json/util_encode.go index a440e6f01..c670a74df 100644 --- a/pkg/envoy/json/util_encode.go +++ b/pkg/envoy/json/util_encode.go @@ -38,6 +38,7 @@ func mapUserstamps(n mapNode, us *resource.Userstamps) (mapNode, error) { "updatedBy", us.UpdatedBy, "deletedBy", us.DeletedBy, "ownedBy", us.OwnedBy, + "runAs", us.RunAs, ) } diff --git a/pkg/envoy/resource/automation_workflow.go b/pkg/envoy/resource/automation_workflow.go new file mode 100644 index 000000000..ad27912a2 --- /dev/null +++ b/pkg/envoy/resource/automation_workflow.go @@ -0,0 +1,139 @@ +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(AUTOMATION_WORKFLOW_RESOURCE_TYPE) + r.Res = res + + r.AddIdentifier(identifiers(res.Handle, res.Meta.Name, res.ID)...) + + // Initial stamps + r.SetTimestamps(MakeCUDATimestamps(&res.CreatedAt, res.UpdatedAt, res.DeletedAt, nil)) + us := MakeCUDOUserstamps(res.CreatedBy, res.UpdatedBy, res.DeletedBy, res.OwnedBy) + us.RunAs = MakeUserstampFromID(res.RunAs) + r.SetUserstamps(us) + + return r +} + +func (r *AutomationWorkflow) AddAutomationTrigger(res *types.Trigger) *AutomationTrigger { + t := &AutomationTrigger{ + base: &base{}, + } + + t.Res = res + + // Initial stamps + t.SetTimestamps(MakeCUDATimestamps(&res.CreatedAt, res.UpdatedAt, res.DeletedAt, nil)) + t.SetUserstamps(MakeCUDOUserstamps(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) 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 +} + +func (r *AutomationWorkflow) Ref() string { + return FirstOkString(r.Res.Handle, r.Res.Meta.Name, strconv.FormatUint(r.Res.ID, 10)) +} + +// 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()) +} diff --git a/pkg/envoy/resource/base.go b/pkg/envoy/resource/base.go index 30cf071cd..271ec502b 100644 --- a/pkg/envoy/resource/base.go +++ b/pkg/envoy/resource/base.go @@ -58,6 +58,7 @@ type ( UpdatedBy *Userstamp DeletedBy *Userstamp OwnedBy *Userstamp + RunAs *Userstamp } UserstampIndex map[string]*Userstamp @@ -123,7 +124,7 @@ func (t *base) SetUserstamps(us *Userstamps) { t.us = us if us != nil { - uu := []*Userstamp{us.CreatedBy, us.UpdatedBy, us.DeletedBy, us.OwnedBy} + uu := []*Userstamp{us.CreatedBy, us.UpdatedBy, us.DeletedBy, us.OwnedBy, us.RunAs} t.SetUserRefs(uu) } } @@ -352,6 +353,10 @@ func MakeUserstampFromRef(ref string) *Userstamp { return us } +func MakeUserstampFromID(ID uint64) *Userstamp { + return MakeUserstampFromRef(strconv.FormatUint(ID, 10)) +} + func (ux UserstampIndex) Add(uu ...*types.User) { for _, u := range uu { sID := strconv.FormatUint(u.ID, 10) diff --git a/pkg/envoy/resource/types.go b/pkg/envoy/resource/types.go index 59fe842ec..05ad8e2a3 100644 --- a/pkg/envoy/resource/types.go +++ b/pkg/envoy/resource/types.go @@ -1,6 +1,7 @@ package resource import ( + at "github.com/cortezaproject/corteza-server/automation/types" ct "github.com/cortezaproject/corteza-server/compose/types" st "github.com/cortezaproject/corteza-server/system/types" ) @@ -39,18 +40,19 @@ type ( ) var ( - APPLICATION_RESOURCE_TYPE = st.ApplicationRBACResource.String() - COMPOSE_CHART_RESOURCE_TYPE = ct.ChartRBACResource.String() - COMPOSE_MODULE_RESOURCE_TYPE = ct.ModuleRBACResource.String() - COMPOSE_NAMESPACE_RESOURCE_TYPE = ct.NamespaceRBACResource.String() - COMPOSE_PAGE_RESOURCE_TYPE = ct.PageRBACResource.String() - COMPOSE_RECORD_RESOURCE_TYPE = "compose:record:" - RBAC_RESOURCE_TYPE = "rbac:rule:" - ROLE_RESOURCE_TYPE = st.RoleRBACResource.String() - SETTINGS_RESOURCE_TYPE = "system:setting:" - USER_RESOURCE_TYPE = st.UserRBACResource.String() - TEMPLATE_RESOURCE_TYPE = st.TemplateRBACResource.String() - DATA_SOURCE_RESOURCE_TYPE = "data:raw:" + APPLICATION_RESOURCE_TYPE = st.ApplicationRBACResource.String() + COMPOSE_CHART_RESOURCE_TYPE = ct.ChartRBACResource.String() + COMPOSE_MODULE_RESOURCE_TYPE = ct.ModuleRBACResource.String() + COMPOSE_NAMESPACE_RESOURCE_TYPE = ct.NamespaceRBACResource.String() + COMPOSE_PAGE_RESOURCE_TYPE = ct.PageRBACResource.String() + COMPOSE_RECORD_RESOURCE_TYPE = "compose:record:" + RBAC_RESOURCE_TYPE = "rbac:rule:" + ROLE_RESOURCE_TYPE = st.RoleRBACResource.String() + SETTINGS_RESOURCE_TYPE = "system:setting:" + USER_RESOURCE_TYPE = st.UserRBACResource.String() + TEMPLATE_RESOURCE_TYPE = st.TemplateRBACResource.String() + DATA_SOURCE_RESOURCE_TYPE = "data:raw:" + AUTOMATION_WORKFLOW_RESOURCE_TYPE = at.WorkflowRBACResource.String() ) func MakeIdentifiers(ss ...string) Identifiers { diff --git a/pkg/envoy/store/application.go b/pkg/envoy/store/application.go index 1aadafe4b..7eb14c2cc 100644 --- a/pkg/envoy/store/application.go +++ b/pkg/envoy/store/application.go @@ -14,6 +14,8 @@ type ( res *resource.Application app *types.Application + + ux *userIndex } ) diff --git a/pkg/envoy/store/application_unmarshal.go b/pkg/envoy/store/application_unmarshal.go index e63ef1fb9..96e226ef7 100644 --- a/pkg/envoy/store/application_unmarshal.go +++ b/pkg/envoy/store/application_unmarshal.go @@ -6,14 +6,18 @@ import ( "github.com/cortezaproject/corteza-server/system/types" ) -func newApplication(app *types.Application) *application { +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( - resource.NewApplication(app.app), + rs, ) } diff --git a/pkg/envoy/store/automation.go b/pkg/envoy/store/automation.go new file mode 100644 index 000000000..5f058166d --- /dev/null +++ b/pkg/envoy/store/automation.go @@ -0,0 +1,153 @@ +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/filter" + "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: []uint64{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.Workflows(&types.WorkflowFilter{ + Query: id, + }) + } + } + + return df +} + +// Roles adds a new RoleFilter +func (df *DecodeFilter) Workflows(f *types.WorkflowFilter) *DecodeFilter { + if df.automationWorkflow == nil { + df.automationWorkflow = make([]*automationWorkflowFilter, 0, 1) + } + df.automationWorkflow = append(df.automationWorkflow, (*automationWorkflowFilter)(f)) + return df +} diff --git a/pkg/envoy/store/automation_workflow.go b/pkg/envoy/store/automation_workflow.go new file mode 100644 index 000000000..0c5d4cb33 --- /dev/null +++ b/pkg/envoy/store/automation_workflow.go @@ -0,0 +1,191 @@ +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 +} + +// findAutomationWorkflowRS looks for the workflow in the resources & the store +// +// Provided resources are prioritized. +func findAutomationWorkflowRS(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 findAutomationWorkflowS(ctx, s, makeGenericFilter(ii)) +} + +// findAutomationWorkflowS looks for the workflow in the store +func findAutomationWorkflowS(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 +} + +// // findAutomationTriggerRS looks for the trigger in the resources & the store +// // +// // Provided resources are prioritized. +// func findAutomationTriggerRS(ctx context.Context, s store.Storer, rr resource.InterfaceSet, ii resource.Identifiers) (wf *types.Trigger, err error) { +// wf = resource.FindAutomationTrigger(rr, ii) +// if wf != nil { +// return wf, nil +// } + +// return findAutomationTriggerS(ctx, s, makeGenericFilter(ii)) +// } + +// // findAutomationTriggerS looks for the trigger in the store +// func findAutomationTriggerS(ctx context.Context, s store.Storer, gf genericFilter) (wf *types.Trigger, err error) { +// if gf.id > 0 { +// wf, err = store.LookupAutomationTriggerByID(ctx, s, gf.id) +// if err != nil && err != store.ErrNotFound { +// return nil, err +// } + +// if wf != nil { +// return +// } +// } + +// for _, i := range gf.identifiers { +// store.SearchAutomationTriggers() + +// nn, _, err := store.SearchAutomationTriggers(ctx, s, types.TriggerFilter{ +// 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 +// } diff --git a/pkg/envoy/store/automation_workflow_marshal.go b/pkg/envoy/store/automation_workflow_marshal.go new file mode 100644 index 000000000..2ca8bf2b7 --- /dev/null +++ b/pkg/envoy/store/automation_workflow_marshal.go @@ -0,0 +1,236 @@ +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" +) + +func newAutomationWorkflowFromResource(res *resource.AutomationWorkflow, cfg *EncoderConfig) resourceState { + return &automationWorkflow{ + cfg: mergeConfig(cfg, res.Config()), + + res: res, + } +} + +// @todo all of the deps and stuff and things + +// Prepare prepares the automationWorkflow to be encoded +// +// Any validation, additional constraining should be performed here. +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) { + // Try to get the original workflow + n.wf, err = findAutomationWorkflowS(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 + } + + // Try to find any related triggers for this workflow + tt, _, err := store.SearchAutomationTriggers(ctx, pl.s, types.TriggerFilter{ + WorkflowID: []uint64{n.wf.ID}, + Disabled: filter.StateInclusive, + }) + if err != nil { + return err + } + + n.tt = tt + return nil +} + +// Encode encodes the automationWorkflow to the store +// +// Encode is allowed to do some data manipulation, but no resource constraints +// should be changed. +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 + } + } + + 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 + } + } + 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...) +} diff --git a/pkg/envoy/store/automation_workflow_unmarshal.go b/pkg/envoy/store/automation_workflow_unmarshal.go new file mode 100644 index 000000000..e937f5032 --- /dev/null +++ b/pkg/envoy/store/automation_workflow_unmarshal.go @@ -0,0 +1,39 @@ +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, + } +} + +// MarshalEnvoy converts the automation workflow struct to a resource +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, + ) +} diff --git a/pkg/envoy/store/decoder.go b/pkg/envoy/store/decoder.go index 9cd01d7ef..49573e4b4 100644 --- a/pkg/envoy/store/decoder.go +++ b/pkg/envoy/store/decoder.go @@ -6,12 +6,16 @@ import ( "github.com/cortezaproject/corteza-server/pkg/envoy" "github.com/cortezaproject/corteza-server/pkg/envoy/resource" "github.com/cortezaproject/corteza-server/store" + "github.com/cortezaproject/corteza-server/system/types" ) type ( - decoder struct{} + decoder struct { + ux *userIndex + } DecodeFilter struct { + // Compose stuff composeNamespace []*composeNamespaceFilter composeModule []*composeModuleFilter @@ -26,6 +30,9 @@ type ( applications []*applicationFilter settings []*settingFilter rbac []*rbacFilter + + // Automation stuff + automationWorkflow []*automationWorkflowFilter } auxMarshaller []envoy.Marshaller @@ -33,15 +40,30 @@ type ( 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...) + // @todo others... + + return df +} + func (aum auxMarshaller) MarshalEnvoy() ([]resource.Interface, error) { ii := make([]resource.Interface, 0, len(aum)) for _, m := range aum { @@ -59,6 +81,13 @@ func (aum auxMarshaller) MarshalEnvoy() ([]resource.Interface, error) { func (d *decoder) Decode(ctx context.Context, s store.Storer, 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) @@ -77,7 +106,8 @@ func (d *decoder) Decode(ctx context.Context, s store.Storer, f *DecodeFilter) ( } compose := newComposeDecoder() - system := newSystemDecoder() + system := newSystemDecoder(d.ux) + automation := newAutomationDecoder(d.ux) mm, err := pof( compose.decodeComposeNamespace(ctx, s, f.composeNamespace), @@ -91,6 +121,8 @@ func (d *decoder) Decode(ctx context.Context, s store.Storer, f *DecodeFilter) ( system.decodeTemplates(ctx, s, f.templates), system.decodeApplications(ctx, s, f.applications), system.decodeSettings(ctx, s, f.settings), + + automation.decodeWorkflows(ctx, s, f.automationWorkflow), ) if err != nil { return nil, err @@ -98,6 +130,7 @@ func (d *decoder) Decode(ctx context.Context, s store.Storer, f *DecodeFilter) ( f.allowRbacResource(compose.resourceID...) f.allowRbacResource(system.resourceID...) + f.allowRbacResource(automation.resourceID...) rr, err := pof( system.decodeRbac(ctx, s, f.rbac), ) @@ -107,3 +140,39 @@ func (d *decoder) Decode(ctx context.Context, s store.Storer, f *DecodeFilter) ( 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: filtered, + }) + if err != nil { + return err + } + + for _, u := range users { + ux.users[u.ID] = u + } + + return nil +} diff --git a/pkg/envoy/store/encoder.go b/pkg/envoy/store/encoder.go index 5097b8672..8ebd7fa99 100644 --- a/pkg/envoy/store/encoder.go +++ b/pkg/envoy/store/encoder.go @@ -134,6 +134,8 @@ func (se *storeEncoder) Prepare(ctx context.Context, ee ...*envoy.ResourceState) err = f(NewUserFromResource(res, se.cfg), ers) case *resource.Template: err = f(NewTemplateFromResource(res, se.cfg), ers) + case *resource.AutomationWorkflow: + err = f(newAutomationWorkflowFromResource(res, se.cfg), ers) case *resource.Role: err = f(NewRoleFromResource(res, se.cfg), ers) case *resource.Application: diff --git a/pkg/envoy/store/setting.go b/pkg/envoy/store/setting.go index 9a9a5dc8a..636417c7d 100644 --- a/pkg/envoy/store/setting.go +++ b/pkg/envoy/store/setting.go @@ -11,5 +11,7 @@ type ( res *resource.Setting st *types.SettingValue + + ux *userIndex } ) diff --git a/pkg/envoy/store/setting_unmarshal.go b/pkg/envoy/store/setting_unmarshal.go index 4a51f15e2..e26267980 100644 --- a/pkg/envoy/store/setting_unmarshal.go +++ b/pkg/envoy/store/setting_unmarshal.go @@ -6,14 +6,19 @@ import ( "github.com/cortezaproject/corteza-server/system/types" ) -func newSetting(res *types.SettingValue) *setting { +func newSetting(res *types.SettingValue, ux *userIndex) *setting { return &setting{ st: res, + + ux: ux, } } func (st *setting) MarshalEnvoy() ([]resource.Interface, error) { + rs := resource.NewSetting(st.st) + syncUserStamps(rs.Userstamps(), st.ux) + return envoy.CollectNodes( - resource.NewSetting(st.st), + rs, ) } diff --git a/pkg/envoy/store/system.go b/pkg/envoy/store/system.go index b72a00e83..74005e308 100644 --- a/pkg/envoy/store/system.go +++ b/pkg/envoy/store/system.go @@ -2,6 +2,7 @@ package store import ( "context" + "strings" "github.com/cortezaproject/corteza-server/pkg/envoy" "github.com/cortezaproject/corteza-server/pkg/rbac" @@ -34,12 +35,14 @@ type ( systemDecoder struct { resourceID []uint64 + ux *userIndex } ) -func newSystemDecoder() *systemDecoder { +func newSystemDecoder(ux *userIndex) *systemDecoder { return &systemDecoder{ resourceID: make([]uint64, 0, 200), + ux: ux, } } @@ -206,7 +209,13 @@ func (d *systemDecoder) decodeApplications(ctx context.Context, s systemStore, f } for _, n := range nn { - mm = append(mm, newApplication(n)) + // Index users + err = d.ux.add( + ctx, + n.OwnerID, + ) + + mm = append(mm, newApplication(n, d.ux)) d.resourceID = append(d.resourceID, n.ID) } @@ -245,7 +254,14 @@ func (d *systemDecoder) decodeSettings(ctx context.Context, s systemStore, ff [] } for _, n := range nn { - mm = append(mm, newSetting(n)) + // Index users + err = d.ux.add( + ctx, + n.OwnedBy, + n.UpdatedBy, + ) + + mm = append(mm, newSetting(n, d.ux)) } // mm = append(mm, NewSettings(nn)) @@ -306,6 +322,44 @@ func (d *systemDecoder) decodeRbac(ctx context.Context, s systemStore, ff []*rba } } +func (df *DecodeFilter) systemFromResource(rr ...string) *DecodeFilter { + for _, r := range rr { + if !strings.HasPrefix(r, "system") { + 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 "system:rols": + df = df.Roles(&types.RoleFilter{ + Query: id, + }) + case "system:uses": + df = df.Users(&types.UserFilter{ + Query: id, + }) + case "system:applicatios": + df = df.Applications(&types.ApplicationFilter{ + Query: id, + }) + case "system:settins": + df = df.Settings(&types.SettingsFilter{}) + case "system:rbac": + df = df.Rbac(&rbac.RuleFilter{}) + } + } + + return df +} + // Roles adds a new RoleFilter func (df *DecodeFilter) Roles(f *types.RoleFilter) *DecodeFilter { if df.roles == nil { diff --git a/pkg/envoy/store/util.go b/pkg/envoy/store/util.go index f3afcfc96..e1b72c939 100644 --- a/pkg/envoy/store/util.go +++ b/pkg/envoy/store/util.go @@ -104,6 +104,7 @@ func resolveUserstamps(ctx context.Context, s store.Storer, rr []resource.Interf us.CreatedBy, err = fetch(us.CreatedBy) us.UpdatedBy, err = fetch(us.UpdatedBy) us.DeletedBy, err = fetch(us.DeletedBy) + us.RunAs, err = fetch(us.RunAs) us.OwnedBy, err = fetch(us.OwnedBy) if err != nil { @@ -113,6 +114,24 @@ func resolveUserstamps(ctx context.Context, s store.Storer, rr []resource.Interf return us, nil } +func syncUserStamps(us *resource.Userstamps, ux *userIndex) { + if us.CreatedBy != nil && us.CreatedBy.UserID > 0 { + us.CreatedBy.U = ux.users[us.CreatedBy.UserID] + } + if us.UpdatedBy != nil && us.UpdatedBy.UserID > 0 { + us.UpdatedBy.U = ux.users[us.UpdatedBy.UserID] + } + if us.DeletedBy != nil && us.DeletedBy.UserID > 0 { + us.DeletedBy.U = ux.users[us.DeletedBy.UserID] + } + if us.OwnedBy != nil && us.OwnedBy.UserID > 0 { + us.OwnedBy.U = ux.users[us.OwnedBy.UserID] + } + if us.RunAs != nil && us.RunAs.UserID > 0 { + us.RunAs.U = ux.users[us.RunAs.UserID] + } +} + func resolveUserRefs(ctx context.Context, s store.Storer, pr []resource.Interface, refs resource.RefSet, dst map[string]uint64) (err error) { for _, uRef := range refs { u := resource.FindUser(pr, uRef.Identifiers) diff --git a/pkg/envoy/yaml/automation.go b/pkg/envoy/yaml/automation.go new file mode 100644 index 000000000..530bd4892 --- /dev/null +++ b/pkg/envoy/yaml/automation.go @@ -0,0 +1,64 @@ +package yaml + +import ( + "reflect" + + "github.com/cortezaproject/corteza-server/pkg/envoy/resource" + "github.com/cortezaproject/corteza-server/pkg/y7s" + "gopkg.in/yaml.v3" +) + +type ( + automation struct { + Workflows automationWorkflowSet + + EncoderConfig *EncoderConfig `yaml:"-"` + } +) + +func (c *automation) MarshalYAML() (interface{}, error) { + cn, _ := makeMap() + var err error + + if len(c.Workflows) > 0 { + c.Workflows.ConfigureEncoder(c.EncoderConfig) + + cn, err = encodeResource(cn, "workflows", c.Workflows, c.EncoderConfig.MappedOutput, "handle") + if err != nil { + return nil, err + } + } + + return cn, nil +} + +func (a *automation) UnmarshalYAML(n *yaml.Node) error { + var err error + + return y7s.EachMap(n, func(k, v *yaml.Node) error { + switch k.Value { + case "workflows": + err = v.Decode(&a.Workflows) + if err != nil { + return err + } + } + return nil + }) +} + +func (a automation) MarshalEnvoy() ([]resource.Interface, error) { + nn := make([]resource.Interface, 0, 100) + rf := reflect.ValueOf(a) + for i := 0; i < rf.NumField(); i++ { + if mr, ok := rf.Field(i).Interface().(EnvoyMarshler); ok { + tmp, err := mr.MarshalEnvoy() + if err != nil { + return nil, err + } + nn = append(nn, tmp...) + } + } + + return nn, nil +} diff --git a/pkg/envoy/yaml/automation_workflow.go b/pkg/envoy/yaml/automation_workflow.go new file mode 100644 index 000000000..a9b76e606 --- /dev/null +++ b/pkg/envoy/yaml/automation_workflow.go @@ -0,0 +1,63 @@ +package yaml + +import ( + "github.com/cortezaproject/corteza-server/automation/types" + "github.com/cortezaproject/corteza-server/pkg/envoy/resource" +) + +type ( + automationWorkflow struct { + res *types.Workflow + triggers automationTriggerSet + steps automationWorkflowStepSet + paths automationWorkflowPathSet + + ts *resource.Timestamps + us *resource.Userstamps + + envoyConfig *resource.EnvoyConfig + encoderConfig *EncoderConfig + + rbac rbacRuleSet + } + automationWorkflowSet []*automationWorkflow + + automationTrigger struct { + res *types.Trigger + + ts *resource.Timestamps + us *resource.Userstamps + + envoyConfig *resource.EnvoyConfig + encoderConfig *EncoderConfig + } + automationTriggerSet []*automationTrigger + + automationWorkflowStep struct { + res *types.WorkflowStep + + ts *resource.Timestamps + us *resource.Userstamps + + envoyConfig *resource.EnvoyConfig + encoderConfig *EncoderConfig + } + automationWorkflowStepSet []*automationWorkflowStep + + automationWorkflowPath struct { + res *types.WorkflowPath + + ts *resource.Timestamps + us *resource.Userstamps + + envoyConfig *resource.EnvoyConfig + encoderConfig *EncoderConfig + } + automationWorkflowPathSet []*automationWorkflowPath +) + +func (nn automationWorkflowSet) ConfigureEncoder(cfg *EncoderConfig) { + for _, n := range nn { + n.encoderConfig = cfg + } +} diff --git a/pkg/envoy/yaml/automation_workflow_marshal.go b/pkg/envoy/yaml/automation_workflow_marshal.go new file mode 100644 index 000000000..841bf6539 --- /dev/null +++ b/pkg/envoy/yaml/automation_workflow_marshal.go @@ -0,0 +1,155 @@ +package yaml + +import ( + "context" + + "github.com/cortezaproject/corteza-server/pkg/envoy" + "github.com/cortezaproject/corteza-server/pkg/envoy/resource" + "github.com/cortezaproject/corteza-server/pkg/envoy/util" +) + +func automationWorkflowFromResource(r *resource.AutomationWorkflow, cfg *EncoderConfig) *automationWorkflow { + tt := make(automationTriggerSet, len(r.Triggers)) + for i, t := range r.Triggers { + tt[i] = &automationTrigger{ + res: t.Res, + encoderConfig: cfg, + } + } + + ss := make(automationWorkflowStepSet, len(r.Steps)) + for i, s := range r.Steps { + ss[i] = &automationWorkflowStep{ + res: s.Res, + encoderConfig: cfg, + } + } + + pp := make(automationWorkflowPathSet, len(r.Paths)) + for i, p := range r.Paths { + pp[i] = &automationWorkflowPath{ + res: p.Res, + encoderConfig: cfg, + } + } + + return &automationWorkflow{ + res: r.Res, + triggers: tt, + steps: ss, + paths: pp, + + encoderConfig: cfg, + } +} + +// Prepare prepares the automationWorkflow to be encoded +// +// Any validation, additional constraining should be performed here. +func (n *automationWorkflow) Prepare(ctx context.Context, state *envoy.ResourceState) (err error) { + wf, ok := state.Res.(*resource.AutomationWorkflow) + if !ok { + return encoderErrInvalidResource(resource.AUTOMATION_WORKFLOW_RESOURCE_TYPE, state.Res.ResourceType()) + } + + n.res = wf.Res + n.us = wf.Userstamps() + + return nil +} + +// Encode encodes the automationWorkflow to the document +// +// Encode is allowed to do some data manipulation, but no resource constraints +// should be changed. +func (n *automationWorkflow) Encode(ctx context.Context, doc *Document, state *envoy.ResourceState) (err error) { + if n.res.ID <= 0 { + n.res.ID = util.NextID() + } + + n.ts, err = resource.MakeCUDATimestamps(&n.res.CreatedAt, n.res.UpdatedAt, n.res.DeletedAt, nil). + Model(n.encoderConfig.TimeLayout, n.encoderConfig.Timezone) + if err != nil { + return err + } + n.us, err = resolveUserstamps(state.ParentResources, n.us) + if err != nil { + return err + } + + // @todo skip eval? + + doc.AddAutomationWorkflow(n) + + return err +} + +func (wf *automationWorkflow) MarshalYAML() (interface{}, error) { + var err error + + nn, err := makeMap( + "handle", wf.res.Handle, + "meta", wf.res.Meta, + "enabled", wf.res.Enabled, + + "trace", wf.res.Trace, + "keepSessions", wf.res.KeepSessions, + + "scope", wf.res.Scope, + "triggers", wf.triggers, + "steps", wf.res.Steps, + "paths", wf.res.Paths, + + // "issues", wf.res.Issues, + "labels", wf.res.Labels, + ) + if err != nil { + return nil, err + } + + nn, err = mapTimestamps(nn, wf.ts) + if err != nil { + return nil, err + } + + nn, err = mapUserstamps(nn, wf.us) + if err != nil { + return nil, err + } + + return nn, nil +} + +func (t *automationTrigger) MarshalYAML() (interface{}, error) { + var err error + + nn, err := makeMap( + "resourceType", t.res.ResourceType, + "eventType", t.res.EventType, + "constraints", t.res.Constraints, + "enabled", t.res.Enabled, + "workflowID", t.res.WorkflowID, + + "stepID", t.res.StepID, + "input", t.res.Input, + + "meta", t.res.Meta, + + "labels", t.res.Labels, + ) + if err != nil { + return nil, err + } + + nn, err = mapTimestamps(nn, t.ts) + if err != nil { + return nil, err + } + + nn, err = mapUserstamps(nn, t.us) + if err != nil { + return nil, err + } + + return nn, nil +} diff --git a/pkg/envoy/yaml/automation_workflow_test.go b/pkg/envoy/yaml/automation_workflow_test.go new file mode 100644 index 000000000..e4d35d5d9 --- /dev/null +++ b/pkg/envoy/yaml/automation_workflow_test.go @@ -0,0 +1,39 @@ +package yaml + +import ( + "testing" + + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +func TestAutomationWorkflow_UnmarshalYAML(t *testing.T) { + var ( + parseString = func(src string) (*automationWorkflow, error) { + w := &automationWorkflow{} + return w, yaml.Unmarshal([]byte(src), w) + } + ) + + t.Run("empty", func(t *testing.T) { + req := require.New(t) + + w, err := parseString(``) + req.NoError(err) + req.NotNil(w) + req.Nil(w.res) + }) + + t.Run("workflow 1", func(t *testing.T) { + req := require.New(t) + + doc, err := parseDocument("workflow_1") + req.NoError(err) + req.NotNil(doc) + req.Len(doc.automation.Workflows, 1) + req.NotNil(doc.automation.Workflows[0]) + req.Len(doc.automation.Workflows[0].triggers, 1) + req.Len(doc.automation.Workflows[0].steps, 1) + req.Len(doc.automation.Workflows[0].paths, 1) + }) +} diff --git a/pkg/envoy/yaml/automation_workflow_unmarshal.go b/pkg/envoy/yaml/automation_workflow_unmarshal.go new file mode 100644 index 000000000..13acf6cf5 --- /dev/null +++ b/pkg/envoy/yaml/automation_workflow_unmarshal.go @@ -0,0 +1,289 @@ +package yaml + +import ( + "github.com/cortezaproject/corteza-server/automation/types" + "github.com/cortezaproject/corteza-server/pkg/envoy" + "github.com/cortezaproject/corteza-server/pkg/envoy/resource" + "github.com/cortezaproject/corteza-server/pkg/y7s" + "gopkg.in/yaml.v3" +) + +func (wset *automationWorkflowSet) UnmarshalYAML(n *yaml.Node) error { + return y7s.Each(n, func(k, v *yaml.Node) (err error) { + var ( + wrap = &automationWorkflow{} + ) + + if v == nil { + return y7s.NodeErr(n, "malformed automation workflow definition") + } + + if err = v.Decode(&wrap); err != nil { + return + } + + if err = decodeRef(k, "automation workflow handle", &wrap.res.Handle); err != nil { + return y7s.NodeErr(n, "Automation workflow reference must be a valid handle") + } + + if wrap.res.Meta.Name == "" { + // if name is not set, use handle + wrap.res.Meta.Name = wrap.res.Handle + } + + *wset = append(*wset, wrap) + return + }) +} + +func (wrap *automationWorkflow) UnmarshalYAML(n *yaml.Node) (err error) { + if wrap.res == nil { + wrap.rbac = make(rbacRuleSet, 0, 10) + wrap.res = &types.Workflow{} + } + + if wrap.rbac, err = decodeRbac(n); err != nil { + return + } + + if wrap.envoyConfig, err = decodeEnvoyConfig(n); err != nil { + return + } + + if wrap.ts, err = decodeTimestamps(n); err != nil { + return + } + + if wrap.us, err = decodeUserstamps(n); err != nil { + return + } + + return y7s.EachMap(n, func(k, v *yaml.Node) (err error) { + switch k.Value { + case "handle": + return y7s.DecodeScalar(v, "workflow handle", &wrap.res.Handle) + case "meta": + return v.Decode(&wrap.res.Meta) + case "enabled": + return y7s.DecodeScalar(v, "workflow enabled", &wrap.res.Enabled) + case "trace": + return y7s.DecodeScalar(v, "workflow trace", &wrap.res.Trace) + case "keepSessions": + return y7s.DecodeScalar(v, "workflow keepSessions", &wrap.res.KeepSessions) + case "scope": + err = v.Decode(&wrap.res.Scope) + if err != nil { + return err + } + + case "triggers": + wrap.triggers = make(automationTriggerSet, 0, 100) + + err = v.Decode(&wrap.triggers) + if err != nil { + return err + } + + case "steps": + wrap.steps = make(automationWorkflowStepSet, 0, 100) + + err = v.Decode(&wrap.steps) + if err != nil { + return err + } + + case "paths": + wrap.paths = make(automationWorkflowPathSet, 0, 100) + + err = v.Decode(&wrap.paths) + if err != nil { + return err + } + } + + return nil + }) +} + +func (wrap *automationTrigger) UnmarshalYAML(n *yaml.Node) (err error) { + if wrap.res == nil { + wrap.res = &types.Trigger{} + } + + if wrap.envoyConfig, err = decodeEnvoyConfig(n); err != nil { + return + } + + if wrap.ts, err = decodeTimestamps(n); err != nil { + return + } + + if wrap.us, err = decodeUserstamps(n); err != nil { + return + } + + return y7s.EachMap(n, func(k, v *yaml.Node) (err error) { + switch k.Value { + case "enabled": + return y7s.DecodeScalar(v, "trigger enabled", &wrap.res.Enabled) + case "stepID": + return y7s.DecodeScalar(v, "trigger step", &wrap.res.StepID) + case "resourceType": + return y7s.DecodeScalar(v, "trigger resourceType", &wrap.res.ResourceType) + case "eventType": + return y7s.DecodeScalar(v, "trigger eventType", &wrap.res.EventType) + case "constraints": + return v.Decode(&wrap.res.Constraints) + case "input": + err = v.Decode(&wrap.res.Input) + if err != nil { + return err + } + + case "meta": + return v.Decode(&wrap.res.Meta) + } + + return nil + }) +} + +func (wrap *automationWorkflowStep) UnmarshalYAML(n *yaml.Node) (err error) { + if wrap.res == nil { + wrap.res = &types.WorkflowStep{} + } + + if wrap.envoyConfig, err = decodeEnvoyConfig(n); err != nil { + return + } + + return y7s.EachMap(n, func(k, v *yaml.Node) (err error) { + switch k.Value { + case "stepID": + return y7s.DecodeScalar(v, "trigger step", &wrap.res.ID) + case "kind": + return y7s.DecodeScalar(v, "step kind", &wrap.res.Kind) + case "ref": + return y7s.DecodeScalar(v, "step ref", &wrap.res.Ref) + case "arguments": + wrap.res.Arguments, err = unmarshalExprSet(v) + return err + case "results": + wrap.res.Results, err = unmarshalExprSet(v) + return err + case "meta": + return v.Decode(&wrap.res.Meta) + } + + return nil + }) +} + +func (wrap *automationWorkflowPath) UnmarshalYAML(n *yaml.Node) (err error) { + if wrap.res == nil { + wrap.res = &types.WorkflowPath{} + } + + if wrap.envoyConfig, err = decodeEnvoyConfig(n); err != nil { + return + } + + return y7s.EachMap(n, func(k, v *yaml.Node) (err error) { + switch k.Value { + case "expr": + return y7s.DecodeScalar(v, "path expr", &wrap.res.Expr) + case "parentID": + return y7s.DecodeScalar(v, "parent ref", &wrap.res.ParentID) + case "childID": + return y7s.DecodeScalar(v, "child ref", &wrap.res.ChildID) + case "meta": + return v.Decode(&wrap.res.Meta) + } + + return nil + }) +} + +func unmarshalExprSet(n *yaml.Node) ([]*types.Expr, error) { + ee := make([]*types.Expr, 0, 10) + + err := y7s.EachSeq(n, func(v *yaml.Node) (err error) { + wrap, err := unmarshalExpr(v) + ee = append(ee, wrap) + return err + }) + + return ee, err +} + +func unmarshalExpr(n *yaml.Node) (*types.Expr, error) { + wrap := &types.Expr{} + + err := y7s.EachMap(n, func(k, v *yaml.Node) (err error) { + switch k.Value { + case "target": + return y7s.DecodeScalar(v, "expression target", &wrap.Target) + case "source": + return y7s.DecodeScalar(v, "expression source", &wrap.Source) + case "expr": + return y7s.DecodeScalar(v, "expression expr", &wrap.Expr) + case "value": + return y7s.DecodeScalar(v, "expression value", &wrap.Value) + case "type": + return y7s.DecodeScalar(v, "expression type", &wrap.Type) + case "tests": + tt := make(types.TestSet, 0, 2) + err = v.Decode(&tt) + if err != nil { + return err + } + wrap.Tests = tt + return nil + } + + return nil + }) + + return wrap, err +} + +func (wset automationWorkflowSet) MarshalEnvoy() ([]resource.Interface, error) { + // namespace usually have bunch of sub-resources defined + nn := make([]resource.Interface, 0, len(wset)*10) + + for _, res := range wset { + if tmp, err := res.MarshalEnvoy(); err != nil { + return nil, err + } else { + nn = append(nn, tmp...) + } + } + + return nn, nil +} + +func (wrap automationWorkflow) MarshalEnvoy() ([]resource.Interface, error) { + rs := resource.NewAutomationWorkflow(wrap.res) + rs.SetTimestamps(wrap.ts) + rs.SetUserstamps(wrap.us) + rs.SetConfig(wrap.envoyConfig) + + for _, t := range wrap.triggers { + trs := rs.AddAutomationTrigger(t.res) + trs.SetTimestamps(t.ts) + trs.SetUserstamps(t.us) + } + + for _, s := range wrap.steps { + rs.AddAutomationWorkflowStep(s.res) + } + + for _, p := range wrap.paths { + rs.AddAutomationWorkflowPath(p.res) + } + + return envoy.CollectNodes( + rs, + wrap.rbac.bindResource(rs), + ) +} diff --git a/pkg/envoy/yaml/compose_record_marshal.go b/pkg/envoy/yaml/compose_record_marshal.go index 3c88a62c1..cca3ee3cd 100644 --- a/pkg/envoy/yaml/compose_record_marshal.go +++ b/pkg/envoy/yaml/compose_record_marshal.go @@ -149,9 +149,15 @@ func resolveUserstamps(rr []resource.Interface, us *resource.Userstamps) (*resou } fetch := func(us *resource.Userstamp) (*resource.Userstamp, error) { - if us == nil { + if us == nil || us.UserID == 0 { return nil, nil } + + // This one can be considered as valid + if us.Ref != "" && us.UserID > 0 && us.U != nil { + return us, nil + } + ii := resource.MakeIdentifiers() if us.UserID > 0 { @@ -184,6 +190,7 @@ func resolveUserstamps(rr []resource.Interface, us *resource.Userstamps) (*resou us.UpdatedBy, err = fetch(us.UpdatedBy) us.DeletedBy, err = fetch(us.DeletedBy) us.OwnedBy, err = fetch(us.OwnedBy) + us.RunAs, err = fetch(us.RunAs) if err != nil { return nil, err diff --git a/pkg/envoy/yaml/document.go b/pkg/envoy/yaml/document.go index 1ca61d414..b3439bbe1 100644 --- a/pkg/envoy/yaml/document.go +++ b/pkg/envoy/yaml/document.go @@ -12,7 +12,9 @@ import ( type ( // Document defines the supported yaml structure Document struct { - compose *compose + compose *compose + automation *automation + roles roleSet users userSet templates templateSet @@ -29,6 +31,10 @@ func (doc *Document) UnmarshalYAML(n *yaml.Node) (err error) { return } + if err = n.Decode(&doc.automation); err != nil { + return + } + if doc.rbac, err = decodeRbac(n); err != nil { return } @@ -71,6 +77,17 @@ func (doc *Document) MarshalYAML() (interface{}, error) { dn, _ = inlineContent(dn, cn) } + if doc.automation != nil { + doc.automation.EncoderConfig = doc.cfg + + cn, err := encodeNode(doc.automation) + if err != nil { + return nil, err + } + + dn, _ = inlineContent(dn, cn) + } + if doc.roles != nil { doc.roles.ConfigureEncoder(doc.cfg) @@ -149,6 +166,9 @@ func (doc *Document) Decode(ctx context.Context) ([]resource.Interface, error) { if doc.templates != nil { mm = append(mm, doc.templates) } + if doc.automation != nil { + mm = append(mm, doc.automation) + } if doc.applications != nil { mm = append(mm, doc.applications) } @@ -234,6 +254,17 @@ func (doc *Document) AddComposeChart(c *composeChart) { doc.compose.Charts = append(doc.compose.Charts, c) } +func (doc *Document) AddAutomationWorkflow(m *automationWorkflow) { + if doc.automation == nil { + doc.automation = &automation{} + } + if doc.automation.Workflows == nil { + doc.automation.Workflows = make(automationWorkflowSet, 0) + } + + doc.automation.Workflows = append(doc.automation.Workflows, m) +} + // AddRole adds a new role to the document func (doc *Document) AddRole(r *role) { if doc.roles == nil { diff --git a/pkg/envoy/yaml/encoder.go b/pkg/envoy/yaml/encoder.go index e850ef823..2aab53c2c 100644 --- a/pkg/envoy/yaml/encoder.go +++ b/pkg/envoy/yaml/encoder.go @@ -134,6 +134,9 @@ func (ye *yamlEncoder) Prepare(ctx context.Context, ee ...*envoy.ResourceState) case *resource.RbacRule: err = f(rbacRuleFromResource(res, ye.cfg), e) + case *resource.AutomationWorkflow: + err = f(automationWorkflowFromResource(res, ye.cfg), e) + default: err = ErrUnknownResource } diff --git a/pkg/envoy/yaml/stamps.go b/pkg/envoy/yaml/stamps.go index d9b994d04..c30b695b1 100644 --- a/pkg/envoy/yaml/stamps.go +++ b/pkg/envoy/yaml/stamps.go @@ -69,6 +69,10 @@ func decodeUserstamps(n *yaml.Node) (*resource.Userstamps, error) { "ownerid", "owner": us.OwnedBy, err = f(v) + case "runas", + "runasid", + "runner": + us.RunAs, err = f(v) } return err }) diff --git a/pkg/envoy/yaml/testdata/workflow_1.yaml b/pkg/envoy/yaml/testdata/workflow_1.yaml new file mode 100644 index 000000000..97fc71269 --- /dev/null +++ b/pkg/envoy/yaml/testdata/workflow_1.yaml @@ -0,0 +1,75 @@ +workflows: + testko_wf: + enabled: true + trace: true + keepSessions: 2 + meta: + name: name here + description: description here + + triggers: + - enabled: true + stepID: 101 + resourceType: "compose:module" + eventType: "beforeCreate" + constraints: + - name: name + op: op + values: + - values + + meta: + description: description + visual: {} + ownedBy: "219521853847258224" + createdAt: "2021-02-23T09:53:41Z" + createdBy: "219521853847258224" + updatedAt: "2021-02-23T09:53:41Z" + updatedBy: "219521853847258224" + deletedAt: "2021-02-23T09:53:41Z" + deletedBy: "219521853847258224" + + steps: + - stepID: 101 + kind: "termination" + arguments: + - target: target + source: source + expr: expr + value: value + type: type + tests: + - expr: expr + error: error + results: + - target: target + source: source + expr: expr + value: value + type: type + tests: + - expr: expr + error: error + meta: + name: name + description: description + visual: {} + + paths: + - expr: expr + parentID: 101 + childID: 101 + meta: + name: name + description: description + visual: {} + + runAs: "219521853847258224" + + ownedBy: "219521853847258224" + createdAt: "2021-02-23T09:53:41Z" + createdBy: "219521853847258224" + updatedAt: "2021-02-23T09:53:41Z" + updatedBy: "219521853847258224" + deletedAt: "2021-02-23T09:53:41Z" + deletedBy: "219521853847258224" diff --git a/pkg/envoy/yaml/util_encode.go b/pkg/envoy/yaml/util_encode.go index eaf62de54..ec7fb26c6 100644 --- a/pkg/envoy/yaml/util_encode.go +++ b/pkg/envoy/yaml/util_encode.go @@ -34,6 +34,7 @@ func mapUserstamps(n *yaml.Node, us *resource.Userstamps) (*yaml.Node, error) { "updatedBy", us.UpdatedBy, "deletedBy", us.DeletedBy, "ownedBy", us.OwnedBy, + "runAs", us.RunAs, ) } diff --git a/tests/envoy/automation.go b/tests/envoy/automation.go new file mode 100644 index 000000000..4b16715e9 --- /dev/null +++ b/tests/envoy/automation.go @@ -0,0 +1,88 @@ +package envoy + +import ( + "context" + "testing" + + "github.com/cortezaproject/corteza-server/automation/types" + su "github.com/cortezaproject/corteza-server/pkg/envoy/store" + "github.com/cortezaproject/corteza-server/store" +) + +func sTestAutomationWorkflow(ctx context.Context, t *testing.T, s store.Storer, pfx string) *types.Workflow { + wf := &types.Workflow{ + ID: su.NextID(), + Handle: pfx + "_handle", + Meta: &types.WorkflowMeta{ + Name: pfx + "_name", + Description: pfx + "_description", + }, + Enabled: true, + Trace: true, + KeepSessions: 10, + Steps: types.WorkflowStepSet{ + &types.WorkflowStep{ + ID: 11, + Kind: "function", + }, + &types.WorkflowStep{ + ID: 12, + Kind: "function", + }, + }, + Paths: types.WorkflowPathSet{ + &types.WorkflowPath{ + ParentID: 11, + ChildID: 12, + Expr: "qwerty", + }, + }, + + CreatedAt: createdAt, + UpdatedAt: &updatedAt, + } + + err := store.CreateAutomationWorkflow(ctx, s, wf) + if err != nil { + t.Fatal(err) + } + + return wf +} + +func sTestAutomationTrigger(ctx context.Context, t *testing.T, s store.Storer, wfID uint64, pfx string) *types.Trigger { + wf := &types.Trigger{ + ID: su.NextID(), + + Enabled: true, + WorkflowID: wfID, + StepID: 11, + + ResourceType: "testko:test:", + + Constraints: types.TriggerConstraintSet{ + &types.TriggerConstraint{ + Name: "qwerty", + Op: "=", + Values: []string{ + "a", + "b", + }, + }, + }, + + Meta: &types.TriggerMeta{ + Description: pfx + "_description", + }, + + CreatedAt: createdAt, + UpdatedAt: &updatedAt, + } + + err := store.CreateAutomationTrigger(ctx, s, wf) + if err != nil { + t.Fatal(err) + } + + return wf +} diff --git a/tests/envoy/main.go b/tests/envoy/main.go index 191a9bfa3..34891c3dc 100644 --- a/tests/envoy/main.go +++ b/tests/envoy/main.go @@ -96,6 +96,9 @@ func truncateStore(ctx context.Context, s store.Storer, t *testing.T) { s.TruncateApplications(ctx), s.TruncateSettings(ctx), s.TruncateRbacRules(ctx), + + s.TruncateAutomationWorkflows(ctx), + s.TruncateAutomationTriggers(ctx), ) if err != nil { t.Fatal(err.Error()) diff --git a/tests/envoy/store_yaml_base_test.go b/tests/envoy/store_yaml_base_test.go index 14260c892..90fa6a66b 100644 --- a/tests/envoy/store_yaml_base_test.go +++ b/tests/envoy/store_yaml_base_test.go @@ -7,7 +7,8 @@ import ( "testing" "time" - ctypes "github.com/cortezaproject/corteza-server/compose/types" + atypes "github.com/cortezaproject/corteza-server/automation/types" + "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" @@ -48,11 +49,45 @@ func TestStoreYaml_base(t *testing.T) { } cases := []*tc{ + { + name: "base automation workflow", + pre: func(ctx context.Context, s store.Storer) (error, *su.DecodeFilter) { + wf := sTestAutomationWorkflow(ctx, t, s, "base") + sTestAutomationTrigger(ctx, t, s, wf.ID, "base") + + df := su.NewDecodeFilter().Workflows(&atypes.WorkflowFilter{}) + return nil, df + }, + check: func(ctx context.Context, s store.Storer, req *require.Assertions) { + wf, err := store.LookupAutomationWorkflowByHandle(ctx, s, "base_handle") + req.NoError(err) + req.NotNil(wf) + + req.Equal("base_handle", wf.Handle) + req.Equal("base_name", wf.Meta.Name) + req.Equal("base_description", wf.Meta.Description) + req.True(wf.Enabled) + req.True(wf.Trace) + req.Equal(10, wf.KeepSessions) + req.Len(wf.Steps, 2) + req.Len(wf.Paths, 1) + req.Equal(createdAt.Format(time.RFC3339), wf.CreatedAt.Format(time.RFC3339)) + req.Equal(updatedAt.Format(time.RFC3339), wf.UpdatedAt.Format(time.RFC3339)) + + tt, _, err := store.SearchAutomationTriggers(ctx, s, atypes.TriggerFilter{ + WorkflowID: []uint64{wf.ID}, + }) + req.NoError(err) + req.NotNil(tt) + req.Len(tt, 1) + }, + }, + { name: "base namespace", pre: func(ctx context.Context, s store.Storer) (error, *su.DecodeFilter) { sTestComposeNamespace(ctx, t, s, "base") - df := su.NewDecodeFilter().ComposeNamespace(&ctypes.NamespaceFilter{ + df := su.NewDecodeFilter().ComposeNamespace(&types.NamespaceFilter{ Slug: "base_namespace", }) return nil, df @@ -78,10 +113,10 @@ func TestStoreYaml_base(t *testing.T) { sTestComposeModule(ctx, t, s, ns.ID, "base") df := su.NewDecodeFilter(). - ComposeNamespace(&ctypes.NamespaceFilter{ + ComposeNamespace(&types.NamespaceFilter{ Slug: "base_namespace", }). - ComposeModule(&ctypes.ModuleFilter{ + ComposeModule(&types.ModuleFilter{ NamespaceID: ns.ID, Handle: "base_module", }) @@ -93,7 +128,7 @@ func TestStoreYaml_base(t *testing.T) { mod, err := store.LookupComposeModuleByNamespaceIDHandle(ctx, s, n.ID, "base_module") req.NoError(err) - mff, _, err := store.SearchComposeModuleFields(ctx, s, ctypes.ModuleFieldFilter{ + mff, _, err := store.SearchComposeModuleFields(ctx, s, types.ModuleFieldFilter{ ModuleID: []uint64{mod.ID}, }) req.NoError(err) @@ -139,10 +174,10 @@ func TestStoreYaml_base(t *testing.T) { sTestComposePage(ctx, t, s, ns.ID, "base") df := su.NewDecodeFilter(). - ComposeNamespace(&ctypes.NamespaceFilter{ + ComposeNamespace(&types.NamespaceFilter{ Slug: "base_namespace", }). - ComposePage(&ctypes.PageFilter{ + ComposePage(&types.PageFilter{ NamespaceID: ns.ID, Handle: "base_page", }) @@ -187,14 +222,14 @@ func TestStoreYaml_base(t *testing.T) { sTestComposeChart(ctx, t, s, ns.ID, mod.ID, "base") df := su.NewDecodeFilter(). - ComposeNamespace(&ctypes.NamespaceFilter{ + ComposeNamespace(&types.NamespaceFilter{ Slug: "base_namespace", }). - ComposeModule(&ctypes.ModuleFilter{ + ComposeModule(&types.ModuleFilter{ NamespaceID: ns.ID, Handle: "base_module", }). - ComposeChart(&ctypes.ChartFilter{ + ComposeChart(&types.ChartFilter{ NamespaceID: ns.ID, Handle: "base_chart", }) @@ -233,17 +268,17 @@ func TestStoreYaml_base(t *testing.T) { sTestComposeRecord(ctx, t, s, ns.ID, mod.ID, usr.ID) df := su.NewDecodeFilter(). - ComposeNamespace(&ctypes.NamespaceFilter{ + ComposeNamespace(&types.NamespaceFilter{ Slug: "base_namespace", }). - ComposeModule(&ctypes.ModuleFilter{ + ComposeModule(&types.ModuleFilter{ NamespaceID: ns.ID, Handle: "base_module", }). Users(&stypes.UserFilter{ Email: "base_user@test.tld", }). - ComposeRecord(&ctypes.RecordFilter{ + ComposeRecord(&types.RecordFilter{ NamespaceID: ns.ID, ModuleID: mod.ID, }) @@ -257,7 +292,7 @@ func TestStoreYaml_base(t *testing.T) { usr, err := store.LookupUserByHandle(ctx, s, "base_user") req.NoError(err) - rr, _, err := store.SearchComposeRecords(ctx, s, mod, ctypes.RecordFilter{ + rr, _, err := store.SearchComposeRecords(ctx, s, mod, types.RecordFilter{ ModuleID: mod.ID, NamespaceID: ns.ID, }) @@ -293,12 +328,12 @@ func TestStoreYaml_base(t *testing.T) { usr := sTestUser(ctx, t, s, "base") recID := su.NextID() - rec := &ctypes.Record{ + rec := &types.Record{ ID: recID, NamespaceID: ns.ID, ModuleID: mod.ID, - Values: ctypes.RecordValueSet{ + Values: types.RecordValueSet{ { RecordID: recID, Name: "BoolTrue", @@ -353,17 +388,17 @@ func TestStoreYaml_base(t *testing.T) { } df := su.NewDecodeFilter(). - ComposeNamespace(&ctypes.NamespaceFilter{ + ComposeNamespace(&types.NamespaceFilter{ Slug: "base_namespace", }). - ComposeModule(&ctypes.ModuleFilter{ + ComposeModule(&types.ModuleFilter{ NamespaceID: ns.ID, Handle: "base_module", }). Users(&stypes.UserFilter{ Email: "base_user@test.tld", }). - ComposeRecord(&ctypes.RecordFilter{ + ComposeRecord(&types.RecordFilter{ NamespaceID: ns.ID, ModuleID: mod.ID, }) @@ -377,7 +412,7 @@ func TestStoreYaml_base(t *testing.T) { usr, err := store.LookupUserByHandle(ctx, s, "base_user") req.NoError(err) - rr, _, err := store.SearchComposeRecords(ctx, s, mod, ctypes.RecordFilter{ + rr, _, err := store.SearchComposeRecords(ctx, s, mod, types.RecordFilter{ ModuleID: mod.ID, NamespaceID: ns.ID, }) diff --git a/tests/envoy/testdata/base/workflows.yaml b/tests/envoy/testdata/base/workflows.yaml new file mode 100644 index 000000000..63c330c34 --- /dev/null +++ b/tests/envoy/testdata/base/workflows.yaml @@ -0,0 +1,103 @@ +workflows: + testko_wf: + enabled: true + trace: true + keepSessions: 2 + meta: + name: name here + description: description here + + triggers: + - enabled: true + stepID: 101 + resourceType: "compose:module" + eventType: "beforeCreate" + constraints: + - name: name + op: op + values: + - values + + meta: + description: description + visual: + edges: + - childID: 101 + parentID: 102 + meta: + description": "" + label": "test" + visual": + source: 101 + target: 102 + value: "test" + + value: "start" + xywh: [2100, 2160, 200, 80] + + createdAt: "2021-02-23T09:53:41Z" + updatedAt: "2021-02-23T09:53:41Z" + + steps: + - stepID: 101 + kind: "expressions" + arguments: + - target: target + source: source + expr: expr + value: value + type: type + tests: + - expr: expr + error: error + results: + - target: target + source: source + expr: expr + value: value + type: type + tests: + - expr: expr + error: error + meta: + name: name + description: description + visual: + value: "expr 1" + xywh: [2340, 2310, 200, 80] + + - stepID: 102 + kind: "expressions" + arguments: + - target: target + source: source + expr: expr + value: value + type: type + tests: + - expr: expr + error: error + results: + - target: target + source: source + expr: expr + value: value + type: type + tests: + - expr: expr + error: error + meta: + name: name + description: description + visual: + value: "expr 1" + xywh: [2540, 2310, 200, 80] + + paths: + - expr: expr + parentID: 101 + childID: 102 + meta: + name: name + description: description + visual: {} diff --git a/tests/envoy/yaml_store_base_test.go b/tests/envoy/yaml_store_base_test.go index 4016e7cf7..264562ecc 100644 --- a/tests/envoy/yaml_store_base_test.go +++ b/tests/envoy/yaml_store_base_test.go @@ -7,7 +7,8 @@ import ( "strings" "testing" - "github.com/cortezaproject/corteza-server/compose/types" + atypes "github.com/cortezaproject/corteza-server/automation/types" + ctypes "github.com/cortezaproject/corteza-server/compose/types" "github.com/cortezaproject/corteza-server/pkg/auth" "github.com/cortezaproject/corteza-server/pkg/envoy/resource" su "github.com/cortezaproject/corteza-server/pkg/envoy/store" @@ -82,7 +83,7 @@ func TestYamlStore_base(t *testing.T) { req.NoError(err) req.NotNil(mod) - mod.Fields, _, err = store.SearchComposeModuleFields(ctx, s, types.ModuleFieldFilter{ModuleID: []uint64{mod.ID}}) + mod.Fields, _, err = store.SearchComposeModuleFields(ctx, s, ctypes.ModuleFieldFilter{ModuleID: []uint64{mod.ID}}) req.NoError(err) req.Equal("mod1", mod.Handle) @@ -310,6 +311,29 @@ func TestYamlStore_base(t *testing.T) { }, }, + { + name: "workflows", + file: "workflows", + check: func(req *require.Assertions) { + ww, err := store.LookupAutomationWorkflowByHandle(ctx, s, "testko_wf") + req.NoError(err) + req.NotNil(ww) + + req.Equal("testko_wf", ww.Handle) + req.Equal("name here", ww.Meta.Name) + req.Equal("description here", ww.Meta.Description) + req.Len(ww.Steps, 2) + req.Len(ww.Paths, 1) + + tt, _, err := store.SearchAutomationTriggers(ctx, s, atypes.TriggerFilter{ + WorkflowID: []uint64{ww.ID}, + }) + req.NoError(err) + req.NotNil(tt) + req.Len(tt, 1) + }, + }, + { name: "roles", file: "roles", @@ -402,7 +426,7 @@ func TestYamlStore_base(t *testing.T) { req.NoError(err) req.NotNil(m) - rr, _, err := store.SearchComposeRecords(ctx, s, m, types.RecordFilter{ModuleID: m.ID, NamespaceID: m.NamespaceID}) + rr, _, err := store.SearchComposeRecords(ctx, s, m, ctypes.RecordFilter{ModuleID: m.ID, NamespaceID: m.NamespaceID}) req.NoError(err) req.NotNil(rr) req.Len(rr, 1) @@ -439,13 +463,13 @@ func TestYamlStore_base(t *testing.T) { req.NoError(err) req.NotNil(mod2) - rr, _, err := store.SearchComposeRecords(ctx, s, mod1, types.RecordFilter{ModuleID: mod1.ID, NamespaceID: mod1.NamespaceID}) + rr, _, err := store.SearchComposeRecords(ctx, s, mod1, ctypes.RecordFilter{ModuleID: mod1.ID, NamespaceID: mod1.NamespaceID}) req.NoError(err) req.NotNil(rr) req.Len(rr, 1) req.Equal("mod1 f1 v1", rr[0].Values[0].Value) - rr, _, err = store.SearchComposeRecords(ctx, s, mod2, types.RecordFilter{ModuleID: mod2.ID, NamespaceID: mod2.NamespaceID}) + rr, _, err = store.SearchComposeRecords(ctx, s, mod2, ctypes.RecordFilter{ModuleID: mod2.ID, NamespaceID: mod2.NamespaceID}) req.NoError(err) req.NotNil(rr) req.Len(rr, 1) @@ -477,13 +501,13 @@ func TestYamlStore_base(t *testing.T) { req.NoError(err) req.NotNil(mod2) - rr1, _, err := store.SearchComposeRecords(ctx, s, mod1, types.RecordFilter{ModuleID: mod1.ID, NamespaceID: mod1.NamespaceID}) + rr1, _, err := store.SearchComposeRecords(ctx, s, mod1, ctypes.RecordFilter{ModuleID: mod1.ID, NamespaceID: mod1.NamespaceID}) req.NoError(err) req.NotNil(rr1) req.Len(rr1, 1) req.Equal("existing value", rr1[0].Values.FilterByName("f1")[0].Value) - rr2, _, err := store.SearchComposeRecords(ctx, s, mod2, types.RecordFilter{ModuleID: mod2.ID, NamespaceID: mod2.NamespaceID}) + rr2, _, err := store.SearchComposeRecords(ctx, s, mod2, ctypes.RecordFilter{ModuleID: mod2.ID, NamespaceID: mod2.NamespaceID}) req.NoError(err) req.NotNil(rr2) req.Len(rr2, 1)