From eb3c114f8306fda42a177e4e34da6f83bf154c94 Mon Sep 17 00:00:00 2001 From: Guiheux Steven Date: Mon, 9 Mar 2020 11:31:21 +0100 Subject: [PATCH] feat(sdk): add version 2 of workflow export entities (#5021) --- cli/cdsctl/workflow_init.go | 8 +- engine/api/templates.go | 15 +- engine/api/templates_test.go | 4 +- engine/api/user_schema.go | 3 +- engine/api/workflow.go | 12 +- engine/api/workflow/as_code.go | 5 +- engine/api/workflow/dao.go | 11 +- engine/api/workflow/dao_run.go | 4 +- engine/api/workflow/execute_node_run.go | 3 +- engine/api/workflow/process.go | 10 +- engine/api/workflow/process_node.go | 16 +- engine/api/workflow/process_start.go | 3 +- engine/api/workflow/repository.go | 4 +- engine/api/workflow/resync_workflow.go | 2 +- engine/api/workflow/run_workflow.go | 4 +- engine/api/workflow/workflow_exporter.go | 17 +- engine/api/workflow/workflow_parser.go | 36 +- engine/api/workflow/workflow_parser_test.go | 15 +- engine/api/workflow_export.go | 9 +- engine/api/workflow_export_test.go | 5 +- engine/api/workflow_import.go | 127 ++- engine/api/workflow_run.go | 13 +- engine/api/workflow_test.go | 1 + engine/api/workflowtemplate/execute.go | 10 +- sdk/build.go | 1 + sdk/error.go | 3 + sdk/exportentities/v1/workflow.go | 540 +++++++++++ sdk/exportentities/v1/workflow_notif_test.go | 129 +++ .../workflow_notification.go} | 2 +- sdk/exportentities/v1/workflow_test.go | 866 +++++++++++++++++ sdk/exportentities/v2/workflow.go | 734 +++++++++++++++ .../{ => v2}/workflow_notif_test.go | 243 ++--- .../v2/workflow_notification.go | 187 ++++ sdk/exportentities/{ => v2}/workflow_test.go | 377 +++----- sdk/exportentities/workflow.go | 870 ++---------------- sdk/messages.go | 148 ++- sdk/workflow_run.go | 8 +- tests/04_sc_workflow_edit_scheduler.yml | 4 +- tests/fixtures/ITSCWRKFLW10/ITSCWRKFLW10.yml | 2 +- tests/fixtures/ITSCWRKFLW3/ITSCWRKFLW3.yml | 2 +- ui/src/app/model/pipeline.model.ts | 8 +- ui/src/app/views/workflow/run/errors.ts | 7 + .../workflow/run/workflow.run.component.ts | 9 +- .../app/views/workflow/run/workflow.run.html | 45 +- ui/src/assets/i18n/en.json | 1 + ui/src/assets/i18n/fr.json | 1 + 46 files changed, 3031 insertions(+), 1493 deletions(-) create mode 100644 sdk/exportentities/v1/workflow.go create mode 100644 sdk/exportentities/v1/workflow_notif_test.go rename sdk/exportentities/{workflow_notif.go => v1/workflow_notification.go} (99%) create mode 100644 sdk/exportentities/v1/workflow_test.go create mode 100644 sdk/exportentities/v2/workflow.go rename sdk/exportentities/{ => v2}/workflow_notif_test.go (63%) create mode 100644 sdk/exportentities/v2/workflow_notification.go rename sdk/exportentities/{ => v2}/workflow_test.go (70%) diff --git a/cli/cdsctl/workflow_init.go b/cli/cdsctl/workflow_init.go index e613742f9c..67c4de7927 100644 --- a/cli/cdsctl/workflow_init.go +++ b/cli/cdsctl/workflow_init.go @@ -243,13 +243,7 @@ func interactiveChoosePipeline(pkey, defaultPipeline string) (string, *sdk.Pipel func craftWorkflowFile(workflowName, appName, pipName, destinationDir string) (string, error) { // Crafting the workflow - wkflw := exportentities.Workflow{ - Version: exportentities.WorkflowVersion1, - Name: workflowName, - ApplicationName: appName, - PipelineName: pipName, - } - + wkflw := exportentities.InitWorkflow(workflowName, appName, pipName) b, err := exportentities.Marshal(wkflw, exportentities.FormatYAML) if err != nil { return "", fmt.Errorf("Unable to write workflow file format: %v", err) diff --git a/engine/api/templates.go b/engine/api/templates.go index f8238656cf..e6f0fa4a14 100644 --- a/engine/api/templates.go +++ b/engine/api/templates.go @@ -353,21 +353,24 @@ func (api *API) applyTemplate(ctx context.Context, u sdk.Identifiable, p *sdk.Pr // parse the generated workflow to find its name an update it in instance if not detached // also set the template path in generated workflow if not detached if !req.Detached { - var wor exportentities.Workflow - if err := yaml.Unmarshal([]byte(result.Workflow), &wor); err != nil { + wor, err := exportentities.UnmarshalWorkflow([]byte(result.Workflow)) + if err != nil { return result, sdk.NewError(sdk.Error{ ID: sdk.ErrWrongRequest.ID, Message: "Cannot parse generated workflow", }, err) } - wti.WorkflowName = wor.Name + wti.WorkflowName = wor.GetName() if err := workflowtemplate.UpdateInstance(tx, wti); err != nil { return result, err } templatePath := fmt.Sprintf("%s/%s", wt.Group.Name, wt.Slug) - wor.Template = &templatePath + wor, err = exportentities.SetTemplate(wor, templatePath) + if err != nil { + return result, err + } b, err := yaml.Marshal(wor) if err != nil { return result, sdk.NewError(sdk.Error{ @@ -455,7 +458,7 @@ func (api *API) postTemplateApplyHandler() service.Handler { log.Debug("postTemplateApplyHandler> template %s applied (withImport=%v)", wt.Slug, withImport) buf := new(bytes.Buffer) - if err := workflowtemplate.Tar(ctx, wt, res, buf); err != nil { + if err := workflowtemplate.Tar(ctx, res, buf); err != nil { return err } @@ -607,7 +610,7 @@ func (api *API) postTemplateBulkHandler() service.Handler { } buf := new(bytes.Buffer) - if err := workflowtemplate.Tar(ctx, wt, res, buf); err != nil { + if err := workflowtemplate.Tar(ctx, res, buf); err != nil { if errD := errorDefer(err); errD != nil { log.Error(ctx, "%v", errD) return diff --git a/engine/api/templates_test.go b/engine/api/templates_test.go index 11139a1c24..bdcb1cba0f 100644 --- a/engine/api/templates_test.go +++ b/engine/api/templates_test.go @@ -27,7 +27,7 @@ func generateTemplate(groupID int64, pipelineName string) *sdk.WorkflowTemplate Slug: slug.Convert(name), Workflow: base64.StdEncoding.EncodeToString([]byte( `name: [[.name]] -version: v1.0 +version: v2.0 workflow: Node-1: pipeline: ` + pipelineName, @@ -150,7 +150,7 @@ func Test_postTemplateBulkHandler(t *testing.T) { Slug: slug.Convert(name), Workflow: base64.StdEncoding.EncodeToString([]byte( `name: [[.name]] -version: v1.0 +version: v2.0 workflow: Node-1: pipeline: ` + pipelineName, diff --git a/engine/api/user_schema.go b/engine/api/user_schema.go index 5d46d900c9..7bb89dd03d 100644 --- a/engine/api/user_schema.go +++ b/engine/api/user_schema.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + v2 "github.com/ovh/cds/sdk/exportentities/v2" "net/http" "reflect" @@ -30,7 +31,7 @@ func (api *API) getUserJSONSchema() service.Handler { var sch *jsonschema.Schema if filter == "" || filter == "workflow" { - sch = ref.ReflectFromType(reflect.TypeOf(exportentities.Workflow{})) + sch = ref.ReflectFromType(reflect.TypeOf(v2.Workflow{})) buf, _ := json.Marshal(sch) res.Workflow = string(buf) } diff --git a/engine/api/workflow.go b/engine/api/workflow.go index da769524f2..5f68816c69 100644 --- a/engine/api/workflow.go +++ b/engine/api/workflow.go @@ -12,8 +12,6 @@ import ( "github.com/go-gorp/gorp" "github.com/gorilla/mux" - yaml "gopkg.in/yaml.v2" - "github.com/ovh/cds/engine/api/application" "github.com/ovh/cds/engine/api/environment" "github.com/ovh/cds/engine/api/event" @@ -210,8 +208,8 @@ func (api *API) postWorkflowRollbackHandler() service.Handler { return sdk.WrapError(err, "cannot load workflow audit %s/%s", key, workflowName) } - var exportWf exportentities.Workflow - if err := yaml.Unmarshal([]byte(audit.DataBefore), &exportWf); err != nil { + exportWf, err := exportentities.UnmarshalWorkflow([]byte(audit.DataBefore)) + if err != nil { return sdk.WrapError(err, "cannot unmarshal data before") } @@ -223,9 +221,9 @@ func (api *API) postWorkflowRollbackHandler() service.Handler { _ = tx.Rollback() }() - newWf, _, err := workflow.ParseAndImport(ctx, tx, api.Cache, *proj, wf, &exportWf, u, workflow.ImportOptions{Force: true, WorkflowName: workflowName}) - if err != nil { - return sdk.WrapError(err, "cannot parse and import previous workflow") + newWf, _, errP := workflow.ParseAndImport(ctx, tx, api.Cache, *proj, wf, exportWf, u, workflow.ImportOptions{Force: true, WorkflowName: workflowName}) + if errP != nil { + return sdk.WrapError(errP, "cannot parse and import previous workflow") } if err := tx.Commit(); err != nil { diff --git a/engine/api/workflow/as_code.go b/engine/api/workflow/as_code.go index baae769776..563e59db9b 100644 --- a/engine/api/workflow/as_code.go +++ b/engine/api/workflow/as_code.go @@ -5,6 +5,7 @@ import ( "context" "encoding/base64" "fmt" + v2 "github.com/ovh/cds/sdk/exportentities/v2" "time" "github.com/go-gorp/gorp" @@ -26,7 +27,7 @@ func UpdateWorkflowAsCode(ctx context.Context, store cache.Store, db gorp.SqlExe var wp exportentities.WorkflowPulled buffw := new(bytes.Buffer) - if _, err := exportWorkflow(ctx, wf, exportentities.FormatYAML, buffw, exportentities.WorkflowSkipIfOnlyOneRepoWebhook); err != nil { + if _, err := exportWorkflow(ctx, wf, exportentities.FormatYAML, buffw, v2.WorkflowSkipIfOnlyOneRepoWebhook); err != nil { return nil, sdk.WrapError(err, "unable to export workflow") } wp.Workflow.Name = wf.Name @@ -47,7 +48,7 @@ func MigrateAsCode(ctx context.Context, db *gorp.DbMap, store cache.Store, proj } // Export workflow - pull, err := Pull(ctx, db, store, proj, wf.Name, exportentities.FormatYAML, encryptFunc, exportentities.WorkflowSkipIfOnlyOneRepoWebhook) + pull, err := Pull(ctx, db, store, proj, wf.Name, exportentities.FormatYAML, encryptFunc, v2.WorkflowSkipIfOnlyOneRepoWebhook) if err != nil { return nil, sdk.WrapError(err, "cannot pull workflow") } diff --git a/engine/api/workflow/dao.go b/engine/api/workflow/dao.go index 7bc9fe8a7b..c303b40107 100644 --- a/engine/api/workflow/dao.go +++ b/engine/api/workflow/dao.go @@ -1329,7 +1329,7 @@ func checkApplication(store cache.Store, db gorp.SqlExecutor, proj sdk.Project, if n.Context.ApplicationName != "" { appDB, err := application.LoadByName(db, store, proj.Key, n.Context.ApplicationName, application.LoadOptions.WithDeploymentStrategies, application.LoadOptions.WithVariables) if err != nil { - if sdk.ErrorIs(err, sdk.ErrPipelineNotFound) { + if sdk.ErrorIs(err, sdk.ErrApplicationNotFound) { return sdk.WithStack(sdk.ErrorWithData(sdk.ErrApplicationNotFound, n.Context.ApplicationName)) } return sdk.WrapError(err, "unable to load application %s", n.Context.ApplicationName) @@ -1390,12 +1390,13 @@ func Push(ctx context.Context, db *gorp.DbMap, store cache.Store, proj *sdk.Proj oldWf = opts.OldWorkflow } else { // load the workflow from database if exists - workflowExists, err = Exists(db, proj.Key, data.wrkflw.Name) + workflowExists, err = Exists(db, proj.Key, data.wrkflw.GetName()) if err != nil { return nil, nil, nil, sdk.WrapError(err, "Cannot check if workflow exists") } if workflowExists { - oldWf, err = Load(ctx, db, store, *proj, data.wrkflw.Name, LoadOptions{WithIcon: true}) + oldWf, err = Load(ctx, db, store, *proj, data.wrkflw.GetName(), LoadOptions{WithIcon: true}) + if err != nil { return nil, nil, nil, sdk.WrapError(err, "Unable to load existing workflow") } @@ -1477,9 +1478,9 @@ func Push(ctx context.Context, db *gorp.DbMap, store cache.Store, proj *sdk.Proj importOptions.HookUUID = opts.HookUUID } - wf, msgList, err := ParseAndImport(ctx, tx, store, *proj, oldWf, &data.wrkflw, u, importOptions) + wf, msgList, err := ParseAndImport(ctx, tx, store, *proj, oldWf, data.wrkflw, u, importOptions) if err != nil { - return msgList, nil, nil, sdk.WrapError(err, "unable to import workflow %s", data.wrkflw.Name) + return msgList, nil, nil, sdk.WrapError(err, "unable to import workflow %s", data.wrkflw.GetName()) } // If the workflow is "as-code", it should always be linked to a git repository diff --git a/engine/api/workflow/dao_run.go b/engine/api/workflow/dao_run.go index 330c1ec720..88688df750 100644 --- a/engine/api/workflow/dao_run.go +++ b/engine/api/workflow/dao_run.go @@ -63,8 +63,8 @@ func UpdateWorkflowRun(ctx context.Context, db gorp.SqlExecutor, wr *sdk.Workflo wr.LastModified = time.Now() for _, info := range wr.Infos { - if info.IsError && info.SubNumber == wr.LastSubNumber { - wr.Status = string(sdk.StatusFail) + if info.Type == sdk.RunInfoTypeError && info.SubNumber == wr.LastSubNumber { + wr.Status = sdk.StatusFail } } diff --git a/engine/api/workflow/execute_node_run.go b/engine/api/workflow/execute_node_run.go index c67113e017..7ac9a3abae 100644 --- a/engine/api/workflow/execute_node_run.go +++ b/engine/api/workflow/execute_node_run.go @@ -347,9 +347,10 @@ func executeNodeRun(ctx context.Context, db gorp.SqlExecutor, store cache.Store, log.Error(ctx, "workflow.execute> Unable to load mutex-locked workflow rnode un: %v", errWRun) return report, nil } - AddWorkflowRunInfo(workflowRun, false, sdk.SpawnMsg{ + AddWorkflowRunInfo(workflowRun, sdk.SpawnMsg{ ID: sdk.MsgWorkflowNodeMutexRelease.ID, Args: []interface{}{waitingRun.WorkflowNodeName}, + Type: sdk.MsgWorkflowNodeMutexRelease.Type, }) if err := UpdateWorkflowRun(ctx, db, workflowRun); err != nil { diff --git a/engine/api/workflow/process.go b/engine/api/workflow/process.go index 8d70e0965e..fc786a6b47 100644 --- a/engine/api/workflow/process.go +++ b/engine/api/workflow/process.go @@ -51,9 +51,10 @@ func checkCondition(ctx context.Context, wr *sdk.WorkflowRun, conditions sdk.Wor luacheck, err := luascript.NewCheck() if err != nil { log.Warning(ctx, "processWorkflowNodeRun> WorkflowCheckConditions error: %s", err) - AddWorkflowRunInfo(wr, true, sdk.SpawnMsg{ + AddWorkflowRunInfo(wr, sdk.SpawnMsg{ ID: sdk.MsgWorkflowError.ID, Args: []interface{}{fmt.Sprintf("Error init LUA System: %v", err)}, + Type: sdk.MsgWorkflowError.Type, }) } luacheck.SetVariables(sdk.ParametersToMap(params)) @@ -62,9 +63,10 @@ func checkCondition(ctx context.Context, wr *sdk.WorkflowRun, conditions sdk.Wor } if errc != nil { log.Warning(ctx, "processWorkflowNodeRun> WorkflowCheckConditions error: %s", errc) - AddWorkflowRunInfo(wr, true, sdk.SpawnMsg{ + AddWorkflowRunInfo(wr, sdk.SpawnMsg{ ID: sdk.MsgWorkflowError.ID, Args: []interface{}{fmt.Sprintf("Error on LUA Condition: %v", errc)}, + Type: sdk.MsgWorkflowError.Type, }) return false } @@ -72,12 +74,12 @@ func checkCondition(ctx context.Context, wr *sdk.WorkflowRun, conditions sdk.Wor } // AddWorkflowRunInfo add WorkflowRunInfo on a WorkflowRun -func AddWorkflowRunInfo(run *sdk.WorkflowRun, isError bool, infos ...sdk.SpawnMsg) { +func AddWorkflowRunInfo(run *sdk.WorkflowRun, infos ...sdk.SpawnMsg) { for _, i := range infos { run.Infos = append(run.Infos, sdk.WorkflowRunInfo{ APITime: time.Now(), Message: i, - IsError: isError, + Type: i.Type, SubNumber: run.LastSubNumber, }) } diff --git a/engine/api/workflow/process_node.go b/engine/api/workflow/process_node.go index 168be13f7b..104a3df9c9 100644 --- a/engine/api/workflow/process_node.go +++ b/engine/api/workflow/process_node.go @@ -37,9 +37,10 @@ func processNodeTriggers(ctx context.Context, db gorp.SqlExecutor, store cache.S r1, _, errPwnr := processNodeRun(ctx, db, store, proj, wr, mapNodes, &t.ChildNode, int(parentSubNumber), parentNodeRun, nil, nil) if errPwnr != nil { log.Error(ctx, "processWorkflowRun> Unable to process node ID=%d: %s", t.ChildNode.ID, errPwnr) - AddWorkflowRunInfo(wr, true, sdk.SpawnMsg{ + AddWorkflowRunInfo(wr, sdk.SpawnMsg{ ID: sdk.MsgWorkflowError.ID, Args: []interface{}{errPwnr.Error()}, + Type: sdk.MsgWorkflowError.Type, }) } _, _ = report.Merge(ctx, r1, nil) @@ -240,9 +241,10 @@ func processNode(ctx context.Context, db gorp.SqlExecutor, store cache.Store, pr vcsServer := repositoriesmanager.GetProjectVCSServer(proj, app.VCSServer) vcsInf, errVcs = getVCSInfos(ctx, db, store, proj.Key, vcsServer, currentJobGitValues, app.Name, app.VCSServer, app.RepositoryFullname) if errVcs != nil { - AddWorkflowRunInfo(wr, true, sdk.SpawnMsg{ + AddWorkflowRunInfo(wr, sdk.SpawnMsg{ ID: sdk.MsgWorkflowError.ID, Args: []interface{}{errVcs.Error()}, + Type: sdk.MsgWorkflowError.Type, }) return nil, false, sdk.WrapError(errVcs, "unable to get git informations") } @@ -298,8 +300,8 @@ func processNode(ctx context.Context, db gorp.SqlExecutor, store cache.Store, pr } for _, info := range wr.Infos { - if info.IsError && info.SubNumber == wr.LastSubNumber { - nr.Status = string(sdk.StatusFail) + if info.Type == sdk.RunInfoTypeError && info.SubNumber == wr.LastSubNumber { + nr.Status = sdk.StatusFail nr.Done = time.Now() break } @@ -365,9 +367,10 @@ func processNode(ctx context.Context, db gorp.SqlExecutor, store cache.Store, pr } if nbMutex > 0 { log.Debug("Noderun %s processed but not executed because of mutex", n.Name) - AddWorkflowRunInfo(wr, false, sdk.SpawnMsg{ + AddWorkflowRunInfo(wr, sdk.SpawnMsg{ ID: sdk.MsgWorkflowNodeMutex.ID, Args: []interface{}{n.Name}, + Type: sdk.MsgWorkflowNodeMutex.Type, }) if err := UpdateWorkflowRun(ctx, db, wr); err != nil { return nil, false, sdk.WrapError(err, "unable to update workflow run") @@ -479,9 +482,10 @@ func computePayload(n *sdk.Node, hookEvent *sdk.WorkflowNodeRunHookEvent, manual func computeNodeContextBuildParameters(ctx context.Context, proj sdk.Project, wr *sdk.WorkflowRun, run *sdk.WorkflowNodeRun, n *sdk.Node, runContext nodeRunContext) { nodeRunParams, errParam := getNodeRunBuildParameters(ctx, proj, wr, run, runContext) if errParam != nil { - AddWorkflowRunInfo(wr, true, sdk.SpawnMsg{ + AddWorkflowRunInfo(wr, sdk.SpawnMsg{ ID: sdk.MsgWorkflowError.ID, Args: []interface{}{errParam.Error()}, + Type: sdk.MsgWorkflowError.Type, }) // if there an error -> display it in workflowRunInfo and not stop the launch log.Error(ctx, "processNode> getNodeRunBuildParameters failed. Project:%s [#%d.%d]%s.%d with payload %v err:%v", proj.Name, wr.Number, run.SubNumber, wr.Workflow.Name, n.ID, run.Payload, errParam) diff --git a/engine/api/workflow/process_start.go b/engine/api/workflow/process_start.go index b0e4a317ad..43c43a6bf9 100644 --- a/engine/api/workflow/process_start.go +++ b/engine/api/workflow/process_start.go @@ -55,12 +55,13 @@ func processStartFromRootNode(ctx context.Context, db gorp.SqlExecutor, store ca log.Debug("processWorkflowRun> starting from the root: %d (pipeline %s)", wr.Workflow.WorkflowData.Node.ID, wr.Workflow.Pipelines[wr.Workflow.WorkflowData.Node.Context.PipelineID].Name) report := new(ProcessorReport) //Run the root: manual or from an event - AddWorkflowRunInfo(wr, false, sdk.SpawnMsg{ + AddWorkflowRunInfo(wr, sdk.SpawnMsg{ ID: sdk.MsgWorkflowStarting.ID, Args: []interface{}{ wr.Workflow.Name, fmt.Sprintf("%d.%d", wr.Number, 0), }, + Type: sdk.MsgWorkflowStarting.Type, }) r1, conditionOK, errP := processNodeRun(ctx, db, store, proj, wr, mapNodes, &wr.Workflow.WorkflowData.Node, 0, nil, hookEvent, manual) diff --git a/engine/api/workflow/repository.go b/engine/api/workflow/repository.go index 69e0b45834..f2f00a4a46 100644 --- a/engine/api/workflow/repository.go +++ b/engine/api/workflow/repository.go @@ -207,7 +207,9 @@ func extractFromCDSFiles(ctx context.Context, tr *tar.Reader) (*exportedEntities mError.Append(fmt.Errorf("two workflows files found: %s and %s", workflowFileName, hdr.Name)) break } - if err := yaml.Unmarshal(b, &res.wrkflw); err != nil { + var err error + res.wrkflw, err = exportentities.UnmarshalWorkflow(b) + if err != nil { log.Error(ctx, "Push> Unable to unmarshal workflow %s: %v", hdr.Name, err) mError.Append(fmt.Errorf("Unable to unmarshal workflow %s: %v", hdr.Name, err)) continue diff --git a/engine/api/workflow/resync_workflow.go b/engine/api/workflow/resync_workflow.go index a705b23227..104eb6770e 100644 --- a/engine/api/workflow/resync_workflow.go +++ b/engine/api/workflow/resync_workflow.go @@ -61,7 +61,7 @@ func ResyncWorkflowRunStatus(ctx context.Context, db gorp.SqlExecutor, wr *sdk.W var isInError bool var newStatus string for _, info := range wr.Infos { - if info.IsError && info.SubNumber == wr.LastSubNumber { + if info.Type == sdk.RunInfoTypeError && info.SubNumber == wr.LastSubNumber { isInError = true break } diff --git a/engine/api/workflow/run_workflow.go b/engine/api/workflow/run_workflow.go index 565a4ed02f..2e2376c624 100644 --- a/engine/api/workflow/run_workflow.go +++ b/engine/api/workflow/run_workflow.go @@ -59,7 +59,7 @@ func runFromHook(ctx context.Context, db gorp.SqlExecutor, store cache.Store, pr // Add add code spawn info for _, msg := range asCodeMsg { - AddWorkflowRunInfo(wr, false, sdk.SpawnMsg{ID: msg.ID, Args: msg.Args}) + AddWorkflowRunInfo(wr, sdk.SpawnMsg{ID: msg.ID, Args: msg.Args, Type: msg.Type}) } //Process it @@ -107,7 +107,7 @@ func StartWorkflowRun(ctx context.Context, db *gorp.DbMap, store cache.Store, pr defer tx.Rollback() // nolint for _, msg := range asCodeInfos { - AddWorkflowRunInfo(wr, false, sdk.SpawnMsg{ID: msg.ID, Args: msg.Args}) + AddWorkflowRunInfo(wr, sdk.SpawnMsg{ID: msg.ID, Args: msg.Args, Type: msg.Type}) } wr.Status = sdk.StatusWaiting diff --git a/engine/api/workflow/workflow_exporter.go b/engine/api/workflow/workflow_exporter.go index cfbfcf769b..1ab0da9d5c 100644 --- a/engine/api/workflow/workflow_exporter.go +++ b/engine/api/workflow/workflow_exporter.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/base64" + v2 "github.com/ovh/cds/sdk/exportentities/v2" "io" "github.com/go-gorp/gorp" @@ -18,7 +19,7 @@ import ( ) // Export a workflow -func Export(ctx context.Context, db gorp.SqlExecutor, cache cache.Store, proj sdk.Project, name string, f exportentities.Format, w io.Writer, opts ...exportentities.WorkflowOptions) (int, error) { +func Export(ctx context.Context, db gorp.SqlExecutor, cache cache.Store, proj sdk.Project, name string, f exportentities.Format, w io.Writer, opts ...v2.ExportOptions) (int, error) { ctx, end := observability.Span(ctx, "workflow.Export") defer end() @@ -29,23 +30,18 @@ func Export(ctx context.Context, db gorp.SqlExecutor, cache cache.Store, proj sd // If repo is from as-code do not export WorkflowSkipIfOnlyOneRepoWebhook if wf.FromRepository != "" { - opts = append(opts, exportentities.WorkflowSkipIfOnlyOneRepoWebhook) + opts = append(opts, v2.WorkflowSkipIfOnlyOneRepoWebhook) } return exportWorkflow(ctx, *wf, f, w, opts...) } -func exportWorkflow(ctx context.Context, wf sdk.Workflow, f exportentities.Format, w io.Writer, opts ...exportentities.WorkflowOptions) (int, error) { +func exportWorkflow(ctx context.Context, wf sdk.Workflow, f exportentities.Format, w io.Writer, opts ...v2.ExportOptions) (int, error) { e, err := exportentities.NewWorkflow(ctx, wf, opts...) if err != nil { return 0, sdk.WrapError(err, "exportWorkflow") } - // Useful to not display history_length in yaml or json if it's his default value - if e.HistoryLength != nil && *e.HistoryLength == sdk.DefaultHistoryLength { - e.HistoryLength = nil - } - // Marshal to the desired format b, err := exportentities.Marshal(e, f) if err != nil { @@ -57,7 +53,8 @@ func exportWorkflow(ctx context.Context, wf sdk.Workflow, f exportentities.Forma // Pull a workflow with all it dependencies; it writes a tar buffer in the writer func Pull(ctx context.Context, db gorp.SqlExecutor, cache cache.Store, proj sdk.Project, name string, f exportentities.Format, - encryptFunc sdk.EncryptFunc, opts ...exportentities.WorkflowOptions) (exportentities.WorkflowPulled, error) { + encryptFunc sdk.EncryptFunc, opts ...v2.ExportOptions) (exportentities.WorkflowPulled, error) { + ctx, end := observability.Span(ctx, "workflow.Pull") defer end() @@ -109,7 +106,7 @@ func Pull(ctx context.Context, db gorp.SqlExecutor, cache cache.Store, proj sdk. buffw := new(bytes.Buffer) // If the repository is "as-code", hide the hook if wf.FromRepository != "" { - opts = append(opts, exportentities.WorkflowSkipIfOnlyOneRepoWebhook) + opts = append(opts, v2.WorkflowSkipIfOnlyOneRepoWebhook) } if _, err := exportWorkflow(ctx, *wf, f, buffw, opts...); err != nil { return wp, sdk.WrapError(err, "unable to export workflow") diff --git a/engine/api/workflow/workflow_parser.go b/engine/api/workflow/workflow_parser.go index e54bbaadef..9bbd9a9f27 100644 --- a/engine/api/workflow/workflow_parser.go +++ b/engine/api/workflow/workflow_parser.go @@ -27,41 +27,35 @@ type ImportOptions struct { } // Parse parse an exportentities.workflow and return the parsed workflow -func Parse(ctx context.Context, proj sdk.Project, ew *exportentities.Workflow) (*sdk.Workflow, error) { - log.Info(ctx, "Parse>> Parse workflow %s in project %s", ew.Name, proj.Key) +func Parse(ctx context.Context, proj sdk.Project, ew exportentities.Workflow) (*sdk.Workflow, error) { + log.Info(ctx, "Parse>> Parse workflow %s in project %s", ew.GetName(), proj.Key) log.Debug("Parse>> Workflow: %+v", ew) - //Check valid application name - rx := sdk.NamePatternRegex - if !rx.MatchString(ew.Name) { - return nil, sdk.WrapError(sdk.ErrInvalidApplicationPattern, "Workflow name %s do not respect pattern %s", ew.Name, sdk.NamePattern) - } - - //Inherit permissions from project - if len(ew.Permissions) == 0 { - ew.Permissions = make(map[string]int) - for _, p := range proj.ProjectGroups { - ew.Permissions[p.Group.Name] = p.Permission - } - } - //Parse workflow - w, errW := ew.GetWorkflow() + w, errW := exportentities.ParseWorkflow(ew) if errW != nil { return nil, sdk.NewError(sdk.ErrWrongRequest, errW) } w.ProjectID = proj.ID w.ProjectKey = proj.Key + // Get permission from project if needed + if len(w.Groups) == 0 { + w.Groups = make([]sdk.GroupPermission, 0, len(proj.ProjectGroups)) + for _, gp := range proj.ProjectGroups { + perm := sdk.GroupPermission{Group: sdk.Group{Name: gp.Group.Name}, Permission: gp.Permission} + w.Groups = append(w.Groups, perm) + } + } return w, nil } // ParseAndImport parse an exportentities.workflow and insert or update the workflow in database -func ParseAndImport(ctx context.Context, db gorp.SqlExecutor, store cache.Store, proj sdk.Project, oldW *sdk.Workflow, ew *exportentities.Workflow, u sdk.Identifiable, opts ImportOptions) (*sdk.Workflow, []sdk.Message, error) { +func ParseAndImport(ctx context.Context, db gorp.SqlExecutor, store cache.Store, proj sdk.Project, oldW *sdk.Workflow, ew exportentities.Workflow, u sdk.Identifiable, opts ImportOptions) (*sdk.Workflow, []sdk.Message, error) { ctx, end := observability.Span(ctx, "workflow.ParseAndImport") defer end() - log.Info(ctx, "ParseAndImport>> Import workflow %s in project %s (force=%v)", ew.Name, proj.Key, opts.Force) + log.Info(ctx, "ParseAndImport>> Import workflow %s in project %s (force=%v)", ew.GetName(), proj.Key, opts.Force) //Parse workflow w, errW := Parse(ctx, proj, ew) @@ -193,5 +187,9 @@ func ParseAndImport(ctx context.Context, db gorp.SqlExecutor, store cache.Store, close(msgChan) done.Wait() + if ew.GetVersion() == exportentities.WorkflowVersion1 { + msgList = append(msgList, sdk.NewMessage(sdk.MsgWorkflowDeprecatedVersion, proj.Key, ew.GetName())) + } + return w, msgList, globalError } diff --git a/engine/api/workflow/workflow_parser_test.go b/engine/api/workflow/workflow_parser_test.go index f47c2958bc..53576be709 100644 --- a/engine/api/workflow/workflow_parser_test.go +++ b/engine/api/workflow/workflow_parser_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + v2 "github.com/ovh/cds/sdk/exportentities/v2" "io/ioutil" "net/http" "testing" @@ -62,9 +63,10 @@ func TestParseAndImport(t *testing.T) { //Reload project proj, _ = project.Load(db, cache, proj.Key, project.LoadOptions.WithApplications, project.LoadOptions.WithEnvironments, project.LoadOptions.WithPipelines) - input := &exportentities.Workflow{ - Name: sdk.RandomString(10), - Workflow: map[string]exportentities.NodeEntry{ + input := v2.Workflow{ + Name: sdk.RandomString(10), + Version: exportentities.WorkflowVersion2, + Workflow: map[string]v2.NodeEntry{ "root": { PipelineName: pipelineName, ApplicationName: app.Name, @@ -246,9 +248,10 @@ func TestParseAndImportFromRepository(t *testing.T) { //Reload project proj, _ = project.Load(db, cache, proj.Key, project.LoadOptions.WithApplications, project.LoadOptions.WithEnvironments, project.LoadOptions.WithPipelines) - input := &exportentities.Workflow{ - Name: sdk.RandomString(10), - Workflow: map[string]exportentities.NodeEntry{ + input := v2.Workflow{ + Name: sdk.RandomString(10), + Version: exportentities.WorkflowVersion2, + Workflow: map[string]v2.NodeEntry{ "root": { PipelineName: pipelineName, ApplicationName: app.Name, diff --git a/engine/api/workflow_export.go b/engine/api/workflow_export.go index 728016b410..618c14f25b 100644 --- a/engine/api/workflow_export.go +++ b/engine/api/workflow_export.go @@ -3,6 +3,7 @@ package api import ( "bytes" "context" + v2 "github.com/ovh/cds/sdk/exportentities/v2" "io" "net/http" @@ -27,9 +28,9 @@ func (api *API) getWorkflowExportHandler() service.Handler { } withPermissions := FormBool(r, "withPermissions") - opts := []exportentities.WorkflowOptions{} + opts := make([]v2.ExportOptions, 0) if withPermissions { - opts = append(opts, exportentities.WorkflowWithPermissions) + opts = append(opts, v2.WorkflowWithPermissions) } f, err := exportentities.GetFormat(format) @@ -58,9 +59,9 @@ func (api *API) getWorkflowPullHandler() service.Handler { name := vars["permWorkflowName"] withPermissions := FormBool(r, "withPermissions") - opts := []exportentities.WorkflowOptions{} + opts := make([]v2.ExportOptions, 0) if withPermissions { - opts = append(opts, exportentities.WorkflowWithPermissions) + opts = append(opts, v2.WorkflowWithPermissions) } proj, err := project.Load(api.mustDB(), api.Cache, key, project.LoadOptions.WithIntegrations) diff --git a/engine/api/workflow_export_test.go b/engine/api/workflow_export_test.go index 7a6fcab25a..266c18e152 100644 --- a/engine/api/workflow_export_test.go +++ b/engine/api/workflow_export_test.go @@ -130,7 +130,7 @@ func Test_getWorkflowExportHandler(t *testing.T) { t.Logf(">>%s", rec.Body.String()) assert.Equal(t, `name: test_1 -version: v1.0 +version: v2.0 workflow: fork: depends_on: @@ -165,6 +165,7 @@ func Test_getWorkflowExportHandlerWithPermissions(t *testing.T) { group2 := &sdk.Group{ Name: "Test_getWorkflowExportHandlerWithPermissions-Group2", } + require.NoError(t, group.Insert(context.TODO(), api.mustDB(), group2)) group2, _ = group.LoadByName(context.TODO(), api.mustDB(), "Test_getWorkflowExportHandlerWithPermissions-Group2") @@ -263,7 +264,7 @@ func Test_getWorkflowExportHandlerWithPermissions(t *testing.T) { t.Logf(">>%s", rec.Body.String()) assert.Equal(t, `name: test_1 -version: v1.0 +version: v2.0 workflow: pip1: pipeline: pip1 diff --git a/engine/api/workflow_import.go b/engine/api/workflow_import.go index 311edcabc0..17dfbb989c 100644 --- a/engine/api/workflow_import.go +++ b/engine/api/workflow_import.go @@ -4,14 +4,11 @@ import ( "archive/tar" "bytes" "context" - "encoding/json" "fmt" "io/ioutil" "net/http" "github.com/gorilla/mux" - "gopkg.in/yaml.v2" - "github.com/ovh/cds/engine/api/event" "github.com/ovh/cds/engine/api/observability" "github.com/ovh/cds/engine/api/project" @@ -29,19 +26,6 @@ func (api *API) postWorkflowPreviewHandler() service.Handler { vars := mux.Vars(r) key := vars[permProjectKey] - //Load project - proj, errp := project.Load(api.mustDB(), api.Cache, key, - project.LoadOptions.WithGroups, - project.LoadOptions.WithApplications, - project.LoadOptions.WithEnvironments, - project.LoadOptions.WithPipelines, - project.LoadOptions.WithIntegrations, - project.LoadOptions.WithApplicationWithDeploymentStrategies, - ) - if errp != nil { - return sdk.WrapError(errp, "postWorkflowPreviewHandler>> Unable load project") - } - body, errr := ioutil.ReadAll(r.Body) if errr != nil { return sdk.NewError(sdk.ErrWrongRequest, errr) @@ -53,24 +37,31 @@ func (api *API) postWorkflowPreviewHandler() service.Handler { contentType = http.DetectContentType(body) } - var ew = new(exportentities.Workflow) - var errw error - switch contentType { - case "application/json": - errw = json.Unmarshal(body, ew) - case "application/x-yaml", "text/x-yaml": - errw = yaml.Unmarshal(body, ew) - default: - return sdk.NewError(sdk.ErrWrongRequest, fmt.Errorf("unsupported content-type: %s", contentType)) + if contentType != "application/x-yaml" && contentType != "text/x-yaml" { + return sdk.NewErrorFrom(sdk.ErrUnsupportedMediaType, fmt.Sprintf("unsupported content-type: %s", contentType)) + } + + //Load project + proj, errp := project.Load(api.mustDB(), api.Cache, key, + project.LoadOptions.WithGroups, + project.LoadOptions.WithApplications, + project.LoadOptions.WithEnvironments, + project.LoadOptions.WithPipelines, + project.LoadOptions.WithIntegrations, + project.LoadOptions.WithApplicationWithDeploymentStrategies, + ) + if errp != nil { + return sdk.WrapError(errp, "postWorkflowPreviewHandler>> Unable load project") } + ew, errw := exportentities.UnmarshalWorkflow(body) if errw != nil { return sdk.NewError(sdk.ErrWrongRequest, errw) } wf, globalError := workflow.Parse(ctx, *proj, ew) if globalError != nil { - return sdk.WrapError(globalError, "unable import workflow %s", ew.Name) + return sdk.WrapError(globalError, "unable import workflow %s", ew.GetName()) } // Browse all node to find IDs @@ -93,19 +84,6 @@ func (api *API) postWorkflowImportHandler() service.Handler { key := vars[permProjectKey] force := FormBool(r, "force") - //Load project - proj, errp := project.Load(api.mustDB(), api.Cache, key, - project.LoadOptions.WithGroups, - project.LoadOptions.WithApplications, - project.LoadOptions.WithEnvironments, - project.LoadOptions.WithPipelines, - project.LoadOptions.WithApplicationWithDeploymentStrategies, - project.LoadOptions.WithIntegrations, - ) - if errp != nil { - return sdk.WrapError(errp, "Unable load project") - } - body, errr := ioutil.ReadAll(r.Body) if errr != nil { return sdk.NewError(sdk.ErrWrongRequest, errr) @@ -117,17 +95,24 @@ func (api *API) postWorkflowImportHandler() service.Handler { contentType = http.DetectContentType(body) } - var ew = new(exportentities.Workflow) - var errw error - switch contentType { - case "application/json": - errw = json.Unmarshal(body, ew) - case "application/x-yaml", "text/x-yaml": - errw = yaml.Unmarshal(body, ew) - default: - return sdk.WrapError(sdk.ErrWrongRequest, "Unsupported content-type: %s", contentType) + if contentType != "application/x-yaml" && contentType != "text/x-yaml" { + return sdk.NewErrorFrom(sdk.ErrUnsupportedMediaType, fmt.Sprintf("unsupported content-type: %s", contentType)) } + //Load project + proj, errp := project.Load(api.mustDB(), api.Cache, key, + project.LoadOptions.WithGroups, + project.LoadOptions.WithApplications, + project.LoadOptions.WithEnvironments, + project.LoadOptions.WithPipelines, + project.LoadOptions.WithApplicationWithDeploymentStrategies, + project.LoadOptions.WithIntegrations, + ) + if errp != nil { + return sdk.WrapError(errp, "Unable load project") + } + + ew, errw := exportentities.UnmarshalWorkflow(body) if errw != nil { return sdk.NewError(sdk.ErrWrongRequest, errw) } @@ -141,13 +126,13 @@ func (api *API) postWorkflowImportHandler() service.Handler { u := getAPIConsumer(ctx) // load the workflow from database if exists - workflowExists, err := workflow.Exists(tx, proj.Key, ew.Name) + workflowExists, err := workflow.Exists(tx, proj.Key, ew.GetName()) if err != nil { return sdk.WrapError(err, "Cannot check if workflow exists") } var wf *sdk.Workflow if workflowExists { - wf, err = workflow.Load(ctx, tx, api.Cache, *proj, ew.Name, workflow.LoadOptions{WithIcon: true}) + wf, err = workflow.Load(ctx, tx, api.Cache, *proj, ew.GetName(), workflow.LoadOptions{WithIcon: true}) if err != nil { return sdk.WrapError(err, "unable to load existing workflow") } @@ -164,7 +149,7 @@ func (api *API) postWorkflowImportHandler() service.Handler { sdkErr := sdk.ExtractHTTPError(globalError, r.Header.Get("Accept-Language")) return service.WriteJSON(w, append(msgListString, sdkErr.Message), sdkErr.Status) } - return sdk.WrapError(globalError, "Unable to import workflow %s", ew.Name) + return sdk.WrapError(globalError, "Unable to import workflow %s", ew.GetName()) } if err := tx.Commit(); err != nil { @@ -193,6 +178,21 @@ func (api *API) putWorkflowImportHandler() service.Handler { key := vars["key"] wfName := vars["permWorkflowName"] + body, errr := ioutil.ReadAll(r.Body) + if errr != nil { + return sdk.NewError(sdk.ErrWrongRequest, errr) + } + defer r.Body.Close() + + contentType := r.Header.Get("Content-Type") + if contentType == "" { + contentType = http.DetectContentType(body) + } + + if contentType != "application/x-yaml" && contentType != "text/x-yaml" { + return sdk.NewErrorFrom(sdk.ErrUnsupportedMediaType, fmt.Sprintf("unsupported content-type: %s", contentType)) + } + // Load project proj, err := project.Load(api.mustDB(), api.Cache, key, project.LoadOptions.WithGroups, @@ -218,28 +218,7 @@ func (api *API) putWorkflowImportHandler() service.Handler { return sdk.WithStack(sdk.ErrForbidden) } - body, errr := ioutil.ReadAll(r.Body) - if errr != nil { - return sdk.NewError(sdk.ErrWrongRequest, errr) - } - defer r.Body.Close() - - contentType := r.Header.Get("Content-Type") - if contentType == "" { - contentType = http.DetectContentType(body) - } - - var ew = new(exportentities.Workflow) - var errw error - switch contentType { - case "application/json": - errw = json.Unmarshal(body, ew) - case "application/x-yaml", "text/x-yaml": - errw = yaml.Unmarshal(body, ew) - default: - return sdk.WrapError(sdk.ErrWrongRequest, "Unsupported content-type: %s", contentType) - } - + ew, errw := exportentities.UnmarshalWorkflow(body) if errw != nil { return sdk.NewError(sdk.ErrWrongRequest, errw) } @@ -259,7 +238,7 @@ func (api *API) putWorkflowImportHandler() service.Handler { sdkErr := sdk.ExtractHTTPError(globalError, r.Header.Get("Accept-Language")) return service.WriteJSON(w, append(msgListString, sdkErr.Message), sdkErr.Status) } - return sdk.WrapError(globalError, "Unable to import workflow %s", ew.Name) + return sdk.WrapError(globalError, "Unable to import workflow %s", ew.GetName()) } if err := tx.Commit(); err != nil { diff --git a/engine/api/workflow_run.go b/engine/api/workflow_run.go index c0476222e4..57cfa7e1d6 100644 --- a/engine/api/workflow_run.go +++ b/engine/api/workflow_run.go @@ -406,7 +406,7 @@ func stopWorkflowRun(ctx context.Context, dbFunc func() *gorp.DbMap, store cache } defer tx.Rollback() //nolint - spwnMsg := sdk.SpawnMsg{ID: sdk.MsgWorkflowNodeStop.ID, Args: []interface{}{ident.GetUsername()}} + spwnMsg := sdk.SpawnMsg{ID: sdk.MsgWorkflowNodeStop.ID, Args: []interface{}{ident.GetUsername()}, Type: sdk.MsgWorkflowNodeStop.Type} stopInfos := sdk.SpawnInfo{ APITime: time.Now(), @@ -414,7 +414,7 @@ func stopWorkflowRun(ctx context.Context, dbFunc func() *gorp.DbMap, store cache Message: spwnMsg, } - workflow.AddWorkflowRunInfo(run, false, spwnMsg) + workflow.AddWorkflowRunInfo(run, spwnMsg) for _, wn := range run.WorkflowNodeRuns { for _, wnr := range wn { @@ -985,8 +985,9 @@ func (api *API) initWorkflowRun(ctx context.Context, db *gorp.DbMap, cache cache infos[i] = sdk.SpawnMsg{ ID: msg.ID, Args: msg.Args, + Type: msg.Type, } - workflow.AddWorkflowRunInfo(wfRun, false, infos...) + workflow.AddWorkflowRunInfo(wfRun, infos...) } r1 := failInitWorkflowRun(ctx, db, wfRun, sdk.WrapError(errCreate, "unable to get workflow from repository.")) report.Merge(ctx, r1, nil) // nolint @@ -1023,7 +1024,8 @@ func failInitWorkflowRun(ctx context.Context, db *gorp.DbMap, wfRun *sdk.Workflo var info sdk.SpawnMsg if sdk.ErrorIs(err, sdk.ErrConditionsNotOk) { info = sdk.SpawnMsg{ - ID: sdk.MsgWorkflowConditionError.ID, + ID: sdk.MsgWorkflowConditionError.ID, + Type: sdk.MsgWorkflowConditionError.Type, } if len(wfRun.WorkflowNodeRuns) == 0 { wfRun.Status = sdk.StatusNeverBuilt @@ -1034,10 +1036,11 @@ func failInitWorkflowRun(ctx context.Context, db *gorp.DbMap, wfRun *sdk.Workflo info = sdk.SpawnMsg{ ID: sdk.MsgWorkflowError.ID, Args: []interface{}{sdk.Cause(err).Error()}, + Type: sdk.MsgWorkflowError.Type, } } - workflow.AddWorkflowRunInfo(wfRun, !sdk.ErrorIs(err, sdk.ErrConditionsNotOk), info) + workflow.AddWorkflowRunInfo(wfRun, info) if errU := workflow.UpdateWorkflowRun(ctx, db, wfRun); errU != nil { log.Error(ctx, "unable to fail workflow run %v", errU) } diff --git a/engine/api/workflow_test.go b/engine/api/workflow_test.go index dee4fb1aa2..d50687096f 100644 --- a/engine/api/workflow_test.go +++ b/engine/api/workflow_test.go @@ -1171,6 +1171,7 @@ func Test_postWorkflowRollbackHandler(t *testing.T) { test.NoError(t, workflow.IsValid(context.Background(), api.Cache, db, wf, *proj, workflow.LoadOptions{})) eWf, err := exportentities.NewWorkflow(context.TODO(), *wf) + test.NoError(t, err) wfBts, err := yaml.Marshal(eWf) test.NoError(t, err) diff --git a/engine/api/workflowtemplate/execute.go b/engine/api/workflowtemplate/execute.go index 98ed6d8d53..0faf20c5da 100644 --- a/engine/api/workflowtemplate/execute.go +++ b/engine/api/workflowtemplate/execute.go @@ -204,7 +204,7 @@ func Execute(wt *sdk.WorkflowTemplate, instance *sdk.WorkflowTemplateInstance) ( } // Tar returns in buffer the a tar file that contains all generated stuff in template result. -func Tar(ctx context.Context, wt *sdk.WorkflowTemplate, res sdk.WorkflowTemplateResult, w io.Writer) error { +func Tar(ctx context.Context, res sdk.WorkflowTemplateResult, w io.Writer) error { tw := tar.NewWriter(w) defer func() { if err := tw.Close(); err != nil { @@ -213,20 +213,20 @@ func Tar(ctx context.Context, wt *sdk.WorkflowTemplate, res sdk.WorkflowTemplate }() // add generated workflow to writer - var wor exportentities.Workflow bs := []byte(res.Workflow) - if err := yaml.Unmarshal(bs, &wor); err != nil { + wor, err := exportentities.UnmarshalWorkflow(bs) + if err != nil { return sdk.NewError(sdk.Error{ ID: sdk.ErrWrongRequest.ID, Message: "Cannot parse generated workflow", }, err) } if err := tw.WriteHeader(&tar.Header{ - Name: fmt.Sprintf(exportentities.PullWorkflowName, wor.Name), + Name: fmt.Sprintf(exportentities.PullWorkflowName, wor.GetName()), Mode: 0644, Size: int64(len(bs)), }); err != nil { - return sdk.WrapError(err, "Unable to write header for workflow %s", wor.Name) + return sdk.WrapError(err, "Unable to write header for workflow %s", wor.GetName()) } if _, err := io.Copy(tw, bytes.NewBuffer(bs)); err != nil { return sdk.WrapError(err, "Unable to copy workflow buffer") diff --git a/sdk/build.go b/sdk/build.go index 3d5865ce8d..6613ee836c 100644 --- a/sdk/build.go +++ b/sdk/build.go @@ -17,6 +17,7 @@ type SpawnInfo struct { type SpawnMsg struct { ID string `json:"id" db:"-"` Args []interface{} `json:"args" db:"-"` + Type string `json:"type" db:"-"` } // ExecutedJob represents a running job diff --git a/sdk/error.go b/sdk/error.go index 39c4e15c26..e21ce53304 100644 --- a/sdk/error.go +++ b/sdk/error.go @@ -201,6 +201,7 @@ var ( ErrInvalidWorkerModelNamePattern = Error{ID: 185, Status: http.StatusBadRequest} ErrWorkflowAsCodeResync = Error{ID: 186, Status: http.StatusForbidden} ErrWorkflowNodeNameDuplicate = Error{ID: 187, Status: http.StatusBadRequest} + ErrUnsupportedMediaType = Error{ID: 188, Status: http.StatusUnsupportedMediaType} ) var errorsAmericanEnglish = map[int]string{ @@ -384,6 +385,7 @@ var errorsAmericanEnglish = map[int]string{ ErrInvalidJobRequirementNetworkAccess.ID: "Invalid job requirement: network requirement must contains ':'. Example: golang.org:http, golang.org:443", ErrWorkflowAsCodeResync.ID: "You cannot resynchronize an as-code workflow", ErrWorkflowNodeNameDuplicate.ID: "You cannot have same name for different pipelines in your workflow", + ErrUnsupportedMediaType.ID: "Request format invalid", } var errorsFrench = map[int]string{ @@ -566,6 +568,7 @@ var errorsFrench = map[int]string{ ErrInvalidJobRequirementNetworkAccess.ID: "Pré-requis de job invalide: Le pré-requis network doit contenir un ':'. Exemple: golang.org:http, golang.org:443", ErrWorkflowAsCodeResync.ID: "Impossible de resynchroniser un workflow en mode as-code", ErrWorkflowNodeNameDuplicate.ID: "Vous ne pouvez pas avoir plusieurs fois le même nom de pipeline dans votre workflow", + ErrUnsupportedMediaType.ID: "Le format de la requête est invalide", } var errorsLanguages = []map[int]string{ diff --git a/sdk/exportentities/v1/workflow.go b/sdk/exportentities/v1/workflow.go new file mode 100644 index 0000000000..43f8b3647c --- /dev/null +++ b/sdk/exportentities/v1/workflow.go @@ -0,0 +1,540 @@ +package v1 + +import ( + "fmt" + "math/rand" + "strings" + "time" + + "github.com/ovh/cds/sdk" +) + +// Workflow is the "as code" representation of a sdk.Workflow +type Workflow struct { + Name string `json:"name" yaml:"name" jsonschema_description:"The name of the workflow."` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Version string `json:"version,omitempty" yaml:"version,omitempty" jsonschema_description:"Version for the yaml syntax, latest is v1.0."` + Template string `json:"template,omitempty" yaml:"template,omitempty" jsonschema_description:"Optional path of the template used to generate the workflow."` + // this will be filled for complex workflows + Workflow map[string]NodeEntry `json:"workflow,omitempty" yaml:"workflow,omitempty" jsonschema_description:"Workflow nodes list."` + Hooks map[string][]HookEntry `json:"hooks,omitempty" yaml:"hooks,omitempty" jsonschema_description:"Workflow hooks list."` + // this will be filled for simple workflows + OneAtATime *bool `json:"one_at_a_time,omitempty" yaml:"one_at_a_time,omitempty" jsonschema_description:"Set to true if you want to limit the execution of this node to one at a time."` + Conditions *ConditionEntry `json:"conditions,omitempty" yaml:"conditions,omitempty" jsonschema_description:"Conditions to run this node.\nhttps://ovh.github.io/cds/docs/concepts/workflow/run-conditions."` + When []string `json:"when,omitempty" yaml:"when,omitempty" jsonschema_description:"Set manual and status condition (ex: 'success')."` //This is used only for manual and success condition + PipelineName string `json:"pipeline,omitempty" yaml:"pipeline,omitempty" jsonschema_description:"The name of a pipeline used for pipeline node."` + Payload map[string]interface{} `json:"payload,omitempty" yaml:"payload,omitempty"` + Parameters map[string]string `json:"parameters,omitempty" yaml:"parameters,omitempty" jsonschema_description:"List of parameters for the workflow."` + ApplicationName string `json:"application,omitempty" yaml:"application,omitempty" jsonschema_description:"The application to use in the context of the node.\nhttps://ovh.github.io/cds/docs/concepts/workflow/pipeline-context"` + EnvironmentName string `json:"environment,omitempty" yaml:"environment,omitempty" jsonschema_description:"The environment to use in the context of the node.\nhttps://ovh.github.io/cds/docs/concepts/workflow/pipeline-context"` + ProjectIntegrationName string `json:"integration,omitempty" yaml:"integration,omitempty" jsonschema_description:"The integration to use in the context of the node.\nhttps://ovh.github.io/cds/docs/concepts/workflow/pipeline-context"` + PipelineHooks []HookEntry `json:"pipeline_hooks,omitempty" yaml:"pipeline_hooks,omitempty"` + // extra workflow data + Permissions map[string]int `json:"permissions,omitempty" yaml:"permissions,omitempty" jsonschema_description:"The permissions for the workflow (ex: myGroup: 7).\nhttps://ovh.github.io/cds/docs/concepts/permissions"` + Metadata map[string]string `json:"metadata,omitempty" yaml:"metadata,omitempty"` + PurgeTags []string `json:"purge_tags,omitempty" yaml:"purge_tags,omitempty"` + Notifications []NotificationEntry `json:"notify,omitempty" yaml:"notify,omitempty"` // This is used when the workflow have only one pipeline + HistoryLength *int64 `json:"history_length,omitempty" yaml:"history_length,omitempty"` + MapNotifications map[string][]NotificationEntry `json:"notifications,omitempty" yaml:"notifications,omitempty"` // This is used when the workflow have more than one pipeline +} + +// NodeEntry represents a node as code +type NodeEntry struct { + ID int64 `json:"-" yaml:"-"` + DependsOn []string `json:"depends_on,omitempty" yaml:"depends_on,omitempty" jsonschema_description:"Names of the parent nodes, can be pipelines, forks or joins."` + Conditions *ConditionEntry `json:"conditions,omitempty" yaml:"conditions,omitempty" jsonschema_description:"Conditions to run this node.\nhttps://ovh.github.io/cds/docs/concepts/workflow/run-conditions."` + When []string `json:"when,omitempty" yaml:"when,omitempty" jsonschema_description:"Set manual and status condition (ex: 'success')."` //This is used only for manual and success condition + PipelineName string `json:"pipeline,omitempty" yaml:"pipeline,omitempty" jsonschema_description:"The name of a pipeline used for pipeline node."` + ApplicationName string `json:"application,omitempty" yaml:"application,omitempty" jsonschema_description:"The application to use in the context of the node.\nhttps://ovh.github.io/cds/docs/concepts/workflow/pipeline-context"` + EnvironmentName string `json:"environment,omitempty" yaml:"environment,omitempty" jsonschema_description:"The environment to use in the context of the node.\nhttps://ovh.github.io/cds/docs/concepts/workflow/pipeline-context"` + ProjectIntegrationName string `json:"integration,omitempty" yaml:"integration,omitempty" jsonschema_description:"The integration to use in the context of the node.\nhttps://ovh.github.io/cds/docs/concepts/workflow/pipeline-context"` + OneAtATime *bool `json:"one_at_a_time,omitempty" yaml:"one_at_a_time,omitempty" jsonschema_description:"Set to true if you want to limit the execution of this node to one at a time."` + Payload map[string]interface{} `json:"payload,omitempty" yaml:"payload,omitempty"` + Parameters map[string]string `json:"parameters,omitempty" yaml:"parameters,omitempty" jsonschema_description:"List of parameters for the workflow."` + OutgoingHookModelName string `json:"trigger,omitempty" yaml:"trigger,omitempty"` + OutgoingHookConfig map[string]string `json:"config,omitempty" yaml:"config,omitempty"` + Permissions map[string]int `json:"permissions,omitempty" yaml:"permissions,omitempty" jsonschema_description:"The permissions for the node (ex: myGroup: 7).\nhttps://ovh.github.io/cds/docs/concepts/permissions"` +} + +type ConditionEntry struct { + PlainConditions []PlainConditionEntry `json:"plain,omitempty" yaml:"check,omitempty"` + LuaScript string `json:"script,omitempty" yaml:"script,omitempty"` +} + +//WorkflowNodeCondition represents a condition to trigger ot not a pipeline in a workflow. Operator can be =, !=, regex +type PlainConditionEntry struct { + Variable string `json:"variable" yaml:"variable"` + Operator string `json:"operator" yaml:"operator"` + Value string `json:"value" yaml:"value"` +} + +// HookEntry represents a hook as code +type HookEntry struct { + Model string `json:"type,omitempty" yaml:"type,omitempty" jsonschema_description:"Model of the hook.\nhttps://ovh.github.io/cds/docs/concepts/workflow/hooks"` + Config map[string]string `json:"config,omitempty" yaml:"config,omitempty"` + Conditions *sdk.WorkflowNodeConditions `json:"conditions,omitempty" yaml:"conditions,omitempty" jsonschema_description:"Conditions to run this hook.\nhttps://ovh.github.io/cds/docs/concepts/workflow/run-conditions."` +} + +// Entries returns the map of all workflow entries +func (w Workflow) Entries() map[string]NodeEntry { + if len(w.Workflow) != 0 { + return w.Workflow + } + + singleEntry := NodeEntry{ + ApplicationName: w.ApplicationName, + EnvironmentName: w.EnvironmentName, + ProjectIntegrationName: w.ProjectIntegrationName, + PipelineName: w.PipelineName, + Conditions: w.Conditions, + When: w.When, + Payload: w.Payload, + Parameters: w.Parameters, + OneAtATime: w.OneAtATime, + } + return map[string]NodeEntry{ + w.PipelineName: singleEntry, + } +} + +func (w Workflow) CheckValidity() error { + mError := new(sdk.MultiError) + + //Check valid application name + rx := sdk.NamePatternRegex + if !rx.MatchString(w.Name) { + mError.Append(fmt.Errorf("workflow name %s do not respect pattern %s", w.Name, sdk.NamePattern)) + } + + if len(w.Workflow) != 0 { + if w.ApplicationName != "" { + mError.Append(fmt.Errorf("error: wrong usage: application %s not allowed here", w.ApplicationName)) + } + if w.EnvironmentName != "" { + mError.Append(fmt.Errorf("error: wrong usage: environment %s not allowed here", w.EnvironmentName)) + } + if w.ProjectIntegrationName != "" { + mError.Append(fmt.Errorf("error: wrong usage: integration %s not allowed here", w.ProjectIntegrationName)) + } + if w.PipelineName != "" { + mError.Append(fmt.Errorf("error: wrong usage: pipeline %s not allowed here", w.PipelineName)) + } + if w.Conditions != nil { + mError.Append(fmt.Errorf("error: wrong usage: conditions not allowed here")) + } + if len(w.When) != 0 { + mError.Append(fmt.Errorf("error: wrong usage: when not allowed here")) + } + if len(w.PipelineHooks) != 0 { + mError.Append(fmt.Errorf("error: wrong usage: pipeline_hooks not allowed here")) + } + } else { + if len(w.Hooks) > 0 { + mError.Append(fmt.Errorf("error: wrong usage: hooks not allowed here")) + } + } + + for name := range w.Hooks { + if _, ok := w.Workflow[name]; !ok { + mError.Append(fmt.Errorf("Error: wrong usage: invalid hook on %s", name)) + } + } + + //Checks map notifications validity + mError.Append(CheckWorkflowNotificationsValidity(w)) + + if mError.IsEmpty() { + return nil + } + return mError +} + +func (w Workflow) CheckDependencies() error { + mError := new(sdk.MultiError) + for s, e := range w.Entries() { + if err := e.checkDependencies(s, w); err != nil { + mError.Append(fmt.Errorf("Error: %s invalid: %v", s, err)) + } + } + + if mError.IsEmpty() { + return nil + } + return mError +} + +func (e NodeEntry) checkDependencies(nodeName string, w Workflow) error { + mError := new(sdk.MultiError) +nextDep: + for _, d := range e.DependsOn { + for s := range w.Workflow { + if s == d { + continue nextDep + } + } + mError.Append(fmt.Errorf("the pipeline %s depends on an unknown pipeline: %s", nodeName, d)) + } + if mError.IsEmpty() { + return nil + } + return mError +} + +// GetWorkflow returns a fresh sdk.Workflow +func (w Workflow) GetWorkflow() (*sdk.Workflow, error) { + var wf = new(sdk.Workflow) + wf.Name = w.Name + wf.Description = w.Description + wf.WorkflowData = &sdk.WorkflowData{} + // Init map + wf.Applications = make(map[int64]sdk.Application) + wf.Pipelines = make(map[int64]sdk.Pipeline) + wf.Environments = make(map[int64]sdk.Environment) + wf.ProjectIntegrations = make(map[int64]sdk.ProjectIntegration) + + if err := w.CheckValidity(); err != nil { + return nil, sdk.WrapError(err, "Unable to check validity") + } + if err := w.CheckDependencies(); err != nil { + return nil, sdk.WrapError(err, "Unable to check dependencies") + } + wf.PurgeTags = w.PurgeTags + if len(w.Metadata) > 0 { + wf.Metadata = make(map[string]string, len(w.Metadata)) + for k, v := range w.Metadata { + wf.Metadata[k] = v + } + } + if w.HistoryLength != nil && *w.HistoryLength > 0 { + wf.HistoryLength = *w.HistoryLength + } else { + wf.HistoryLength = sdk.DefaultHistoryLength + } + + rand.Seed(time.Now().Unix()) + entries := w.Entries() + var attempt int + fakeID := rand.Int63n(5000) + // attempt is there to avoid infinite loop, but it should not happened becase we check validty and dependencies earlier + for len(entries) != 0 && attempt < 10000 { + for name, entry := range entries { + entry.ID = fakeID + ok, err := entry.processNode(name, wf) + if err != nil { + return nil, sdk.WrapError(err, "Unable to process node") + } + if ok { + delete(entries, name) + fakeID++ + } + } + attempt++ + } + if len(entries) > 0 { + return nil, sdk.WithStack(fmt.Errorf("Unable to process %+v", entries)) + } + + //Process hooks + wf.VisitNode(w.processHooks) + + //Compute permissions + wf.Groups = make([]sdk.GroupPermission, 0, len(w.Permissions)) + for g, p := range w.Permissions { + perm := sdk.GroupPermission{Group: sdk.Group{Name: g}, Permission: p} + wf.Groups = append(wf.Groups, perm) + } + + //Compute notifications + if err := w.processNotifications(wf); err != nil { + return nil, err + } + + // if there is a template instance id on the workflow export, add it + if w.Template != "" { + templatePath := strings.Split(w.Template, "/") + if len(templatePath) != 2 { + return nil, sdk.WithStack(fmt.Errorf("Invalid template path")) + } + wf.Template = &sdk.WorkflowTemplate{ + Group: &sdk.Group{Name: templatePath[0]}, + Slug: templatePath[1], + } + } + + wf.SortNode() + + return wf, nil +} + +func (e *NodeEntry) getNode(name string) (*sdk.Node, error) { + var mutex bool + if e.OneAtATime != nil && *e.OneAtATime { + mutex = true + } + node := &sdk.Node{ + Name: name, + Ref: name, + Type: sdk.NodeTypeFork, + Context: &sdk.NodeContext{ + PipelineName: e.PipelineName, + ApplicationName: e.ApplicationName, + EnvironmentName: e.EnvironmentName, + ProjectIntegrationName: e.ProjectIntegrationName, + Mutex: mutex, + }, + } + + if e.PipelineName != "" { + node.Type = sdk.NodeTypePipeline + } else if e.OutgoingHookModelName != "" { + node.Type = sdk.NodeTypeOutGoingHook + } else if len(e.DependsOn) > 1 { + node.Type = sdk.NodeTypeJoin + node.JoinContext = make([]sdk.NodeJoin, 0, len(e.DependsOn)) + for _, parent := range e.DependsOn { + node.JoinContext = append(node.JoinContext, sdk.NodeJoin{ParentName: parent}) + } + } + + if len(e.Permissions) > 0 { + //Compute permissions + node.Groups = make([]sdk.GroupPermission, 0, len(e.Permissions)) + for g, p := range e.Permissions { + perm := sdk.GroupPermission{Group: sdk.Group{Name: g}, Permission: p} + node.Groups = append(node.Groups, perm) + } + } + + if e.Conditions != nil { + node.Context.Conditions = sdk.WorkflowNodeConditions{ + PlainConditions: make([]sdk.WorkflowNodeCondition, 0, len(e.Conditions.PlainConditions)), + LuaScript: e.Conditions.LuaScript, + } + for _, c := range e.Conditions.PlainConditions { + node.Context.Conditions.PlainConditions = append(node.Context.Conditions.PlainConditions, sdk.WorkflowNodeCondition{ + Variable: c.Variable, + Operator: c.Operator, + Value: c.Value, + }) + } + } + + if len(e.Payload) > 0 { + if len(e.DependsOn) > 0 { + return nil, sdk.WrapError(sdk.ErrInvalidNodeDefaultPayload, "Default payload cannot be set on another node than the first one (node : %s)", name) + } + node.Context.DefaultPayload = e.Payload + } + + mapPipelineParameters := sdk.ParametersFromMap(e.Parameters) + node.Context.DefaultPipelineParameters = mapPipelineParameters + + for _, w := range e.When { + switch w { + case "success": + node.Context.Conditions.PlainConditions = append(node.Context.Conditions.PlainConditions, sdk.WorkflowNodeCondition{ + Operator: sdk.WorkflowConditionsOperatorEquals, + Value: sdk.StatusSuccess, + Variable: "cds.status", + }) + case "manual": + node.Context.Conditions.PlainConditions = append(node.Context.Conditions.PlainConditions, sdk.WorkflowNodeCondition{ + Operator: sdk.WorkflowConditionsOperatorEquals, + Value: "true", + Variable: "cds.manual", + }) + default: + return nil, fmt.Errorf("Unsupported when condition %s", w) + } + } + + if e.OneAtATime != nil { + node.Context.Mutex = *e.OneAtATime + } + + if e.OutgoingHookModelName != "" { + node.Type = sdk.NodeTypeOutGoingHook + config := sdk.WorkflowNodeHookConfig{} + for k, v := range e.OutgoingHookConfig { + config[k] = sdk.WorkflowNodeHookConfigValue{ + Value: v, + } + } + node.OutGoingHookContext = &sdk.NodeOutGoingHook{ + Config: config, + HookModelName: e.OutgoingHookModelName, + } + } + return node, nil +} + +func (w *Workflow) processHooks(n *sdk.Node, wf *sdk.Workflow) { + var addHooks = func(hooks []HookEntry) { + for _, h := range hooks { + cfg := make(sdk.WorkflowNodeHookConfig, len(h.Config)) + for k, v := range h.Config { + var hType string + switch h.Model { + case sdk.KafkaHookModelName, sdk.RabbitMQHookModelName: + if k == sdk.HookModelIntegration { + hType = sdk.HookConfigTypeIntegration + } else { + hType = sdk.HookConfigTypeString + } + default: + hType = sdk.HookConfigTypeString + } + cfg[k] = sdk.WorkflowNodeHookConfigValue{ + Value: v, + Configurable: true, + Type: hType, + } + } + + hook := sdk.NodeHook{ + Config: cfg, + HookModelName: h.Model, + } + + if h.Conditions != nil { + hook.Conditions = *h.Conditions + } + n.Hooks = append(n.Hooks, hook) + } + } + + if len(w.PipelineHooks) > 0 { + //Only one node workflow + addHooks(w.PipelineHooks) + return + } + + addHooks(w.Hooks[n.Name]) +} + +func (e *NodeEntry) processNode(name string, w *sdk.Workflow) (bool, error) { + // Find WorkflowNodeAncestors + exist, err := e.processNodeAncestors(name, w) + if err != nil { + return false, err + } + + if exist { + return true, nil + } + + return false, nil +} + +func (e *NodeEntry) processNodeAncestors(name string, w *sdk.Workflow) (bool, error) { + var ancestorsExist = true + var ancestors []*sdk.Node + + if len(e.DependsOn) == 1 { + a := e.DependsOn[0] + //Looking for the ancestor + ancestor := w.WorkflowData.NodeByName(a) + if ancestor == nil { + ancestorsExist = false + } + ancestors = append(ancestors, ancestor) + } else { + for _, a := range e.DependsOn { + //Looking for the ancestor + ancestor := w.WorkflowData.NodeByName(a) + if ancestor == nil { + ancestorsExist = false + break + } + ancestors = append(ancestors, ancestor) + } + } + + if !ancestorsExist { + return false, nil + } + + n, err := e.getNode(name) + if err != nil { + return false, err + } + + switch len(ancestors) { + case 0: + w.WorkflowData.Node = *n + return true, nil + case 1: + w.AddTrigger(ancestors[0].Name, *n) + return true, nil + default: + if n != nil && n.Type == sdk.NodeTypeJoin && joinAsNode(n) { + w.WorkflowData.Joins = append(w.WorkflowData.Joins, *n) + return true, nil + } + } + + // Compute join + + // Try to find an existing join with the same references + var join *sdk.Node + for i := range w.WorkflowData.Joins { + j := &w.WorkflowData.Joins[i] + var joinFound = true + + for _, ref := range j.JoinContext { + var refFound bool + for _, a := range e.DependsOn { + if ref.ParentName == a { + refFound = true + break + } + } + if !refFound { + joinFound = false + break + } + } + + if joinFound { + j.Ref = fmt.Sprintf("fakeRef%d", e.ID) + join = j + } + } + + var appendJoin bool + if join == nil { + joinContext := make([]sdk.NodeJoin, 0, len(e.DependsOn)) + for _, d := range e.DependsOn { + joinContext = append(joinContext, sdk.NodeJoin{ + ParentName: d, + }) + } + join = &sdk.Node{ + JoinContext: joinContext, + Type: sdk.NodeTypeJoin, + Ref: fmt.Sprintf("fakeRef%d", e.ID), + } + appendJoin = true + } + + join.Triggers = append(join.Triggers, sdk.NodeTrigger{ + ChildNode: *n, + }) + + if appendJoin { + w.WorkflowData.Joins = append(w.WorkflowData.Joins, *join) + } + return true, nil +} + +func joinAsNode(n *sdk.Node) bool { + return n.Context != nil && (n.Context.Conditions.LuaScript != "" || len(n.Context.Conditions.PlainConditions) > 0) +} + +func (w Workflow) GetName() string { + return w.Name +} + +func (w Workflow) GetVersion() string { + return w.Version +} diff --git a/sdk/exportentities/v1/workflow_notif_test.go b/sdk/exportentities/v1/workflow_notif_test.go new file mode 100644 index 0000000000..8dabb813c5 --- /dev/null +++ b/sdk/exportentities/v1/workflow_notif_test.go @@ -0,0 +1,129 @@ +package v1_test + +import ( + v1 "github.com/ovh/cds/sdk/exportentities/v1" + "reflect" + "testing" + + "github.com/ovh/cds/engine/api/test" + "github.com/ovh/cds/sdk" + yaml "gopkg.in/yaml.v2" +) + +func Test_checkWorkflowNotificationsValidity(t *testing.T) { + + type args struct { + yaml string + } + tests := []struct { + name string + args args + want error + }{ + { + name: "test multiple notifications", + want: nil, + args: args{ + yaml: `name: test1 +version: v1.0 +workflow: + DDOS-me: + pipeline: DDOS-me + application: test1 + payload: + git.author: "" + git.branch: master + git.hash: "" + git.hash.before: "" + git.message: "" + git.repository: bnjjj/godevoxx + git.tag: "" + DDOS-me_2: + depends_on: + - DDOS-me + when: + - success + pipeline: DDOS-me +metadata: + default_tags: git.branch,git.author +notifications: + DDOS-me,DDOS-me_2: + - type: jabber + settings: + on_success: always + on_failure: change + on_start: true + send_to_groups: true + send_to_author: false + recipients: + - q + template: + subject: '{{.cds.project}}/{{.cds.application}} {{.cds.pipeline}} {{.cds.environment}}#{{.cds.version}} + {{.cds.status}}' + body: |- + Project : {{.cds.project}} + Application : {{.cds.application}} + Pipeline : {{.cds.pipeline}}/{{.cds.environment}}#{{.cds.version}} + Status : {{.cds.status}} + Details : {{.cds.buildURL}} + Triggered by : {{.cds.triggered_by.username}} + Branch : {{.git.branch}} + DDOS-me_2: + - type: email + settings: + template: + subject: '{{.cds.project}}/{{.cds.application}} {{.cds.pipeline}} {{.cds.environment}}#{{.cds.version}} + {{.cds.status}}' + body: |- + Project : {{.cds.project}} + Application : {{.cds.application}} + Pipeline : {{.cds.pipeline}}/{{.cds.environment}}#{{.cds.version}} + Status : {{.cds.status}} + Details : {{.cds.buildURL}} + Triggered by : {{.cds.triggered_by.username}} + Branch : {{.git.branch}} + - type: vcs + settings: + template: + disable_comment: true +`, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var w v1.Workflow + test.NoError(t, yaml.Unmarshal([]byte(tt.args.yaml), &w)) + if got := v1.CheckWorkflowNotificationsValidity(w); got != tt.want { + t.Errorf("checkWorkflowNotificationsValidity() = %#v, want %v", got, tt.want) + } + }) + } +} + +func Test_processNotificationValues(t *testing.T) { + type args struct { + notif v1.NotificationEntry + } + tests := []struct { + name string + args args + want sdk.WorkflowNotification + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := v1.ProcessNotificationValues(tt.args.notif) + if (err != nil) != tt.wantErr { + t.Errorf("processNotificationValues() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("processNotificationValues() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/sdk/exportentities/workflow_notif.go b/sdk/exportentities/v1/workflow_notification.go similarity index 99% rename from sdk/exportentities/workflow_notif.go rename to sdk/exportentities/v1/workflow_notification.go index 9d65a1470f..7c8f39fecb 100644 --- a/sdk/exportentities/workflow_notif.go +++ b/sdk/exportentities/v1/workflow_notification.go @@ -1,4 +1,4 @@ -package exportentities +package v1 import ( "context" diff --git a/sdk/exportentities/v1/workflow_test.go b/sdk/exportentities/v1/workflow_test.go new file mode 100644 index 0000000000..f6cb4756d5 --- /dev/null +++ b/sdk/exportentities/v1/workflow_test.go @@ -0,0 +1,866 @@ +package v1_test + +import ( + "github.com/ovh/cds/sdk/exportentities" + v1 "github.com/ovh/cds/sdk/exportentities/v1" + "sort" + "strings" + "testing" + + "github.com/fsamin/go-dump" + "github.com/ovh/cds/sdk" + "github.com/stretchr/testify/assert" +) + +func TestWorkflow_checkDependencies(t *testing.T) { + type fields struct { + Name string + Description string + Version string + Workflow map[string]v1.NodeEntry + Hooks map[string][]v1.HookEntry + Conditions *v1.ConditionEntry + When []string + PipelineName string + ApplicationName string + EnvironmentName string + ProjectIntegrationName string + PipelineHooks []v1.HookEntry + Permissions map[string]int + HistoryLength int64 + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "Simple Workflow without dependencies should not raise an error", + fields: fields{ + PipelineName: "pipeline", + }, + wantErr: false, + }, + { + name: "Complex Workflow with a dependency should not raise an error", + fields: fields{ + Workflow: map[string]v1.NodeEntry{ + "root": { + PipelineName: "pipeline", + }, + "child": { + PipelineName: "pipeline", + DependsOn: []string{"root"}, + }, + }, + }, + wantErr: false, + }, + { + name: "Complex Workflow with a dependencies and a join should not raise an error", + fields: fields{ + Workflow: map[string]v1.NodeEntry{ + "root": { + PipelineName: "pipeline", + }, + "first-child": { + PipelineName: "pipeline", + DependsOn: []string{"root"}, + }, + "second-child": { + PipelineName: "pipeline", + DependsOn: []string{"root"}, + }, + "third-child": { + PipelineName: "pipeline", + DependsOn: []string{"first-child", "second-child"}, + }, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := v1.Workflow{ + Name: tt.fields.Name, + Description: tt.fields.Description, + Version: tt.fields.Version, + Workflow: tt.fields.Workflow, + Hooks: tt.fields.Hooks, + Conditions: tt.fields.Conditions, + When: tt.fields.When, + PipelineName: tt.fields.PipelineName, + ApplicationName: tt.fields.ApplicationName, + EnvironmentName: tt.fields.EnvironmentName, + ProjectIntegrationName: tt.fields.ProjectIntegrationName, + PipelineHooks: tt.fields.PipelineHooks, + Permissions: tt.fields.Permissions, + } + if err := w.CheckDependencies(); (err != nil) != tt.wantErr { + t.Errorf("Workflow.checkDependencies() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestWorkflow_checkValidity(t *testing.T) { + type fields struct { + Name string + Version string + Workflow map[string]v1.NodeEntry + Hooks map[string][]v1.HookEntry + DependsOn []string + Conditions *v1.ConditionEntry + When []string + PipelineName string + ApplicationName string + EnvironmentName string + ProjectIntegrationName string + PipelineHooks []v1.HookEntry + Permissions map[string]int + OneAtATime *bool + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "Should raise an error", + fields: fields{ + PipelineName: "pipeline", + Workflow: map[string]v1.NodeEntry{ + "root": { + PipelineName: "pipeline", + }, + "child": { + PipelineName: "pipeline", + DependsOn: []string{"root"}, + }, + }, + }, + wantErr: true, + }, + { + name: "Should not raise an error", + fields: fields{ + Name: "myworkflow", + Workflow: map[string]v1.NodeEntry{ + "root": { + PipelineName: "pipeline", + }, + "child": { + PipelineName: "pipeline", + DependsOn: []string{"root"}, + }, + }, + }, + wantErr: false, + }, + { + name: "Too simple to raise an error", + fields: fields{ + Name: "myworkflow", + PipelineName: "pipeline", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := v1.Workflow{ + Name: tt.fields.Name, + Version: tt.fields.Version, + Workflow: tt.fields.Workflow, + Hooks: tt.fields.Hooks, + Conditions: tt.fields.Conditions, + When: tt.fields.When, + PipelineName: tt.fields.PipelineName, + ApplicationName: tt.fields.ApplicationName, + EnvironmentName: tt.fields.EnvironmentName, + ProjectIntegrationName: tt.fields.ProjectIntegrationName, + PipelineHooks: tt.fields.PipelineHooks, + Permissions: tt.fields.Permissions, + OneAtATime: tt.fields.OneAtATime, + } + if err := w.CheckValidity(); (err != nil) != tt.wantErr { + t.Errorf("Workflow.checkValidity() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestWorkflow_GetWorkflow(t *testing.T) { + strue := true + type fields struct { + Name string + Description string + Version string + Workflow map[string]v1.NodeEntry + Hooks map[string][]v1.HookEntry + DependsOn []string + Conditions *v1.ConditionEntry + When []string + PipelineName string + ApplicationName string + EnvironmentName string + ProjectIntegrationName string + PipelineHooks []v1.HookEntry + Permissions map[string]int + HistoryLength int64 + OneAtATime *bool + } + tsts := []struct { + name string + fields fields + want sdk.Workflow + wantErr bool + }{ + // pipeline + { + name: "Simple workflow with mutex should not raise an error", + fields: fields{ + Version: exportentities.ActionVersion1, + Name: "myworkflow", + PipelineName: "pipeline", + OneAtATime: &strue, + }, + wantErr: false, + want: sdk.Workflow{ + Name: "myworkflow", + HistoryLength: sdk.DefaultHistoryLength, + WorkflowData: &sdk.WorkflowData{ + Node: sdk.Node{ + Name: "pipeline", + Type: "pipeline", + Context: &sdk.NodeContext{ + PipelineName: "pipeline", + Mutex: true, + }, + }, + }, + }, + }, + // pipeline + { + name: "Simple workflow should not raise an error", + fields: fields{ + Version: exportentities.ActionVersion1, + Name: "myworkflow", + PipelineName: "pipeline", + Description: "this is my description", + PipelineHooks: []v1.HookEntry{ + { + Model: "Scheduler", + Config: map[string]string{ + "crontab": "* * * * *", + "payload": "{}", + }, + Conditions: &sdk.WorkflowNodeConditions{ + LuaScript: "return true", + }, + }, + }, + }, + wantErr: false, + want: sdk.Workflow{ + Name: "myworkflow", + HistoryLength: sdk.DefaultHistoryLength, + Description: "this is my description", + WorkflowData: &sdk.WorkflowData{ + Node: sdk.Node{ + Name: "pipeline", + Type: "pipeline", + Context: &sdk.NodeContext{ + PipelineName: "pipeline", + }, + Hooks: []sdk.NodeHook{ + { + HookModelName: "Scheduler", + Conditions: sdk.WorkflowNodeConditions{ + LuaScript: "return true", + }, + Config: sdk.WorkflowNodeHookConfig{ + "crontab": sdk.WorkflowNodeHookConfigValue{ + Value: "* * * * *", + Configurable: true, + Type: sdk.HookConfigTypeString, + }, + "payload": sdk.WorkflowNodeHookConfigValue{ + Value: "{}", + Configurable: true, + Type: sdk.HookConfigTypeString, + }, + }, + }, + }, + }, + }, + }, + }, + // hook conditions + { + name: "Workflow with multiple nodes should display hook conditions", + fields: fields{ + Name: "myworkflow", + Version: exportentities.ActionVersion1, + Workflow: map[string]v1.NodeEntry{ + "root": { + PipelineName: "pipeline-root", + }, + "child": { + PipelineName: "pipeline-child", + DependsOn: []string{"root"}, + OneAtATime: &v1.True, + }, + }, + Hooks: map[string][]v1.HookEntry{ + "root": []v1.HookEntry{{ + Model: "Scheduler", + Config: map[string]string{ + "crontab": "* * * * *", + "payload": "{}", + }, + Conditions: &sdk.WorkflowNodeConditions{ + LuaScript: "return true", + }, + }}, + }, + }, + wantErr: false, + want: sdk.Workflow{ + Name: "myworkflow", + HistoryLength: sdk.DefaultHistoryLength, + WorkflowData: &sdk.WorkflowData{ + Node: sdk.Node{ + Name: "root", + Type: "pipeline", + Hooks: []sdk.NodeHook{ + { + HookModelName: "Scheduler", + Conditions: sdk.WorkflowNodeConditions{ + LuaScript: "return true", + }, + Config: sdk.WorkflowNodeHookConfig{ + "crontab": sdk.WorkflowNodeHookConfigValue{ + Value: "* * * * *", + Configurable: true, + Type: sdk.HookConfigTypeString, + }, + "payload": sdk.WorkflowNodeHookConfigValue{ + Value: "{}", + Configurable: true, + Type: sdk.HookConfigTypeString, + }, + }, + }, + }, + Context: &sdk.NodeContext{ + PipelineName: "pipeline-root", + }, + Triggers: []sdk.NodeTrigger{ + { + ChildNode: sdk.Node{ + Name: "child", + Ref: "child", + Type: "pipeline", + Context: &sdk.NodeContext{ + PipelineName: "pipeline-child", + Mutex: true, + }, + }, + }, + }, + }, + }, + }, + }, + // root(pipeline-root) -> child(pipeline-child) + { + name: "Complexe workflow without joins and mutex should not raise an error", + fields: fields{ + Name: "myworkflow", + Version: exportentities.ActionVersion1, + Workflow: map[string]v1.NodeEntry{ + "root": { + PipelineName: "pipeline-root", + }, + "child": { + PipelineName: "pipeline-child", + DependsOn: []string{"root"}, + OneAtATime: &v1.True, + }, + }, + }, + wantErr: false, + want: sdk.Workflow{ + Name: "myworkflow", + HistoryLength: sdk.DefaultHistoryLength, + WorkflowData: &sdk.WorkflowData{ + Node: sdk.Node{ + Name: "root", + Type: "pipeline", + Context: &sdk.NodeContext{ + PipelineName: "pipeline-root", + }, + Triggers: []sdk.NodeTrigger{ + { + ChildNode: sdk.Node{ + Name: "child", + Ref: "child", + Type: "pipeline", + Context: &sdk.NodeContext{ + PipelineName: "pipeline-child", + Mutex: true, + }, + }, + }, + }, + }, + }, + }, + }, + // root(pipeline-root) -> child(pipeline-child) + { + name: "Complexe workflow without joins with a default payload on a non root node should raise an error", + fields: fields{ + Name: "myworkflow", + Version: exportentities.ActionVersion1, + Workflow: map[string]v1.NodeEntry{ + "root": { + PipelineName: "pipeline-root", + }, + "child": { + PipelineName: "pipeline-child", + DependsOn: []string{"root"}, + Payload: map[string]interface{}{ + "test": "content", + }, + OneAtATime: &v1.True, + }, + }, + }, + wantErr: true, + }, + // root(pipeline-root) -> child(pipeline-child) + { + name: "Complexe workflow unordered without joins should not raise an error", + fields: fields{ + Name: "myworkflow", + Version: exportentities.ActionVersion1, + Workflow: map[string]v1.NodeEntry{ + "child": { + PipelineName: "pipeline-child", + DependsOn: []string{"root"}, + }, + "root": { + PipelineName: "pipeline-root", + }, + }, + HistoryLength: 25, + }, + wantErr: false, + want: sdk.Workflow{ + Name: "myworkflow", + HistoryLength: 25, + WorkflowData: &sdk.WorkflowData{ + Node: sdk.Node{ + Name: "root", + Ref: "root", + Type: "pipeline", + Context: &sdk.NodeContext{ + PipelineName: "pipeline-root", + }, + Triggers: []sdk.NodeTrigger{ + { + ChildNode: sdk.Node{ + Name: "child", + Ref: "child", + Type: "pipeline", + Context: &sdk.NodeContext{ + PipelineName: "pipeline-child", + }, + }, + }, + }, + }, + }, + }, + }, + // root(pipeline-root) -> first(pipeline-child) -> second(pipeline-child) + { + name: "Complexe workflow without joins should not raise an error", + fields: fields{ + Name: "myworkflow", + Version: exportentities.ActionVersion1, + Workflow: map[string]v1.NodeEntry{ + "root": { + PipelineName: "pipeline-root", + }, + "first": { + PipelineName: "pipeline-child", + DependsOn: []string{"root"}, + }, + "second": { + PipelineName: "pipeline-child", + DependsOn: []string{"first"}, + }, + }, + }, + wantErr: false, + want: sdk.Workflow{ + Name: "myworkflow", + HistoryLength: sdk.DefaultHistoryLength, + WorkflowData: &sdk.WorkflowData{ + Node: sdk.Node{ + Name: "root", + Ref: "root", + Type: "pipeline", + Context: &sdk.NodeContext{ + PipelineName: "pipeline-root", + }, + Triggers: []sdk.NodeTrigger{ + { + ChildNode: sdk.Node{ + Name: "first", + Ref: "first", + Type: "pipeline", + Context: &sdk.NodeContext{ + PipelineName: "pipeline-child", + }, + + Triggers: []sdk.NodeTrigger{ + { + ChildNode: sdk.Node{ + Name: "second", + Ref: "second", + Type: "pipeline", + Context: &sdk.NodeContext{ + PipelineName: "pipeline-child", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + // A(pipeline)(*) -> B(pipeline) -> join -> D(pipeline) -> join -> G(pipeline) + // -> C(pipeline) / -> E(pipeline) / + // -> F(pipeline) + { + name: "Complexe workflow with joins should not raise an error", + fields: fields{ + Name: "myworkflow", + Version: exportentities.ActionVersion1, + Workflow: map[string]v1.NodeEntry{ + "A": { + PipelineName: "pipeline", + }, + "B": { + PipelineName: "pipeline", + DependsOn: []string{"A"}, + }, + "C": { + PipelineName: "pipeline", + DependsOn: []string{"A"}, + }, + "D": { + PipelineName: "pipeline", + DependsOn: []string{"B", "C"}, + }, + "E": { + PipelineName: "pipeline", + DependsOn: []string{"B", "C"}, + }, + "F": { + PipelineName: "pipeline", + DependsOn: []string{"B", "C"}, + }, + "G": { + PipelineName: "pipeline", + DependsOn: []string{"D", "E"}, + }, + }, + Hooks: map[string][]v1.HookEntry{ + "A": { + { + Model: "Scheduler", + Config: map[string]string{ + "crontab": "* * * * *", + "payload": "{}", + }, + }, + }, + }, + }, + wantErr: false, + want: sdk.Workflow{ + Name: "myworkflow", + HistoryLength: sdk.DefaultHistoryLength, + WorkflowData: &sdk.WorkflowData{ + Node: sdk.Node{ + Name: "A", + Ref: "A", + Type: "pipeline", + Context: &sdk.NodeContext{ + PipelineName: "pipeline", + }, + Triggers: []sdk.NodeTrigger{ + { + ChildNode: sdk.Node{ + Name: "B", + Ref: "B", + Type: "pipeline", + Context: &sdk.NodeContext{ + PipelineName: "pipeline", + }, + }, + }, + { + ChildNode: sdk.Node{ + Name: "C", + Ref: "C", + Type: "pipeline", + Context: &sdk.NodeContext{ + PipelineName: "pipeline", + }, + }, + }, + }, + Hooks: []sdk.NodeHook{ + { + HookModelName: "Scheduler", + Config: sdk.WorkflowNodeHookConfig{ + "crontab": sdk.WorkflowNodeHookConfigValue{ + Value: "* * * * *", + Configurable: true, + Type: sdk.HookConfigTypeString, + }, + "payload": sdk.WorkflowNodeHookConfigValue{ + Value: "{}", + Configurable: true, + Type: sdk.HookConfigTypeString, + }, + }, + }, + }, + }, + Joins: []sdk.Node{ + { + Type: "join", + JoinContext: []sdk.NodeJoin{ + { + ParentName: "B", + }, + { + ParentName: "C", + }, + }, + Triggers: []sdk.NodeTrigger{ + { + ChildNode: sdk.Node{ + Name: "D", + Ref: "D", + Type: "pipeline", + Context: &sdk.NodeContext{ + PipelineName: "pipeline", + }, + }, + }, + { + ChildNode: sdk.Node{ + Name: "E", + Ref: "E", + Type: "pipeline", + Context: &sdk.NodeContext{ + PipelineName: "pipeline", + }, + }, + }, + { + ChildNode: sdk.Node{ + Name: "F", + Ref: "F", + Type: "pipeline", + Context: &sdk.NodeContext{ + PipelineName: "pipeline", + }, + }, + }, + }, + }, + { + Type: "join", + JoinContext: []sdk.NodeJoin{ + { + ParentName: "D", + }, + { + ParentName: "E", + }, + }, + Triggers: []sdk.NodeTrigger{ + { + ChildNode: sdk.Node{ + Name: "G", + Ref: "G", + Type: "pipeline", + Context: &sdk.NodeContext{ + PipelineName: "pipeline", + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "Complex workflow with integration should not raise an error", + fields: fields{ + Name: "myworkflow", + Version: exportentities.ActionVersion1, + PipelineName: "pipeline", + ProjectIntegrationName: "integration", + }, + wantErr: false, + want: sdk.Workflow{ + Name: "myworkflow", + HistoryLength: sdk.DefaultHistoryLength, + WorkflowData: &sdk.WorkflowData{ + Node: sdk.Node{ + Name: "pipeline", + Ref: "pipeline", + Type: "pipeline", + Context: &sdk.NodeContext{ + PipelineName: "pipeline", + ProjectIntegrationName: "integration", + }, + }, + }, + }, + }, + { + name: "Root and a outgoing hook should not raise an error", + fields: fields{ + Name: "myworkflow", + Version: exportentities.ActionVersion1, + Workflow: map[string]v1.NodeEntry{ + "A": { + PipelineName: "pipeline", + }, + "B": { + OutgoingHookModelName: "webhook", + OutgoingHookConfig: map[string]string{ + "url": "https://www.ovh.com", + }, + DependsOn: []string{"A"}, + }, + }, + }, + wantErr: false, + want: sdk.Workflow{ + Name: "myworkflow", + HistoryLength: sdk.DefaultHistoryLength, + WorkflowData: &sdk.WorkflowData{ + Node: sdk.Node{ + Name: "A", + Ref: "pipeline", + Type: "pipeline", + Context: &sdk.NodeContext{ + PipelineName: "pipeline", + }, + Triggers: []sdk.NodeTrigger{ + { + ChildNode: sdk.Node{ + Name: "B", + Type: sdk.NodeTypeOutGoingHook, + Context: &sdk.NodeContext{}, + OutGoingHookContext: &sdk.NodeOutGoingHook{ + HookModelName: "webhook", + Config: sdk.WorkflowNodeHookConfig{ + "url": sdk.WorkflowNodeHookConfigValue{ + Value: "https://www.ovh.com", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + for _, tt := range tsts { + t.Run(tt.name, func(t *testing.T) { + w := v1.Workflow{ + Name: tt.fields.Name, + Description: tt.fields.Description, + Version: tt.fields.Version, + Workflow: tt.fields.Workflow, + Hooks: tt.fields.Hooks, + Conditions: tt.fields.Conditions, + When: tt.fields.When, + PipelineName: tt.fields.PipelineName, + ApplicationName: tt.fields.ApplicationName, + EnvironmentName: tt.fields.EnvironmentName, + ProjectIntegrationName: tt.fields.ProjectIntegrationName, + PipelineHooks: tt.fields.PipelineHooks, + Permissions: tt.fields.Permissions, + HistoryLength: &tt.fields.HistoryLength, + OneAtATime: tt.fields.OneAtATime, + } + + got, err := exportentities.ParseWorkflow(w) + if (err != nil) != tt.wantErr { + t.Errorf("Workflow.GetWorkflow() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if tt.wantErr { + assert.Error(t, err) + return + } + + got.HookModels = nil + got.OutGoingHookModels = nil + got.Applications = nil + got.Pipelines = nil + got.Environments = nil + got.ProjectIntegrations = nil + + expextedValues, _ := dump.ToStringMap(tt.want) + actualValues, _ := dump.ToStringMap(got) + + var keysExpextedValues []string + for k := range expextedValues { + keysExpextedValues = append(keysExpextedValues, k) + } + sort.Strings(keysExpextedValues) + + for _, expectedKey := range keysExpextedValues { + expectedValue := expextedValues[expectedKey] + actualValue, ok := actualValues[expectedKey] + if strings.Contains(expectedKey, ".Ref") { + assert.NotEmpty(t, actualValue, "value %s is empty but shoud not be empty", expectedKey) + } else { + assert.True(t, ok, "%s not found", expectedKey) + assert.Equal(t, expectedValue, actualValue, "value %s doesn't match. Got %s but want %s", expectedKey, actualValue, expectedValue) + } + } + + for actualKey := range actualValues { + _, ok := expextedValues[actualKey] + assert.True(t, ok, "got %s, but not found is expected workflow", actualKey) + } + }) + } +} diff --git a/sdk/exportentities/v2/workflow.go b/sdk/exportentities/v2/workflow.go new file mode 100644 index 0000000000..1d58f393b0 --- /dev/null +++ b/sdk/exportentities/v2/workflow.go @@ -0,0 +1,734 @@ +package v2 + +import ( + "context" + "fmt" + "math/rand" + "sort" + "strings" + "time" + + "github.com/fsamin/go-dump" + + "github.com/ovh/cds/sdk" +) + +// Workflow is the "as code" representation of a sdk.Workflow +type Workflow struct { + Name string `json:"name" yaml:"name" jsonschema_description:"The name of the workflow."` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Version string `json:"version,omitempty" yaml:"version,omitempty" jsonschema_description:"Version for the yaml syntax, latest is v1.0."` + Template string `json:"template,omitempty" yaml:"template,omitempty" jsonschema_description:"Optional path of the template used to generate the workflow."` + + Workflow map[string]NodeEntry `json:"workflow,omitempty" yaml:"workflow,omitempty" jsonschema_description:"Workflow nodes list."` + Hooks map[string][]HookEntry `json:"hooks,omitempty" yaml:"hooks,omitempty" jsonschema_description:"Workflow hooks list."` + + // extra workflow data + Permissions map[string]int `json:"permissions,omitempty" yaml:"permissions,omitempty" jsonschema_description:"The permissions for the workflow (ex: myGroup: 7).\nhttps://ovh.github.io/cds/docs/concepts/permissions"` + Metadata map[string]string `json:"metadata,omitempty" yaml:"metadata,omitempty"` + PurgeTags []string `json:"purge_tags,omitempty" yaml:"purge_tags,omitempty"` + Notifications []NotificationEntry `json:"notifications,omitempty" yaml:"notifications,omitempty"` // This is used when the workflow have only one pipeline + HistoryLength *int64 `json:"history_length,omitempty" yaml:"history_length,omitempty"` +} + +// NodeEntry represents a node as code +type NodeEntry struct { + ID int64 `json:"-" yaml:"-"` + DependsOn []string `json:"depends_on,omitempty" yaml:"depends_on,omitempty" jsonschema_description:"Names of the parent nodes, can be pipelines, forks or joins."` + Conditions *ConditionEntry `json:"conditions,omitempty" yaml:"conditions,omitempty" jsonschema_description:"Conditions to run this node.\nhttps://ovh.github.io/cds/docs/concepts/workflow/run-conditions."` + When []string `json:"when,omitempty" yaml:"when,omitempty" jsonschema_description:"Set manual and status condition (ex: 'success')."` //This is used only for manual and success condition + PipelineName string `json:"pipeline,omitempty" yaml:"pipeline,omitempty" jsonschema_description:"The name of a pipeline used for pipeline node."` + ApplicationName string `json:"application,omitempty" yaml:"application,omitempty" jsonschema_description:"The application to use in the context of the node.\nhttps://ovh.github.io/cds/docs/concepts/workflow/pipeline-context"` + EnvironmentName string `json:"environment,omitempty" yaml:"environment,omitempty" jsonschema_description:"The environment to use in the context of the node.\nhttps://ovh.github.io/cds/docs/concepts/workflow/pipeline-context"` + ProjectIntegrationName string `json:"integration,omitempty" yaml:"integration,omitempty" jsonschema_description:"The integration to use in the context of the node.\nhttps://ovh.github.io/cds/docs/concepts/workflow/pipeline-context"` + OneAtATime *bool `json:"one_at_a_time,omitempty" yaml:"one_at_a_time,omitempty" jsonschema_description:"Set to true if you want to limit the execution of this node to one at a time."` + Payload map[string]interface{} `json:"payload,omitempty" yaml:"payload,omitempty"` + Parameters map[string]string `json:"parameters,omitempty" yaml:"parameters,omitempty" jsonschema_description:"List of parameters for the workflow."` + OutgoingHookModelName string `json:"trigger,omitempty" yaml:"trigger,omitempty"` + OutgoingHookConfig map[string]string `json:"config,omitempty" yaml:"config,omitempty"` + Permissions map[string]int `json:"permissions,omitempty" yaml:"permissions,omitempty" jsonschema_description:"The permissions for the node (ex: myGroup: 7).\nhttps://ovh.github.io/cds/docs/concepts/permissions"` +} + +type ConditionEntry struct { + PlainConditions []PlainConditionEntry `json:"plain,omitempty" yaml:"check,omitempty"` + LuaScript string `json:"script,omitempty" yaml:"script,omitempty"` +} + +//WorkflowNodeCondition represents a condition to trigger ot not a pipeline in a workflow. Operator can be =, !=, regex +type PlainConditionEntry struct { + Variable string `json:"variable" yaml:"variable"` + Operator string `json:"operator" yaml:"operator"` + Value string `json:"value" yaml:"value"` +} + +// HookEntry represents a hook as code +type HookEntry struct { + Model string `json:"type,omitempty" yaml:"type,omitempty" jsonschema_description:"Model of the hook.\nhttps://ovh.github.io/cds/docs/concepts/workflow/hooks"` + Config map[string]string `json:"config,omitempty" yaml:"config,omitempty"` + Conditions *sdk.WorkflowNodeConditions `json:"conditions,omitempty" yaml:"conditions,omitempty" jsonschema_description:"Conditions to run this hook.\nhttps://ovh.github.io/cds/docs/concepts/workflow/run-conditions."` +} + +type ExportOptions func(w sdk.Workflow, exportedWorkflow *Workflow) error + +//NewWorkflow creates a new exportable workflow +func NewWorkflow(ctx context.Context, w sdk.Workflow, version string, opts ...ExportOptions) (Workflow, error) { + exportedWorkflow := Workflow{} + exportedWorkflow.Name = w.Name + exportedWorkflow.Description = w.Description + exportedWorkflow.Version = version + exportedWorkflow.Workflow = map[string]NodeEntry{} + exportedWorkflow.Hooks = map[string][]HookEntry{} + if len(w.Metadata) > 0 { + exportedWorkflow.Metadata = make(map[string]string, len(w.Metadata)) + for k, v := range w.Metadata { + // don't export empty metadata + if v != "" { + exportedWorkflow.Metadata[k] = v + } + } + } + + if w.HistoryLength > 0 && w.HistoryLength != sdk.DefaultHistoryLength { + exportedWorkflow.HistoryLength = &w.HistoryLength + } + + exportedWorkflow.PurgeTags = w.PurgeTags + + nodes := w.WorkflowData.Array() + + for _, n := range nodes { + if n.Type == sdk.NodeTypeJoin && !joinAsNode(n) { + continue + } + + entry, err := craftNodeEntry(w, *n) + if err != nil { + return exportedWorkflow, sdk.WrapError(err, "Unable to craft Node entry %s", n.Name) + } + exportedWorkflow.Workflow[n.Name] = entry + + for _, h := range n.Hooks { + if exportedWorkflow.Hooks == nil { + exportedWorkflow.Hooks = make(map[string][]HookEntry) + } + + m := sdk.GetBuiltinHookModelByName(h.HookModelName) + if m == nil { + return exportedWorkflow, sdk.WrapError(sdk.ErrNotFound, "unable to find hook model %s", h.HookModelName) + } + pipHook := HookEntry{ + Model: h.HookModelName, + Config: h.Config.Values(m.DefaultConfig), + Conditions: &h.Conditions, + } + + if h.Conditions.LuaScript == "" && len(h.Conditions.PlainConditions) == 0 { + pipHook.Conditions = nil + } + + exportedWorkflow.Hooks[n.Name] = append(exportedWorkflow.Hooks[n.Name], pipHook) + } + } + + //Notifications + if err := craftNotifications(ctx, w, &exportedWorkflow); err != nil { + return exportedWorkflow, err + } + + for _, f := range opts { + if err := f(w, &exportedWorkflow); err != nil { + return exportedWorkflow, sdk.WrapError(err, "Unable to run function") + } + } + + if w.Template != nil { + path := fmt.Sprintf("%s/%s", w.Template.Group.Name, w.Template.Slug) + exportedWorkflow.Template = path + } + + return exportedWorkflow, nil +} + +func joinAsNode(n *sdk.Node) bool { + return n.Context != nil && (n.Context.Conditions.LuaScript != "" || len(n.Context.Conditions.PlainConditions) > 0) +} + +func craftNodeEntry(w sdk.Workflow, n sdk.Node) (NodeEntry, error) { + entry := NodeEntry{} + + ancestors := []string{} + + if n.Type != sdk.NodeTypeJoin { + nodes := w.WorkflowData.Array() + for _, node := range nodes { + if n.Name == node.Name { + continue + } + for _, t := range node.Triggers { + if t.ChildNode.Name == n.Name { + if node.Type == sdk.NodeTypeJoin && !joinAsNode(node) { + for _, jp := range node.JoinContext { + parentNode := w.WorkflowData.NodeByRef(jp.ParentName) + if parentNode == nil { + return entry, sdk.WithStack(sdk.ErrWorkflowNodeNotFound) + } + ancestors = append(ancestors, parentNode.Name) + } + } else { + ancestors = append(ancestors, node.Name) + } + } + } + } + } else { + for _, jc := range n.JoinContext { + ancestors = append(ancestors, jc.ParentName) + } + } + + sort.Strings(ancestors) + entry.DependsOn = ancestors + + if n.Context != nil && n.Context.PipelineName != "" { + entry.PipelineName = n.Context.PipelineName + } + + if n.Context != nil { + conditions := make([]sdk.WorkflowNodeCondition, 0) + for _, c := range n.Context.Conditions.PlainConditions { + if c.Operator == sdk.WorkflowConditionsOperatorEquals && + c.Value == sdk.StatusSuccess && + c.Variable == "cds.status" { + entry.When = append(entry.When, "success") + } else if c.Operator == sdk.WorkflowConditionsOperatorEquals && + c.Value == "true" && + c.Variable == "cds.manual" { + entry.When = append(entry.When, "manual") + } else { + conditions = append(conditions, c) + } + } + + if len(conditions) > 0 || n.Context.Conditions.LuaScript != "" { + entry.Conditions = &ConditionEntry{ + PlainConditions: make([]PlainConditionEntry, 0, len(conditions)), + LuaScript: n.Context.Conditions.LuaScript, + } + for _, c := range conditions { + entry.Conditions.PlainConditions = append(entry.Conditions.PlainConditions, PlainConditionEntry{ + Value: c.Value, + Operator: c.Operator, + Variable: c.Variable, + }) + } + } + + if n.Context.ApplicationName != "" { + entry.ApplicationName = n.Context.ApplicationName + } + if n.Context.EnvironmentName != "" { + entry.EnvironmentName = n.Context.EnvironmentName + } + if n.Context.ProjectIntegrationName != "" { + entry.ProjectIntegrationName = n.Context.ProjectIntegrationName + } + + if n.Context.Mutex { + entry.OneAtATime = &n.Context.Mutex + } + + if n.Context.HasDefaultPayload() { + enc := dump.NewDefaultEncoder() + enc.ExtraFields.DetailedMap = false + enc.ExtraFields.DetailedStruct = false + enc.ExtraFields.Len = false + enc.ExtraFields.Type = false + enc.Formatters = nil + m, err := enc.ToMap(n.Context.DefaultPayload) + if err != nil { + return entry, sdk.WrapError(err, "Unable to encode payload") + } + entry.Payload = m + } + + if len(n.Context.DefaultPipelineParameters) > 0 { + entry.Parameters = sdk.ParametersToMap(n.Context.DefaultPipelineParameters) + } + } + + if len(n.Groups) > 0 { + entry.Permissions = map[string]int{} + for _, gr := range n.Groups { + entry.Permissions[gr.Group.Name] = gr.Permission + } + } + + if n.OutGoingHookContext != nil { + entry.OutgoingHookModelName = n.OutGoingHookContext.HookModelName + + m := sdk.GetBuiltinOutgoingHookModelByName(entry.OutgoingHookModelName) + if m == nil { + return entry, sdk.WrapError(sdk.ErrNotFound, "unable to find outgoing hook model %s", entry.OutgoingHookModelName) + } + entry.OutgoingHookConfig = n.OutGoingHookContext.Config.Values(m.DefaultConfig) + } + + return entry, nil +} + +// WorkflowWithPermissions export workflow with permissions +func WorkflowWithPermissions(w sdk.Workflow, exportedWorkflow *Workflow) error { + exportedWorkflow.Permissions = make(map[string]int, len(w.Groups)) + for _, p := range w.Groups { + exportedWorkflow.Permissions[p.Group.Name] = p.Permission + } + + for _, node := range w.WorkflowData.Array() { + if len(exportedWorkflow.Workflow) > 1 { // Else the permissions are the same than the workflow + for exportedNodeName, entry := range exportedWorkflow.Workflow { + if entry.Permissions == nil { + entry.Permissions = map[string]int{} + } + if node.Name == exportedNodeName { + for _, p := range node.Groups { + entry.Permissions[p.Group.Name] = p.Permission + } + exportedWorkflow.Workflow[exportedNodeName] = entry + } + } + } + } + + return nil +} + +// WorkflowSkipIfOnlyOneRepoWebhook skips the repo webhook if it's the only one +// It also won't export the default payload +func WorkflowSkipIfOnlyOneRepoWebhook(w sdk.Workflow, exportedWorkflow *Workflow) error { + for nodeName, hs := range exportedWorkflow.Hooks { + if nodeName == w.WorkflowData.Node.Name && len(hs) == 1 { + if hs[0].Model == sdk.RepositoryWebHookModelName { + delete(exportedWorkflow.Hooks, nodeName) + if exportedWorkflow.Workflow != nil { + for nodeName := range exportedWorkflow.Workflow { + if nodeName == w.WorkflowData.Node.Name { + entry := exportedWorkflow.Workflow[nodeName] + entry.Payload = nil + exportedWorkflow.Workflow[nodeName] = entry + break + } + } + } + break + } + } + } + + return nil +} + +func (w Workflow) GetName() string { + return w.Name +} + +func (w Workflow) GetVersion() string { + return w.Version +} + +func (w *Workflow) SetTemplatePath(path string) { + w.Template = path +} + +// GetWorkflow returns a fresh sdk.Workflow +func (w Workflow) GetWorkflow() (*sdk.Workflow, error) { + var wf = new(sdk.Workflow) + wf.Name = w.Name + wf.Description = w.Description + wf.WorkflowData = &sdk.WorkflowData{} + // Init map + wf.Applications = make(map[int64]sdk.Application) + wf.Pipelines = make(map[int64]sdk.Pipeline) + wf.Environments = make(map[int64]sdk.Environment) + wf.ProjectIntegrations = make(map[int64]sdk.ProjectIntegration) + + if err := w.CheckValidity(); err != nil { + return nil, sdk.WrapError(err, "Unable to check validity") + } + if err := w.CheckDependencies(); err != nil { + return nil, sdk.WrapError(err, "Unable to check dependencies") + } + wf.PurgeTags = w.PurgeTags + if len(w.Metadata) > 0 { + wf.Metadata = make(map[string]string, len(w.Metadata)) + for k, v := range w.Metadata { + wf.Metadata[k] = v + } + } + if w.HistoryLength != nil && *w.HistoryLength > 0 { + wf.HistoryLength = *w.HistoryLength + } else { + wf.HistoryLength = sdk.DefaultHistoryLength + } + + rand.Seed(time.Now().Unix()) + var attempt int + fakeID := rand.Int63n(5000) + // attempt is there to avoid infinite loop, but it should not happened becase we check validity and dependencies earlier + for len(w.Workflow) != 0 && attempt < 10000 { + for name, entry := range w.Workflow { + entry.ID = fakeID + ok, err := entry.processNode(name, wf) + if err != nil { + return nil, sdk.WrapError(err, "Unable to process node") + } + if ok { + delete(w.Workflow, name) + fakeID++ + } + } + attempt++ + } + if len(w.Workflow) > 0 { + return nil, sdk.WithStack(fmt.Errorf("unable to process %+v", w.Workflow)) + } + + //Process hooks + wf.VisitNode(w.processHooks) + + //Compute permissions + wf.Groups = make([]sdk.GroupPermission, 0, len(w.Permissions)) + for g, p := range w.Permissions { + perm := sdk.GroupPermission{Group: sdk.Group{Name: g}, Permission: p} + wf.Groups = append(wf.Groups, perm) + } + + //Compute notifications + if err := w.processNotifications(wf); err != nil { + return nil, err + } + + // if there is a template instance id on the workflow export, add it + if w.Template != "" { + templatePath := strings.Split(w.Template, "/") + if len(templatePath) != 2 { + return nil, sdk.WithStack(fmt.Errorf("Invalid template path")) + } + wf.Template = &sdk.WorkflowTemplate{ + Group: &sdk.Group{Name: templatePath[0]}, + Slug: templatePath[1], + } + } + + wf.SortNode() + + return wf, nil +} + +func (w Workflow) CheckValidity() error { + mError := new(sdk.MultiError) + + //Check valid application name + rx := sdk.NamePatternRegex + if !rx.MatchString(w.Name) { + mError.Append(fmt.Errorf("workflow name %s do not respect pattern %s", w.Name, sdk.NamePattern)) + } + + for name := range w.Hooks { + if _, ok := w.Workflow[name]; !ok { + mError.Append(fmt.Errorf("error: wrong usage: invalid hook on %s", name)) + } + } + + //Checks map notifications validity + mError.Append(CheckWorkflowNotificationsValidity(w)) + + if mError.IsEmpty() { + return nil + } + return mError +} + +func (w Workflow) CheckDependencies() error { + mError := new(sdk.MultiError) + for s, e := range w.Workflow { + if err := e.checkDependencies(s, w); err != nil { + mError.Append(fmt.Errorf("Error: %s invalid: %v", s, err)) + } + } + + if mError.IsEmpty() { + return nil + } + return mError +} + +func (e NodeEntry) checkDependencies(nodeName string, w Workflow) error { + mError := new(sdk.MultiError) +nextDep: + for _, d := range e.DependsOn { + for s := range w.Workflow { + if s == d { + continue nextDep + } + } + mError.Append(fmt.Errorf("the pipeline %s depends on an unknown pipeline: %s", nodeName, d)) + } + if mError.IsEmpty() { + return nil + } + return mError +} + +func (e *NodeEntry) processNode(name string, w *sdk.Workflow) (bool, error) { + // Find WorkflowNodeAncestors + exist, err := e.processNodeAncestors(name, w) + if err != nil { + return false, err + } + + if exist { + return true, nil + } + + return false, nil +} + +func (e *NodeEntry) processNodeAncestors(name string, w *sdk.Workflow) (bool, error) { + var ancestorsExist = true + var ancestors []*sdk.Node + + if len(e.DependsOn) == 1 { + a := e.DependsOn[0] + //Looking for the ancestor + ancestor := w.WorkflowData.NodeByName(a) + if ancestor == nil { + ancestorsExist = false + } + ancestors = append(ancestors, ancestor) + } else { + for _, a := range e.DependsOn { + //Looking for the ancestor + ancestor := w.WorkflowData.NodeByName(a) + if ancestor == nil { + ancestorsExist = false + break + } + ancestors = append(ancestors, ancestor) + } + } + + if !ancestorsExist { + return false, nil + } + + n, err := e.getNode(name) + if err != nil { + return false, err + } + + switch len(ancestors) { + case 0: + w.WorkflowData.Node = *n + return true, nil + case 1: + w.AddTrigger(ancestors[0].Name, *n) + return true, nil + default: + if n != nil && n.Type == sdk.NodeTypeJoin && joinAsNode(n) { + w.WorkflowData.Joins = append(w.WorkflowData.Joins, *n) + return true, nil + } + } + + // Compute join + + // Try to find an existing join with the same references + var join *sdk.Node + for i := range w.WorkflowData.Joins { + j := &w.WorkflowData.Joins[i] + var joinFound = true + + for _, ref := range j.JoinContext { + var refFound bool + for _, a := range e.DependsOn { + if ref.ParentName == a { + refFound = true + break + } + } + if !refFound { + joinFound = false + break + } + } + + if joinFound { + j.Ref = fmt.Sprintf("fakeRef%d", e.ID) + join = j + } + } + + var appendJoin bool + if join == nil { + joinContext := make([]sdk.NodeJoin, 0, len(e.DependsOn)) + for _, d := range e.DependsOn { + joinContext = append(joinContext, sdk.NodeJoin{ + ParentName: d, + }) + } + join = &sdk.Node{ + JoinContext: joinContext, + Type: sdk.NodeTypeJoin, + Ref: fmt.Sprintf("fakeRef%d", e.ID), + } + appendJoin = true + } + + join.Triggers = append(join.Triggers, sdk.NodeTrigger{ + ChildNode: *n, + }) + + if appendJoin { + w.WorkflowData.Joins = append(w.WorkflowData.Joins, *join) + } + return true, nil +} + +func (e *NodeEntry) getNode(name string) (*sdk.Node, error) { + var mutex bool + if e.OneAtATime != nil && *e.OneAtATime { + mutex = true + } + node := &sdk.Node{ + Name: name, + Ref: name, + Type: sdk.NodeTypeFork, + Context: &sdk.NodeContext{ + PipelineName: e.PipelineName, + ApplicationName: e.ApplicationName, + EnvironmentName: e.EnvironmentName, + ProjectIntegrationName: e.ProjectIntegrationName, + Mutex: mutex, + }, + } + + if e.PipelineName != "" { + node.Type = sdk.NodeTypePipeline + } else if e.OutgoingHookModelName != "" { + node.Type = sdk.NodeTypeOutGoingHook + } else if len(e.DependsOn) > 1 { + node.Type = sdk.NodeTypeJoin + node.JoinContext = make([]sdk.NodeJoin, 0, len(e.DependsOn)) + for _, parent := range e.DependsOn { + node.JoinContext = append(node.JoinContext, sdk.NodeJoin{ParentName: parent}) + } + } + + if len(e.Permissions) > 0 { + //Compute permissions + node.Groups = make([]sdk.GroupPermission, 0, len(e.Permissions)) + for g, p := range e.Permissions { + perm := sdk.GroupPermission{Group: sdk.Group{Name: g}, Permission: p} + node.Groups = append(node.Groups, perm) + } + } + + if e.Conditions != nil { + node.Context.Conditions = sdk.WorkflowNodeConditions{ + PlainConditions: make([]sdk.WorkflowNodeCondition, 0, len(e.Conditions.PlainConditions)), + LuaScript: e.Conditions.LuaScript, + } + for _, c := range e.Conditions.PlainConditions { + node.Context.Conditions.PlainConditions = append(node.Context.Conditions.PlainConditions, sdk.WorkflowNodeCondition{ + Variable: c.Variable, + Operator: c.Operator, + Value: c.Value, + }) + } + } + + if len(e.Payload) > 0 { + if len(e.DependsOn) > 0 { + return nil, sdk.WrapError(sdk.ErrInvalidNodeDefaultPayload, "Default payload cannot be set on another node than the first one (node : %s)", name) + } + node.Context.DefaultPayload = e.Payload + } + + mapPipelineParameters := sdk.ParametersFromMap(e.Parameters) + node.Context.DefaultPipelineParameters = mapPipelineParameters + + for _, w := range e.When { + switch w { + case "success": + node.Context.Conditions.PlainConditions = append(node.Context.Conditions.PlainConditions, sdk.WorkflowNodeCondition{ + Operator: sdk.WorkflowConditionsOperatorEquals, + Value: sdk.StatusSuccess, + Variable: "cds.status", + }) + case "manual": + node.Context.Conditions.PlainConditions = append(node.Context.Conditions.PlainConditions, sdk.WorkflowNodeCondition{ + Operator: sdk.WorkflowConditionsOperatorEquals, + Value: "true", + Variable: "cds.manual", + }) + default: + return nil, fmt.Errorf("Unsupported when condition %s", w) + } + } + + if e.OneAtATime != nil { + node.Context.Mutex = *e.OneAtATime + } + + if e.OutgoingHookModelName != "" { + node.Type = sdk.NodeTypeOutGoingHook + config := sdk.WorkflowNodeHookConfig{} + for k, v := range e.OutgoingHookConfig { + config[k] = sdk.WorkflowNodeHookConfigValue{ + Value: v, + } + } + node.OutGoingHookContext = &sdk.NodeOutGoingHook{ + Config: config, + HookModelName: e.OutgoingHookModelName, + } + } + return node, nil +} + +func (w *Workflow) processHooks(n *sdk.Node, wf *sdk.Workflow) { + var addHooks = func(hooks []HookEntry) { + for _, h := range hooks { + cfg := make(sdk.WorkflowNodeHookConfig, len(h.Config)) + for k, v := range h.Config { + var hType string + switch h.Model { + case sdk.KafkaHookModelName, sdk.RabbitMQHookModelName: + if k == sdk.HookModelIntegration { + hType = sdk.HookConfigTypeIntegration + } else { + hType = sdk.HookConfigTypeString + } + default: + hType = sdk.HookConfigTypeString + } + cfg[k] = sdk.WorkflowNodeHookConfigValue{ + Value: v, + Configurable: true, + Type: hType, + } + } + + hook := sdk.NodeHook{ + Config: cfg, + HookModelName: h.Model, + } + + if h.Conditions != nil { + hook.Conditions = *h.Conditions + } + n.Hooks = append(n.Hooks, hook) + } + } + addHooks(w.Hooks[n.Name]) +} diff --git a/sdk/exportentities/workflow_notif_test.go b/sdk/exportentities/v2/workflow_notif_test.go similarity index 63% rename from sdk/exportentities/workflow_notif_test.go rename to sdk/exportentities/v2/workflow_notif_test.go index 2b5fa51b80..2dfc955505 100644 --- a/sdk/exportentities/workflow_notif_test.go +++ b/sdk/exportentities/v2/workflow_notif_test.go @@ -1,7 +1,8 @@ -package exportentities_test +package v2_test import ( "context" + v2 "github.com/ovh/cds/sdk/exportentities/v2" "reflect" "testing" @@ -28,7 +29,7 @@ func Test_checkWorkflowNotificationsValidity(t *testing.T) { want: nil, args: args{ yaml: `name: test1 -version: v1.0 +version: v2.0 workflow: DDOS-me: pipeline: DDOS-me @@ -50,45 +51,48 @@ workflow: metadata: default_tags: git.branch,git.author notifications: - DDOS-me,DDOS-me_2: - - type: jabber - settings: - on_success: always - on_failure: change - on_start: true - send_to_groups: true - send_to_author: false - recipients: - - q - template: - subject: '{{.cds.project}}/{{.cds.application}} {{.cds.pipeline}} {{.cds.environment}}#{{.cds.version}} - {{.cds.status}}' - body: |- - Project : {{.cds.project}} - Application : {{.cds.application}} - Pipeline : {{.cds.pipeline}}/{{.cds.environment}}#{{.cds.version}} - Status : {{.cds.status}} - Details : {{.cds.buildURL}} - Triggered by : {{.cds.triggered_by.username}} - Branch : {{.git.branch}} - DDOS-me_2: - - type: email - settings: - template: - subject: '{{.cds.project}}/{{.cds.application}} {{.cds.pipeline}} {{.cds.environment}}#{{.cds.version}} - {{.cds.status}}' - body: |- - Project : {{.cds.project}} - Application : {{.cds.application}} - Pipeline : {{.cds.pipeline}}/{{.cds.environment}}#{{.cds.version}} - Status : {{.cds.status}} - Details : {{.cds.buildURL}} - Triggered by : {{.cds.triggered_by.username}} - Branch : {{.git.branch}} - - type: vcs - settings: - template: - disable_comment: true +- type: jabber + pipelines: + - DDOS-me + - DDOS-me_2 + settings: + on_success: always + on_failure: change + on_start: true + send_to_groups: true + send_to_author: false + recipients: + - q + template: + subject: '{{.cds.project}}/{{.cds.application}} {{.cds.pipeline}} {{.cds.environment}}#{{.cds.version}} + {{.cds.status}}' + body: |- + Project : {{.cds.project}} + Application : {{.cds.application}} + Pipeline : {{.cds.pipeline}}/{{.cds.environment}}#{{.cds.version}} + Status : {{.cds.status}} + Details : {{.cds.buildURL}} + Triggered by : {{.cds.triggered_by.username}} + Branch : {{.git.branch}} +- type: email + pipelines: + - DDOS-me_2 + settings: + template: + subject: '{{.cds.project}}/{{.cds.application}} {{.cds.pipeline}} {{.cds.environment}}#{{.cds.version}} + {{.cds.status}}' + body: |- + Project : {{.cds.project}} + Application : {{.cds.application}} + Pipeline : {{.cds.pipeline}}/{{.cds.environment}}#{{.cds.version}} + Status : {{.cds.status}} + Details : {{.cds.buildURL}} + Triggered by : {{.cds.triggered_by.username}} + Branch : {{.git.branch}} +- type: vcs + settings: + template: + disable_comment: true `, }, }, @@ -96,9 +100,9 @@ notifications: for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - var w exportentities.Workflow + var w v2.Workflow test.NoError(t, yaml.Unmarshal([]byte(tt.args.yaml), &w)) - if got := exportentities.CheckWorkflowNotificationsValidity(w); got != tt.want { + if got := v2.CheckWorkflowNotificationsValidity(w); got != tt.want { t.Errorf("checkWorkflowNotificationsValidity() = %#v, want %v", got, tt.want) } }) @@ -107,7 +111,7 @@ notifications: func Test_processNotificationValues(t *testing.T) { type args struct { - notif exportentities.NotificationEntry + notif v2.NotificationEntry } tests := []struct { name string @@ -119,7 +123,7 @@ func Test_processNotificationValues(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := exportentities.ProcessNotificationValues(tt.args.notif) + got, err := v2.ProcessNotificationValues(tt.args.notif) if (err != nil) != tt.wantErr { t.Errorf("processNotificationValues() error = %v, wantErr %v", err, tt.wantErr) return @@ -138,33 +142,49 @@ func TestFromYAMLToYAMLWithNotif(t *testing.T) { wantErr bool }{ { - name: "test one pipeline with one notif", + name: "two pipelines with one notif", yaml: `name: test-notif-1 -version: v1.0 -pipeline: test -notify: +version: v2.0 +workflow: + test: + pipeline: test + test_2: + depends_on: + - test + when: + - success + pipeline: test +notifications: - type: jabber - settings: - template: - subject: '{{.cds.project}}/{{.cds.application}} {{.cds.pipeline}} {{.cds.environment}}#{{.cds.version}} - {{.cds.status}}' - body: |- - Project : {{.cds.project}} - Application : {{.cds.application}} - Pipeline : {{.cds.pipeline}}/{{.cds.environment}}#{{.cds.version}} - Status : {{.cds.status}} - Details : {{.cds.buildURL}} - Triggered by : {{.cds.triggered_by.username}} - Branch : {{.git.branch}} + pipelines: + - test + - test_2 `, }, { - name: "test one pipeline with two notif", - yaml: `name: test-notif-2 -version: v1.0 -pipeline: test -notify: -- type: jabber + name: "two pipelines with two notifs", + yaml: `name: test-notif-1 +version: v2.0 +workflow: + test: + pipeline: test + test_2: + depends_on: + - test + when: + - success + pipeline: test +notifications: +- type: email + pipelines: + - test settings: + on_success: always + on_failure: change + on_start: true + send_to_groups: true + send_to_author: false + recipients: + - a template: subject: '{{.cds.project}}/{{.cds.application}} {{.cds.pipeline}} {{.cds.environment}}#{{.cds.version}} {{.cds.status}}' @@ -176,15 +196,11 @@ notify: Details : {{.cds.buildURL}} Triggered by : {{.cds.triggered_by.username}} Branch : {{.git.branch}} -- type: email +- type: jabber + pipelines: + - test + - test_2 settings: - on_success: always - on_failure: change - on_start: true - send_to_groups: true - send_to_author: false - recipients: - - a template: subject: '{{.cds.project}}/{{.cds.application}} {{.cds.pipeline}} {{.cds.environment}}#{{.cds.version}} {{.cds.status}}' @@ -196,79 +212,12 @@ notify: Details : {{.cds.buildURL}} Triggered by : {{.cds.triggered_by.username}} Branch : {{.git.branch}} -`, - }, { - name: "two pipelines with one notif", - yaml: `name: test-notif-1 -version: v1.0 -workflow: - test: - pipeline: test - test_2: - depends_on: - - test - when: - - success - pipeline: test -notifications: - test,test_2: - - type: jabber -`, - }, { - name: "two pipelines with two notifs", - yaml: `name: test-notif-1 -version: v1.0 -workflow: - test: - pipeline: test - test_2: - depends_on: - - test - when: - - success - pipeline: test -notifications: - test: - - type: email - settings: - on_success: always - on_failure: change - on_start: true - send_to_groups: true - send_to_author: false - recipients: - - a - template: - subject: '{{.cds.project}}/{{.cds.application}} {{.cds.pipeline}} {{.cds.environment}}#{{.cds.version}} - {{.cds.status}}' - body: |- - Project : {{.cds.project}} - Application : {{.cds.application}} - Pipeline : {{.cds.pipeline}}/{{.cds.environment}}#{{.cds.version}} - Status : {{.cds.status}} - Details : {{.cds.buildURL}} - Triggered by : {{.cds.triggered_by.username}} - Branch : {{.git.branch}} - test,test_2: - - type: jabber - settings: - template: - subject: '{{.cds.project}}/{{.cds.application}} {{.cds.pipeline}} {{.cds.environment}}#{{.cds.version}} - {{.cds.status}}' - body: |- - Project : {{.cds.project}} - Application : {{.cds.application}} - Pipeline : {{.cds.pipeline}}/{{.cds.environment}}#{{.cds.version}} - Status : {{.cds.status}} - Details : {{.cds.buildURL}} - Triggered by : {{.cds.triggered_by.username}} - Branch : {{.git.branch}} `, }, { name: "two pipelines with one notif without node name", yaml: `name: test-notif-2-pipeline-no-node -version: v1.0 +version: v2.0 workflow: test: pipeline: test @@ -279,15 +228,13 @@ workflow: - success pipeline: test notifications: - "": - - type: jabber +- type: jabber `, }, } for _, tst := range tests { t.Run(tst.name, func(t *testing.T) { - var yamlWorkflow exportentities.Workflow - err := exportentities.Unmarshal([]byte(tst.yaml), exportentities.FormatYAML, &yamlWorkflow) + yamlWorkflow, err := exportentities.UnmarshalWorkflow([]byte(tst.yaml)) if err != nil { if !tst.wantErr { t.Error("Unmarshal raised an error", err) @@ -298,7 +245,7 @@ notifications: t.Error("Unmarshal should return an error but it doesn't") return } - w, err := yamlWorkflow.GetWorkflow() + w, err := exportentities.ParseWorkflow(yamlWorkflow) if err != nil { if !tst.wantErr { t.Error("GetWorkflow raised an error", err) diff --git a/sdk/exportentities/v2/workflow_notification.go b/sdk/exportentities/v2/workflow_notification.go new file mode 100644 index 0000000000..aed58c386b --- /dev/null +++ b/sdk/exportentities/v2/workflow_notification.go @@ -0,0 +1,187 @@ +package v2 + +import ( + "context" + "fmt" + "sort" + + "github.com/ovh/cds/sdk" + "github.com/ovh/cds/sdk/log" +) + +// Booleans +var ( + True = true + False = false +) + +// NotificationEntry represents a notification set on a nodeEntry +type NotificationEntry struct { + Type string `json:"type" yaml:"type"` + Pipelines []string `json:"pipelines" yaml:"pipelines,omitempty"` + Settings *sdk.UserNotificationSettings `json:"settings,omitempty" yaml:"settings,omitempty"` +} + +// craftNotificationEntry returns the NotificationEntry and the name of the nodeEntries concerned +func craftNotificationEntry(ctx context.Context, w sdk.Workflow, notif sdk.WorkflowNotification) (NotificationEntry, error) { + entry := NotificationEntry{ + Pipelines: make([]string, len(notif.SourceNodeRefs)), + } + for i, ref := range notif.SourceNodeRefs { + node := w.WorkflowData.NodeByName(ref) + if node == nil { + log.Error(ctx, "unable to find workflow node %s", ref) + return entry, sdk.ErrWorkflowNodeNotFound + } + entry.Pipelines[i] = node.Name + } + sort.Strings(entry.Pipelines) + entry.Type = notif.Type + entry.Settings = ¬if.Settings + + // Replace the default values by nil + if entry.Settings.OnStart != nil && !*entry.Settings.OnStart { + entry.Settings.OnStart = nil + } + if entry.Settings.SendToGroups != nil && !*entry.Settings.SendToGroups { + entry.Settings.SendToGroups = nil + } + if entry.Settings.SendToAuthor != nil && *entry.Settings.SendToAuthor { + entry.Settings.SendToAuthor = nil + } + // Replace the default values by empty strings + if entry.Settings.OnSuccess == sdk.UserNotificationChange { + entry.Settings.OnSuccess = "" + } + if entry.Settings.OnFailure == sdk.UserNotificationAlways { + entry.Settings.OnFailure = "" + } + // Replace default templates by nil if they are default values + if entry.Settings.Template != nil { + defaultTemplate, has := sdk.UserNotificationTemplateMap[entry.Type] + if !has { + return entry, fmt.Errorf("workflow notification %s not found", entry.Type) + } + if defaultTemplate.Subject == entry.Settings.Template.Subject { + entry.Settings.Template.Subject = "" + } + if defaultTemplate.Body == entry.Settings.Template.Body { + entry.Settings.Template.Body = "" + } + if entry.Settings.Template.Body == "" && entry.Settings.Template.Subject == "" { + if entry.Settings.Template.DisableComment == nil || !*entry.Settings.Template.DisableComment { + entry.Settings.Template = nil + } + } + } + + // Finally if settings are all default, lets skip it + if entry.Settings.OnFailure == "" && + entry.Settings.OnStart == nil && + entry.Settings.OnSuccess == "" && + len(entry.Settings.Recipients) == 0 && + entry.Settings.SendToAuthor == nil && + entry.Settings.SendToGroups == nil && + entry.Settings.Template == nil { + entry.Settings = nil + } + + return entry, nil +} + +func craftNotifications(ctx context.Context, w sdk.Workflow, exportedWorkflow *Workflow) error { + for i, notif := range w.Notifications { + notifEntry, err := craftNotificationEntry(ctx, w, notif) + if err != nil { + return sdk.WrapError(err, "unable to craft notification") + } + if exportedWorkflow.Notifications == nil { + exportedWorkflow.Notifications = make([]NotificationEntry, len(w.Notifications)) + } + exportedWorkflow.Notifications[i] = notifEntry + + } + return nil +} + +func CheckWorkflowNotificationsValidity(w Workflow) error { + mError := new(sdk.MultiError) + for _, notifEntry := range w.Notifications { + for _, s := range notifEntry.Pipelines { + if _, ok := w.Workflow[s]; !ok { + mError.Append(fmt.Errorf("error: wrong usage: invalid notification on %s (%s is missing)", notifEntry.Pipelines, s)) + } + } + } + if len(*mError) == 0 { + return nil + } + return mError +} + +func ProcessNotificationValues(notif NotificationEntry) (sdk.WorkflowNotification, error) { + n := sdk.WorkflowNotification{ + Type: notif.Type, + } + defaultTemplate, has := sdk.UserNotificationTemplateMap[n.Type] + //Check the type + if !has { + return n, fmt.Errorf("Error: wrong usage: invalid notification type %s", n.Type) + } + //Default settings + if notif.Settings == nil { + n.Settings = sdk.UserNotificationSettings{ + OnFailure: sdk.UserNotificationAlways, + OnSuccess: sdk.UserNotificationChange, + OnStart: &False, + SendToAuthor: &True, + SendToGroups: &False, + Template: &defaultTemplate, + } + } else { + n.Settings = *notif.Settings + } + //Default values + if n.Settings.OnFailure == "" { + n.Settings.OnFailure = sdk.UserNotificationAlways + } + if n.Settings.OnSuccess == "" { + n.Settings.OnSuccess = sdk.UserNotificationChange + } + if n.Settings.OnStart == nil { + n.Settings.OnStart = &False + } + if n.Settings.SendToAuthor == nil { + n.Settings.SendToAuthor = &True + } + if n.Settings.SendToGroups == nil { + n.Settings.SendToGroups = &False + } + if n.Settings.Template == nil { + n.Settings.Template = &defaultTemplate + } else { + if n.Settings.Template.Subject == "" { + n.Settings.Template.Subject = defaultTemplate.Subject + } + if n.Settings.Template.Body == "" { + n.Settings.Template.Body = defaultTemplate.Body + } + if n.Settings.Template.DisableComment == nil || !*n.Settings.Template.DisableComment { + n.Settings.Template.DisableComment = nil + } + } + return n, nil +} + +func (w *Workflow) processNotifications(wrkflw *sdk.Workflow) error { + for _, notif := range w.Notifications { + n, err := ProcessNotificationValues(notif) + if err != nil { + return sdk.WrapError(err, "unable to process notification") + } + n.SourceNodeRefs = notif.Pipelines + wrkflw.Notifications = append(wrkflw.Notifications, n) + + } + return nil +} diff --git a/sdk/exportentities/workflow_test.go b/sdk/exportentities/v2/workflow_test.go similarity index 70% rename from sdk/exportentities/workflow_test.go rename to sdk/exportentities/v2/workflow_test.go index 2d6ba3bbf4..883c20ce38 100644 --- a/sdk/exportentities/workflow_test.go +++ b/sdk/exportentities/v2/workflow_test.go @@ -1,4 +1,4 @@ -package exportentities_test +package v2_test import ( "context" @@ -6,48 +6,37 @@ import ( "strings" "testing" - "github.com/ovh/cds/sdk/exportentities" - "github.com/fsamin/go-dump" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v2" + "github.com/ovh/cds/sdk/exportentities" + v2 "github.com/ovh/cds/sdk/exportentities/v2" + "github.com/ovh/cds/sdk" ) func TestWorkflow_checkDependencies(t *testing.T) { type fields struct { - Name string - Description string - Version string - Workflow map[string]exportentities.NodeEntry - Hooks map[string][]exportentities.HookEntry - Conditions *exportentities.ConditionEntry - When []string - PipelineName string - ApplicationName string - EnvironmentName string - ProjectIntegrationName string - PipelineHooks []exportentities.HookEntry - Permissions map[string]int - HistoryLength int64 + Name string + Description string + Version string + Workflow map[string]v2.NodeEntry + Hooks map[string][]v2.HookEntry + Permissions map[string]int + HistoryLength int64 } tests := []struct { name string fields fields wantErr bool }{ - { - name: "Simple Workflow without dependencies should not raise an error", - fields: fields{ - PipelineName: "pipeline", - }, - wantErr: false, - }, { name: "Complex Workflow with a dependency should not raise an error", fields: fields{ - Workflow: map[string]exportentities.NodeEntry{ + Name: "myWorkflow", + Version: exportentities.WorkflowVersion2, + Workflow: map[string]v2.NodeEntry{ "root": { PipelineName: "pipeline", }, @@ -62,7 +51,9 @@ func TestWorkflow_checkDependencies(t *testing.T) { { name: "Complex Workflow with a dependencies and a join should not raise an error", fields: fields{ - Workflow: map[string]exportentities.NodeEntry{ + Name: "myWorkflow", + Version: exportentities.WorkflowVersion2, + Workflow: map[string]v2.NodeEntry{ "root": { PipelineName: "pipeline", }, @@ -85,20 +76,13 @@ func TestWorkflow_checkDependencies(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - w := exportentities.Workflow{ - Name: tt.fields.Name, - Description: tt.fields.Description, - Version: tt.fields.Version, - Workflow: tt.fields.Workflow, - Hooks: tt.fields.Hooks, - Conditions: tt.fields.Conditions, - When: tt.fields.When, - PipelineName: tt.fields.PipelineName, - ApplicationName: tt.fields.ApplicationName, - EnvironmentName: tt.fields.EnvironmentName, - ProjectIntegrationName: tt.fields.ProjectIntegrationName, - PipelineHooks: tt.fields.PipelineHooks, - Permissions: tt.fields.Permissions, + w := v2.Workflow{ + Name: tt.fields.Name, + Description: tt.fields.Description, + Version: tt.fields.Version, + Workflow: tt.fields.Workflow, + Hooks: tt.fields.Hooks, + Permissions: tt.fields.Permissions, } if err := w.CheckDependencies(); (err != nil) != tt.wantErr { t.Errorf("Workflow.checkDependencies() error = %v, wantErr %v", err, tt.wantErr) @@ -109,46 +93,23 @@ func TestWorkflow_checkDependencies(t *testing.T) { func TestWorkflow_checkValidity(t *testing.T) { type fields struct { - Name string - Version string - Workflow map[string]exportentities.NodeEntry - Hooks map[string][]exportentities.HookEntry - DependsOn []string - Conditions *exportentities.ConditionEntry - When []string - PipelineName string - ApplicationName string - EnvironmentName string - ProjectIntegrationName string - PipelineHooks []exportentities.HookEntry - Permissions map[string]int - OneAtATime *bool + Name string + Version string + Workflow map[string]v2.NodeEntry + Hooks map[string][]v2.HookEntry + Permissions map[string]int } tests := []struct { name string fields fields wantErr bool }{ - { - name: "Should raise an error", - fields: fields{ - PipelineName: "pipeline", - Workflow: map[string]exportentities.NodeEntry{ - "root": { - PipelineName: "pipeline", - }, - "child": { - PipelineName: "pipeline", - DependsOn: []string{"root"}, - }, - }, - }, - wantErr: true, - }, { name: "Should not raise an error", fields: fields{ - Workflow: map[string]exportentities.NodeEntry{ + Name: "myWorkflow", + Version: exportentities.WorkflowVersion2, + Workflow: map[string]v2.NodeEntry{ "root": { PipelineName: "pipeline", }, @@ -160,30 +121,15 @@ func TestWorkflow_checkValidity(t *testing.T) { }, wantErr: false, }, - { - name: "Too simple to raise an error", - fields: fields{ - PipelineName: "pipeline", - }, - wantErr: false, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - w := exportentities.Workflow{ - Name: tt.fields.Name, - Version: tt.fields.Version, - Workflow: tt.fields.Workflow, - Hooks: tt.fields.Hooks, - Conditions: tt.fields.Conditions, - When: tt.fields.When, - PipelineName: tt.fields.PipelineName, - ApplicationName: tt.fields.ApplicationName, - EnvironmentName: tt.fields.EnvironmentName, - ProjectIntegrationName: tt.fields.ProjectIntegrationName, - PipelineHooks: tt.fields.PipelineHooks, - Permissions: tt.fields.Permissions, - OneAtATime: tt.fields.OneAtATime, + w := v2.Workflow{ + Name: tt.fields.Name, + Version: tt.fields.Version, + Workflow: tt.fields.Workflow, + Hooks: tt.fields.Hooks, + Permissions: tt.fields.Permissions, } if err := w.CheckValidity(); (err != nil) != tt.wantErr { t.Errorf("Workflow.checkValidity() error = %v, wantErr %v", err, tt.wantErr) @@ -193,24 +139,15 @@ func TestWorkflow_checkValidity(t *testing.T) { } func TestWorkflow_GetWorkflow(t *testing.T) { - strue := true + true := true type fields struct { - Name string - Description string - Version string - Workflow map[string]exportentities.NodeEntry - Hooks map[string][]exportentities.HookEntry - DependsOn []string - Conditions *exportentities.ConditionEntry - When []string - PipelineName string - ApplicationName string - EnvironmentName string - ProjectIntegrationName string - PipelineHooks []exportentities.HookEntry - Permissions map[string]int - HistoryLength int64 - OneAtATime *bool + Name string + Description string + Version string + Workflow map[string]v2.NodeEntry + Hooks map[string][]v2.HookEntry + Permissions map[string]int + HistoryLength int64 } tsts := []struct { name string @@ -218,98 +155,24 @@ func TestWorkflow_GetWorkflow(t *testing.T) { want sdk.Workflow wantErr bool }{ - // pipeline - { - name: "Simple workflow with mutex should not raise an error", - fields: fields{ - PipelineName: "pipeline", - OneAtATime: &strue, - }, - wantErr: false, - want: sdk.Workflow{ - HistoryLength: sdk.DefaultHistoryLength, - WorkflowData: &sdk.WorkflowData{ - Node: sdk.Node{ - Name: "pipeline", - Type: "pipeline", - Context: &sdk.NodeContext{ - PipelineName: "pipeline", - Mutex: true, - }, - }, - }, - }, - }, - // pipeline - { - name: "Simple workflow should not raise an error", - fields: fields{ - PipelineName: "pipeline", - Description: "this is my description", - PipelineHooks: []exportentities.HookEntry{ - { - Model: "Scheduler", - Config: map[string]string{ - "crontab": "* * * * *", - "payload": "{}", - }, - Conditions: &sdk.WorkflowNodeConditions{ - LuaScript: "return true", - }, - }, - }, - }, - wantErr: false, - want: sdk.Workflow{ - HistoryLength: sdk.DefaultHistoryLength, - Description: "this is my description", - WorkflowData: &sdk.WorkflowData{ - Node: sdk.Node{ - Name: "pipeline", - Type: "pipeline", - Context: &sdk.NodeContext{ - PipelineName: "pipeline", - }, - Hooks: []sdk.NodeHook{ - { - HookModelName: "Scheduler", - Conditions: sdk.WorkflowNodeConditions{ - LuaScript: "return true", - }, - Config: sdk.WorkflowNodeHookConfig{ - "crontab": sdk.WorkflowNodeHookConfigValue{ - Value: "* * * * *", - Configurable: true, - Type: sdk.HookConfigTypeString, - }, - "payload": sdk.WorkflowNodeHookConfigValue{ - Value: "{}", - Configurable: true, - Type: sdk.HookConfigTypeString, - }, - }, - }, - }, - }, - }, - }, - }, // hook conditions { name: "Workflow with multiple nodes should display hook conditions", fields: fields{ - Workflow: map[string]exportentities.NodeEntry{ + Name: "myWorkflow", + Version: exportentities.WorkflowVersion2, + Workflow: map[string]v2.NodeEntry{ "root": { PipelineName: "pipeline-root", }, "child": { PipelineName: "pipeline-child", DependsOn: []string{"root"}, - OneAtATime: &exportentities.True, + OneAtATime: &v2.True, }, }, - Hooks: map[string][]exportentities.HookEntry{ - "root": []exportentities.HookEntry{{ + Hooks: map[string][]v2.HookEntry{ + "root": {{ Model: "Scheduler", Config: map[string]string{ "crontab": "* * * * *", @@ -323,6 +186,7 @@ func TestWorkflow_GetWorkflow(t *testing.T) { }, wantErr: false, want: sdk.Workflow{ + Name: "myWorkflow", HistoryLength: sdk.DefaultHistoryLength, WorkflowData: &sdk.WorkflowData{ Node: sdk.Node{ @@ -372,19 +236,22 @@ func TestWorkflow_GetWorkflow(t *testing.T) { { name: "Complexe workflow without joins and mutex should not raise an error", fields: fields{ - Workflow: map[string]exportentities.NodeEntry{ + Name: "myWorkflow", + Version: exportentities.WorkflowVersion2, + Workflow: map[string]v2.NodeEntry{ "root": { PipelineName: "pipeline-root", }, "child": { PipelineName: "pipeline-child", DependsOn: []string{"root"}, - OneAtATime: &exportentities.True, + OneAtATime: &v2.True, }, }, }, wantErr: false, want: sdk.Workflow{ + Name: "myWorkflow", HistoryLength: sdk.DefaultHistoryLength, WorkflowData: &sdk.WorkflowData{ Node: sdk.Node{ @@ -414,7 +281,9 @@ func TestWorkflow_GetWorkflow(t *testing.T) { { name: "Complexe workflow without joins with a default payload on a non root node should raise an error", fields: fields{ - Workflow: map[string]exportentities.NodeEntry{ + Name: "myWorkflow", + Version: exportentities.WorkflowVersion2, + Workflow: map[string]v2.NodeEntry{ "root": { PipelineName: "pipeline-root", }, @@ -424,7 +293,7 @@ func TestWorkflow_GetWorkflow(t *testing.T) { Payload: map[string]interface{}{ "test": "content", }, - OneAtATime: &exportentities.True, + OneAtATime: &v2.True, }, }, }, @@ -434,7 +303,9 @@ func TestWorkflow_GetWorkflow(t *testing.T) { { name: "Complexe workflow unordered without joins should not raise an error", fields: fields{ - Workflow: map[string]exportentities.NodeEntry{ + Name: "myWorkflow", + Version: exportentities.WorkflowVersion2, + Workflow: map[string]v2.NodeEntry{ "child": { PipelineName: "pipeline-child", DependsOn: []string{"root"}, @@ -447,6 +318,7 @@ func TestWorkflow_GetWorkflow(t *testing.T) { }, wantErr: false, want: sdk.Workflow{ + Name: "myWorkflow", HistoryLength: 25, WorkflowData: &sdk.WorkflowData{ Node: sdk.Node{ @@ -476,7 +348,9 @@ func TestWorkflow_GetWorkflow(t *testing.T) { { name: "Complexe workflow without joins should not raise an error", fields: fields{ - Workflow: map[string]exportentities.NodeEntry{ + Name: "myWorkflow", + Version: exportentities.WorkflowVersion2, + Workflow: map[string]v2.NodeEntry{ "root": { PipelineName: "pipeline-root", }, @@ -492,6 +366,7 @@ func TestWorkflow_GetWorkflow(t *testing.T) { }, wantErr: false, want: sdk.Workflow{ + Name: "myWorkflow", HistoryLength: sdk.DefaultHistoryLength, WorkflowData: &sdk.WorkflowData{ Node: sdk.Node{ @@ -536,7 +411,9 @@ func TestWorkflow_GetWorkflow(t *testing.T) { { name: "Complexe workflow with joins should not raise an error", fields: fields{ - Workflow: map[string]exportentities.NodeEntry{ + Name: "myWorkflow", + Version: exportentities.WorkflowVersion2, + Workflow: map[string]v2.NodeEntry{ "A": { PipelineName: "pipeline", }, @@ -565,7 +442,7 @@ func TestWorkflow_GetWorkflow(t *testing.T) { DependsOn: []string{"D", "E"}, }, }, - Hooks: map[string][]exportentities.HookEntry{ + Hooks: map[string][]v2.HookEntry{ "A": { { Model: "Scheduler", @@ -579,6 +456,7 @@ func TestWorkflow_GetWorkflow(t *testing.T) { }, wantErr: false, want: sdk.Workflow{ + Name: "myWorkflow", HistoryLength: sdk.DefaultHistoryLength, WorkflowData: &sdk.WorkflowData{ Node: sdk.Node{ @@ -699,32 +577,12 @@ func TestWorkflow_GetWorkflow(t *testing.T) { }, }, }, - { - name: "Complex workflow with integration should not raise an error", - fields: fields{ - PipelineName: "pipeline", - ProjectIntegrationName: "integration", - }, - wantErr: false, - want: sdk.Workflow{ - HistoryLength: sdk.DefaultHistoryLength, - WorkflowData: &sdk.WorkflowData{ - Node: sdk.Node{ - Name: "pipeline", - Ref: "pipeline", - Type: "pipeline", - Context: &sdk.NodeContext{ - PipelineName: "pipeline", - ProjectIntegrationName: "integration", - }, - }, - }, - }, - }, { name: "Root and a outgoing hook should not raise an error", fields: fields{ - Workflow: map[string]exportentities.NodeEntry{ + Name: "myWorkflow", + Version: exportentities.WorkflowVersion2, + Workflow: map[string]v2.NodeEntry{ "A": { PipelineName: "pipeline", }, @@ -739,6 +597,7 @@ func TestWorkflow_GetWorkflow(t *testing.T) { }, wantErr: false, want: sdk.Workflow{ + Name: "myWorkflow", HistoryLength: sdk.DefaultHistoryLength, WorkflowData: &sdk.WorkflowData{ Node: sdk.Node{ @@ -773,24 +632,16 @@ func TestWorkflow_GetWorkflow(t *testing.T) { for _, tt := range tsts { t.Run(tt.name, func(t *testing.T) { - w := exportentities.Workflow{ - Name: tt.fields.Name, - Description: tt.fields.Description, - Version: tt.fields.Version, - Workflow: tt.fields.Workflow, - Hooks: tt.fields.Hooks, - Conditions: tt.fields.Conditions, - When: tt.fields.When, - PipelineName: tt.fields.PipelineName, - ApplicationName: tt.fields.ApplicationName, - EnvironmentName: tt.fields.EnvironmentName, - ProjectIntegrationName: tt.fields.ProjectIntegrationName, - PipelineHooks: tt.fields.PipelineHooks, - Permissions: tt.fields.Permissions, - HistoryLength: &tt.fields.HistoryLength, - OneAtATime: tt.fields.OneAtATime, + w := v2.Workflow{ + Name: tt.fields.Name, + Description: tt.fields.Description, + Version: tt.fields.Version, + Workflow: tt.fields.Workflow, + Hooks: tt.fields.Hooks, + Permissions: tt.fields.Permissions, + HistoryLength: &tt.fields.HistoryLength, } - got, err := w.GetWorkflow() + got, err := exportentities.ParseWorkflow(w) if (err != nil) != tt.wantErr { t.Errorf("Workflow.GetWorkflow() error = %v, wantErr %v", err, tt.wantErr) return @@ -845,9 +696,14 @@ func TestFromYAMLToYAML(t *testing.T) { { name: "1_start -> 2_webhook -> 3_after_webhook -> 4_fork_before_end -> 5_end", yaml: `name: test1 -version: v1.0 +version: v2.0 workflow: 1_start: + conditions: + check: + - variable: git.branch + operator: eq + value: master pipeline: test 2_webHook: depends_on: @@ -876,7 +732,7 @@ workflow: }, { name: "test with outgoing hooks", yaml: `name: DDOS -version: v1.0 +version: v2.0 workflow: 1_test-outgoing-hooks: pipeline: DDOS-me @@ -916,7 +772,7 @@ metadata: }, { name: "tests with outgoing hooks with a join", yaml: `name: DDOS -version: v1.0 +version: v2.0 workflow: 1_test-outgoing-hooks: pipeline: DDOS-me @@ -963,7 +819,7 @@ metadata: }, { name: "test with outgoing hooks, a join, and a fork", yaml: `name: DDOS -version: v1.0 +version: v2.0 workflow: 1_test-outgoing-hooks: pipeline: DDOS-me @@ -1021,29 +877,11 @@ workflow: - 4_end metadata: default_tags: git.branch,git.author -`, - }, { - name: "simple pipeline triggered by a webhook", - yaml: `name: test4 -version: v1.0 -pipeline: DDOS-me -application: test1 -pipeline_hooks: -- type: WebHook - config: - method: POST - conditions: - check: - - variable: lolilol - operator: eq - value: coucou -metadata: - default_tags: git.branch,git.author `, }, { name: "pipeline with two hooks", yaml: `name: test3 -version: v1.0 +version: v2.0 workflow: 1_start: pipeline: test @@ -1080,7 +918,7 @@ hooks: }, { name: "workflow with template", yaml: `name: test4 -version: v1.0 +version: v2.0 template: shared.infra/example workflow: 1_start: @@ -1094,7 +932,7 @@ workflow: { name: "Join with condition", yaml: `name: joins -version: v1.0 +version: v2.0 workflow: aa: pipeline: aa @@ -1133,20 +971,12 @@ workflow: - aa_2 when: - manual -`, - }, - { - name: "Workflow with mutex on root", - yaml: `name: mymutex -version: v1.0 -one_at_a_time: true -pipeline: env `, }, { name: "Workflow with mutex pipeline child", yaml: `name: mymutex -version: v1.0 +version: v2.0 workflow: env: pipeline: env @@ -1163,8 +993,7 @@ workflow: } for _, tst := range tests { t.Run(tst.name, func(t *testing.T) { - var yamlWorkflow exportentities.Workflow - err := exportentities.Unmarshal([]byte(tst.yaml), exportentities.FormatYAML, &yamlWorkflow) + yamlWorkflow, err := exportentities.UnmarshalWorkflow([]byte(tst.yaml)) if err != nil { if !tst.wantErr { t.Error("Unmarshal raised an error", err) @@ -1175,7 +1004,7 @@ workflow: t.Error("Unmarshal should return an error but it doesn't") return } - w, err := yamlWorkflow.GetWorkflow() + w, err := exportentities.ParseWorkflow(yamlWorkflow) if err != nil { if !tst.wantErr { t.Error("GetWorkflow raised an error", err) diff --git a/sdk/exportentities/workflow.go b/sdk/exportentities/workflow.go index 30d13da7af..60226022cd 100644 --- a/sdk/exportentities/workflow.go +++ b/sdk/exportentities/workflow.go @@ -3,14 +3,12 @@ package exportentities import ( "context" "fmt" - "math/rand" - "sort" - "strings" - "time" - "github.com/fsamin/go-dump" + "gopkg.in/yaml.v2" "github.com/ovh/cds/sdk" + "github.com/ovh/cds/sdk/exportentities/v1" + "github.com/ovh/cds/sdk/exportentities/v2" ) // Name pattern for pull files. @@ -21,35 +19,6 @@ const ( PullEnvironmentName = "%s.env.yml" ) -// Workflow is the "as code" representation of a sdk.Workflow -type Workflow struct { - Name string `json:"name" yaml:"name" jsonschema_description:"The name of the workflow."` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Version string `json:"version,omitempty" yaml:"version,omitempty" jsonschema_description:"Version for the yaml syntax, latest is v1.0."` - Template *string `json:"template,omitempty" yaml:"template,omitempty" jsonschema_description:"Optional path of the template used to generate the workflow."` - // this will be filled for complex workflows - Workflow map[string]NodeEntry `json:"workflow,omitempty" yaml:"workflow,omitempty" jsonschema_description:"Workflow nodes list."` - Hooks map[string][]HookEntry `json:"hooks,omitempty" yaml:"hooks,omitempty" jsonschema_description:"Workflow hooks list."` - // this will be filled for simple workflows - OneAtATime *bool `json:"one_at_a_time,omitempty" yaml:"one_at_a_time,omitempty" jsonschema_description:"Set to true if you want to limit the execution of this node to one at a time."` - Conditions *ConditionEntry `json:"conditions,omitempty" yaml:"conditions,omitempty" jsonschema_description:"Conditions to run this node.\nhttps://ovh.github.io/cds/docs/concepts/workflow/run-conditions."` - When []string `json:"when,omitempty" yaml:"when,omitempty" jsonschema_description:"Set manual and status condition (ex: 'success')."` //This is used only for manual and success condition - PipelineName string `json:"pipeline,omitempty" yaml:"pipeline,omitempty" jsonschema_description:"The name of a pipeline used for pipeline node."` - Payload map[string]interface{} `json:"payload,omitempty" yaml:"payload,omitempty"` - Parameters map[string]string `json:"parameters,omitempty" yaml:"parameters,omitempty" jsonschema_description:"List of parameters for the workflow."` - ApplicationName string `json:"application,omitempty" yaml:"application,omitempty" jsonschema_description:"The application to use in the context of the node.\nhttps://ovh.github.io/cds/docs/concepts/workflow/pipeline-context"` - EnvironmentName string `json:"environment,omitempty" yaml:"environment,omitempty" jsonschema_description:"The environment to use in the context of the node.\nhttps://ovh.github.io/cds/docs/concepts/workflow/pipeline-context"` - ProjectIntegrationName string `json:"integration,omitempty" yaml:"integration,omitempty" jsonschema_description:"The integration to use in the context of the node.\nhttps://ovh.github.io/cds/docs/concepts/workflow/pipeline-context"` - PipelineHooks []HookEntry `json:"pipeline_hooks,omitempty" yaml:"pipeline_hooks,omitempty"` - // extra workflow data - Permissions map[string]int `json:"permissions,omitempty" yaml:"permissions,omitempty" jsonschema_description:"The permissions for the workflow (ex: myGroup: 7).\nhttps://ovh.github.io/cds/docs/concepts/permissions"` - Metadata map[string]string `json:"metadata,omitempty" yaml:"metadata,omitempty"` - PurgeTags []string `json:"purge_tags,omitempty" yaml:"purge_tags,omitempty"` - Notifications []NotificationEntry `json:"notify,omitempty" yaml:"notify,omitempty"` // This is used when the workflow have only one pipeline - HistoryLength *int64 `json:"history_length,omitempty" yaml:"history_length,omitempty"` - MapNotifications map[string][]NotificationEntry `json:"notifications,omitempty" yaml:"notifications,omitempty"` // This is used when the workflow have more than one pipeline -} - // WorkflowPulled contains all the yaml base64 that are needed to generate a workflow tar file. type WorkflowPulled struct { Workflow WorkflowPulledItem `json:"workflow"` @@ -64,804 +33,95 @@ type WorkflowPulledItem struct { Value string `json:"value"` } -// NodeEntry represents a node as code -type NodeEntry struct { - ID int64 `json:"-" yaml:"-"` - DependsOn []string `json:"depends_on,omitempty" yaml:"depends_on,omitempty" jsonschema_description:"Names of the parent nodes, can be pipelines, forks or joins."` - Conditions *ConditionEntry `json:"conditions,omitempty" yaml:"conditions,omitempty" jsonschema_description:"Conditions to run this node.\nhttps://ovh.github.io/cds/docs/concepts/workflow/run-conditions."` - When []string `json:"when,omitempty" yaml:"when,omitempty" jsonschema_description:"Set manual and status condition (ex: 'success')."` //This is used only for manual and success condition - PipelineName string `json:"pipeline,omitempty" yaml:"pipeline,omitempty" jsonschema_description:"The name of a pipeline used for pipeline node."` - ApplicationName string `json:"application,omitempty" yaml:"application,omitempty" jsonschema_description:"The application to use in the context of the node.\nhttps://ovh.github.io/cds/docs/concepts/workflow/pipeline-context"` - EnvironmentName string `json:"environment,omitempty" yaml:"environment,omitempty" jsonschema_description:"The environment to use in the context of the node.\nhttps://ovh.github.io/cds/docs/concepts/workflow/pipeline-context"` - ProjectIntegrationName string `json:"integration,omitempty" yaml:"integration,omitempty" jsonschema_description:"The integration to use in the context of the node.\nhttps://ovh.github.io/cds/docs/concepts/workflow/pipeline-context"` - OneAtATime *bool `json:"one_at_a_time,omitempty" yaml:"one_at_a_time,omitempty" jsonschema_description:"Set to true if you want to limit the execution of this node to one at a time."` - Payload map[string]interface{} `json:"payload,omitempty" yaml:"payload,omitempty"` - Parameters map[string]string `json:"parameters,omitempty" yaml:"parameters,omitempty" jsonschema_description:"List of parameters for the workflow."` - OutgoingHookModelName string `json:"trigger,omitempty" yaml:"trigger,omitempty"` - OutgoingHookConfig map[string]string `json:"config,omitempty" yaml:"config,omitempty"` - Permissions map[string]int `json:"permissions,omitempty" yaml:"permissions,omitempty" jsonschema_description:"The permissions for the node (ex: myGroup: 7).\nhttps://ovh.github.io/cds/docs/concepts/permissions"` -} - -type ConditionEntry struct { - PlainConditions []PlainConditionEntry `json:"plain,omitempty" yaml:"check,omitempty"` - LuaScript string `json:"script,omitempty" yaml:"script,omitempty"` +type Workflow interface { + GetName() string + GetVersion() string } -//WorkflowNodeCondition represents a condition to trigger ot not a pipeline in a workflow. Operator can be =, !=, regex -type PlainConditionEntry struct { - Variable string `json:"variable" yaml:"variable"` - Operator string `json:"operator" yaml:"operator"` - Value string `json:"value" yaml:"value"` -} - -// HookEntry represents a hook as code -type HookEntry struct { - Model string `json:"type,omitempty" yaml:"type,omitempty" jsonschema_description:"Model of the hook.\nhttps://ovh.github.io/cds/docs/concepts/workflow/hooks"` - Config map[string]string `json:"config,omitempty" yaml:"config,omitempty"` - Conditions *sdk.WorkflowNodeConditions `json:"conditions,omitempty" yaml:"conditions,omitempty" jsonschema_description:"Conditions to run this hook.\nhttps://ovh.github.io/cds/docs/concepts/workflow/run-conditions."` -} - -// WorkflowVersion is the type for the version -type WorkflowVersion string - -// There are the supported versions const ( WorkflowVersion1 = "v1.0" + WorkflowVersion2 = "v2.0" ) -func craftNodeEntry(w sdk.Workflow, n sdk.Node) (NodeEntry, error) { - entry := NodeEntry{} - - ancestors := []string{} - - if n.Type != sdk.NodeTypeJoin { - nodes := w.WorkflowData.Array() - for _, node := range nodes { - if n.Name == node.Name { - continue - } - for _, t := range node.Triggers { - if t.ChildNode.Name == n.Name { - if node.Type == sdk.NodeTypeJoin && !joinAsNode(node) { - for _, jp := range node.JoinContext { - parentNode := w.WorkflowData.NodeByRef(jp.ParentName) - if parentNode == nil { - return entry, sdk.WithStack(sdk.ErrWorkflowNodeNotFound) - } - ancestors = append(ancestors, parentNode.Name) - } - } else { - ancestors = append(ancestors, node.Name) - } - } - } - } - } else { - for _, jc := range n.JoinContext { - ancestors = append(ancestors, jc.ParentName) - } - } - - sort.Strings(ancestors) - entry.DependsOn = ancestors - - if n.Context != nil && n.Context.PipelineName != "" { - entry.PipelineName = n.Context.PipelineName - } - - if n.Context != nil { - conditions := []sdk.WorkflowNodeCondition{} - for _, c := range n.Context.Conditions.PlainConditions { - if c.Operator == sdk.WorkflowConditionsOperatorEquals && - c.Value == sdk.StatusSuccess && - c.Variable == "cds.status" { - entry.When = append(entry.When, "success") - } else if c.Operator == sdk.WorkflowConditionsOperatorEquals && - c.Value == "true" && - c.Variable == "cds.manual" { - entry.When = append(entry.When, "manual") - } else { - conditions = append(conditions, c) - } - } - - if len(conditions) > 0 || n.Context.Conditions.LuaScript != "" { - entry.Conditions = &ConditionEntry{ - PlainConditions: make([]PlainConditionEntry, 0, len(conditions)), - LuaScript: n.Context.Conditions.LuaScript, - } - for _, c := range conditions { - entry.Conditions.PlainConditions = append(entry.Conditions.PlainConditions, PlainConditionEntry{ - Value: c.Value, - Operator: c.Operator, - Variable: c.Variable, - }) - } - } - - if n.Context.ApplicationName != "" { - entry.ApplicationName = n.Context.ApplicationName - } - if n.Context.EnvironmentName != "" { - entry.EnvironmentName = n.Context.EnvironmentName - } - if n.Context.ProjectIntegrationName != "" { - entry.ProjectIntegrationName = n.Context.ProjectIntegrationName - } - - if n.Context.Mutex { - entry.OneAtATime = &n.Context.Mutex - } - - if n.Context.HasDefaultPayload() { - enc := dump.NewDefaultEncoder() - enc.ExtraFields.DetailedMap = false - enc.ExtraFields.DetailedStruct = false - enc.ExtraFields.Len = false - enc.ExtraFields.Type = false - enc.Formatters = nil - m, err := enc.ToMap(n.Context.DefaultPayload) - if err != nil { - return entry, sdk.WrapError(err, "Unable to encode payload") - } - entry.Payload = m - } - - if len(n.Context.DefaultPipelineParameters) > 0 { - entry.Parameters = sdk.ParametersToMap(n.Context.DefaultPipelineParameters) - } - } - - if len(n.Groups) > 0 { - entry.Permissions = map[string]int{} - for _, gr := range n.Groups { - entry.Permissions[gr.Group.Name] = gr.Permission - } - } - - if n.OutGoingHookContext != nil { - entry.OutgoingHookModelName = n.OutGoingHookContext.HookModelName - - m := sdk.GetBuiltinOutgoingHookModelByName(entry.OutgoingHookModelName) - if m == nil { - return entry, sdk.WrapError(sdk.ErrNotFound, "unable to find outgoing hook model %s", entry.OutgoingHookModelName) - } - entry.OutgoingHookConfig = n.OutGoingHookContext.Config.Values(m.DefaultConfig) - } - - return entry, nil -} - -// WorkflowOptions is the type for several workflow-as-code options -type WorkflowOptions func(sdk.Workflow, *Workflow) error - -// WorkflowWithPermissions export workflow with permissions -func WorkflowWithPermissions(w sdk.Workflow, exportedWorkflow *Workflow) error { - exportedWorkflow.Permissions = make(map[string]int, len(w.Groups)) - for _, p := range w.Groups { - exportedWorkflow.Permissions[p.Group.Name] = p.Permission - } - - for _, node := range w.WorkflowData.Array() { - entries := exportedWorkflow.Entries() - if len(entries) > 1 { // Else the permissions are the same than the workflow - for exportedNodeName, entry := range entries { - if entry.Permissions == nil { - entry.Permissions = map[string]int{} - } - if node.Name == exportedNodeName { - for _, p := range node.Groups { - entry.Permissions[p.Group.Name] = p.Permission - } - exportedWorkflow.Workflow[exportedNodeName] = entry - } - } - } - } - - return nil -} - -// WorkflowSkipIfOnlyOneRepoWebhook skips the repo webhook if it's the only one -// It also won't export the default payload -func WorkflowSkipIfOnlyOneRepoWebhook(w sdk.Workflow, exportedWorkflow *Workflow) error { - if len(exportedWorkflow.Workflow) == 0 { - if len(exportedWorkflow.PipelineHooks) == 1 && exportedWorkflow.PipelineHooks[0].Model == sdk.RepositoryWebHookModelName { - exportedWorkflow.PipelineHooks = nil - exportedWorkflow.Payload = nil - - } - return nil - } - - for nodeName, hs := range exportedWorkflow.Hooks { - if nodeName == w.WorkflowData.Node.Name && len(hs) == 1 { - if hs[0].Model == sdk.RepositoryWebHookModelName { - delete(exportedWorkflow.Hooks, nodeName) - if exportedWorkflow.Workflow != nil { - for nodeName := range exportedWorkflow.Workflow { - if nodeName == w.WorkflowData.Node.Name { - entry := exportedWorkflow.Workflow[nodeName] - entry.Payload = nil - exportedWorkflow.Workflow[nodeName] = entry - break - } - } - } - break - } - } - } - - return nil -} - -func joinAsNode(n *sdk.Node) bool { - return n.Context != nil && (n.Context.Conditions.LuaScript != "" || len(n.Context.Conditions.PlainConditions) > 0) -} - -//NewWorkflow creates a new exportable workflow -func NewWorkflow(ctx context.Context, w sdk.Workflow, opts ...WorkflowOptions) (Workflow, error) { - exportedWorkflow := Workflow{} - exportedWorkflow.Name = w.Name - exportedWorkflow.Description = w.Description - exportedWorkflow.Version = WorkflowVersion1 - exportedWorkflow.Workflow = map[string]NodeEntry{} - exportedWorkflow.Hooks = map[string][]HookEntry{} - if len(w.Metadata) > 0 { - exportedWorkflow.Metadata = make(map[string]string, len(w.Metadata)) - for k, v := range w.Metadata { - // don't export empty metadata - if v != "" { - exportedWorkflow.Metadata[k] = v - } - } - } - - if w.HistoryLength > 0 && w.HistoryLength != sdk.DefaultHistoryLength { - exportedWorkflow.HistoryLength = &w.HistoryLength - } - - exportedWorkflow.PurgeTags = w.PurgeTags - - nodes := w.WorkflowData.Array() - - if len(nodes) == 1 { - n := w.WorkflowData.Node - entry, err := craftNodeEntry(w, n) - if err != nil { - return exportedWorkflow, err - } - exportedWorkflow.ApplicationName = entry.ApplicationName - exportedWorkflow.PipelineName = entry.PipelineName - exportedWorkflow.EnvironmentName = entry.EnvironmentName - exportedWorkflow.ProjectIntegrationName = entry.ProjectIntegrationName - exportedWorkflow.OneAtATime = entry.OneAtATime - if entry.Conditions != nil && (len(entry.Conditions.PlainConditions) > 0 || entry.Conditions.LuaScript != "") { - exportedWorkflow.When = entry.When - exportedWorkflow.Conditions = entry.Conditions - } - for _, h := range n.Hooks { - if exportedWorkflow.Hooks == nil { - exportedWorkflow.Hooks = make(map[string][]HookEntry) - } - - m := sdk.GetBuiltinHookModelByName(h.HookModelName) - if m == nil { - return exportedWorkflow, sdk.WrapError(sdk.ErrNotFound, "unable to find hook model %s", h.HookModelName) - } - - pipHook := HookEntry{ - Model: h.HookModelName, - Config: h.Config.Values(m.DefaultConfig), - Conditions: &h.Conditions, - } - - if h.Conditions.LuaScript == "" && len(h.Conditions.PlainConditions) == 0 { - pipHook.Conditions = nil - } - - exportedWorkflow.PipelineHooks = append(exportedWorkflow.PipelineHooks, pipHook) - } - exportedWorkflow.Payload = entry.Payload - exportedWorkflow.Parameters = entry.Parameters - } else { - for _, n := range nodes { - if n.Type == sdk.NodeTypeJoin && !joinAsNode(n) { - continue - } - - entry, err := craftNodeEntry(w, *n) - if err != nil { - return exportedWorkflow, sdk.WrapError(err, "Unable to craft Node entry %s", n.Name) - } - exportedWorkflow.Workflow[n.Name] = entry - - for _, h := range n.Hooks { - if exportedWorkflow.Hooks == nil { - exportedWorkflow.Hooks = make(map[string][]HookEntry) - } - - m := sdk.GetBuiltinHookModelByName(h.HookModelName) - if m == nil { - return exportedWorkflow, sdk.WrapError(sdk.ErrNotFound, "unable to find hook model %s", h.HookModelName) - } - pipHook := HookEntry{ - Model: h.HookModelName, - Config: h.Config.Values(m.DefaultConfig), - Conditions: &h.Conditions, - } - - if h.Conditions.LuaScript == "" && len(h.Conditions.PlainConditions) == 0 { - pipHook.Conditions = nil - } - - exportedWorkflow.Hooks[n.Name] = append(exportedWorkflow.Hooks[n.Name], pipHook) - } - } - } - - //Notifications - if err := craftNotifications(ctx, w, &exportedWorkflow); err != nil { - return exportedWorkflow, err - } - - for _, f := range opts { - if err := f(w, &exportedWorkflow); err != nil { - return exportedWorkflow, sdk.WrapError(err, "Unable to run function") - } - } - - if w.Template != nil { - path := fmt.Sprintf("%s/%s", w.Template.Group.Name, w.Template.Slug) - exportedWorkflow.Template = &path - } - - return exportedWorkflow, nil -} - -// Entries returns the map of all workflow entries -func (w Workflow) Entries() map[string]NodeEntry { - if len(w.Workflow) != 0 { - return w.Workflow - } - - singleEntry := NodeEntry{ - ApplicationName: w.ApplicationName, - EnvironmentName: w.EnvironmentName, - ProjectIntegrationName: w.ProjectIntegrationName, - PipelineName: w.PipelineName, - Conditions: w.Conditions, - When: w.When, - Payload: w.Payload, - Parameters: w.Parameters, - OneAtATime: w.OneAtATime, - } - return map[string]NodeEntry{ - w.PipelineName: singleEntry, - } +type WorkflowVersion struct { + Version string `yaml:"version"` } -func (w Workflow) CheckValidity() error { - mError := new(sdk.MultiError) - - if len(w.Workflow) != 0 { - if w.ApplicationName != "" { - mError.Append(fmt.Errorf("Error: wrong usage: application %s not allowed here", w.ApplicationName)) - } - if w.EnvironmentName != "" { - mError.Append(fmt.Errorf("Error: wrong usage: environment %s not allowed here", w.EnvironmentName)) - } - if w.ProjectIntegrationName != "" { - mError.Append(fmt.Errorf("Error: wrong usage: integration %s not allowed here", w.ProjectIntegrationName)) - } - if w.PipelineName != "" { - mError.Append(fmt.Errorf("Error: wrong usage: pipeline %s not allowed here", w.PipelineName)) - } - if w.Conditions != nil { - mError.Append(fmt.Errorf("Error: wrong usage: conditions not allowed here")) - } - if len(w.When) != 0 { - mError.Append(fmt.Errorf("Error: wrong usage: when not allowed here")) - } - if len(w.PipelineHooks) != 0 { - mError.Append(fmt.Errorf("Error: wrong usage: pipeline_hooks not allowed here")) - } - } else { - if len(w.Hooks) > 0 { - mError.Append(fmt.Errorf("Error: wrong usage: hooks not allowed here")) - } - } - - for name := range w.Hooks { - if _, ok := w.Workflow[name]; !ok { - mError.Append(fmt.Errorf("Error: wrong usage: invalid hook on %s", name)) - } - } - - //Checks map notifications validity - mError.Append(CheckWorkflowNotificationsValidity(w)) - - if mError.IsEmpty() { - return nil +func UnmarshalWorkflow(body []byte) (Workflow, error) { + var workflowVersion WorkflowVersion + if err := yaml.Unmarshal(body, &workflowVersion); err != nil { + return nil, sdk.WrapError(sdk.ErrWrongRequest, "invalid workflow data: %v", err) } - return mError -} - -func (w Workflow) CheckDependencies() error { - mError := new(sdk.MultiError) - for s, e := range w.Entries() { - if err := e.checkDependencies(s, w); err != nil { - mError.Append(fmt.Errorf("Error: %s invalid: %v", s, err)) + switch workflowVersion.Version { + case WorkflowVersion1: + var workflowV1 v1.Workflow + if err := yaml.Unmarshal(body, &workflowV1); err != nil { + return nil, sdk.WrapError(sdk.ErrWrongRequest, "invalid workflow v1 format: %v", err) } - } - - if mError.IsEmpty() { - return nil - } - return mError -} - -func (e NodeEntry) checkDependencies(nodeName string, w Workflow) error { - mError := new(sdk.MultiError) -nextDep: - for _, d := range e.DependsOn { - for s := range w.Workflow { - if s == d { - continue nextDep - } + return workflowV1, nil + case WorkflowVersion2: + var workflowV2 v2.Workflow + if err := yaml.Unmarshal(body, &workflowV2); err != nil { + return nil, sdk.WrapError(sdk.ErrWrongRequest, "invalid workflow v2 format: %v", err) } - mError.Append(fmt.Errorf("the pipeline %s depends on an unknown pipeline: %s", nodeName, d)) - } - if mError.IsEmpty() { - return nil + return workflowV2, nil } - return mError + return nil, sdk.WrapError(sdk.ErrWrongRequest, "invalid workflow version: %s", workflowVersion.Version) } -// GetWorkflow returns a fresh sdk.Workflow -func (w Workflow) GetWorkflow() (*sdk.Workflow, error) { - var wf = new(sdk.Workflow) - wf.Name = w.Name - wf.Description = w.Description - wf.WorkflowData = &sdk.WorkflowData{} - // Init map - wf.Applications = make(map[int64]sdk.Application) - wf.Pipelines = make(map[int64]sdk.Pipeline) - wf.Environments = make(map[int64]sdk.Environment) - wf.ProjectIntegrations = make(map[int64]sdk.ProjectIntegration) - - if err := w.CheckValidity(); err != nil { - return nil, sdk.WrapError(err, "Unable to check validity") - } - if err := w.CheckDependencies(); err != nil { - return nil, sdk.WrapError(err, "Unable to check dependencies") - } - wf.PurgeTags = w.PurgeTags - if len(w.Metadata) > 0 { - wf.Metadata = make(map[string]string, len(w.Metadata)) - for k, v := range w.Metadata { - wf.Metadata[k] = v - } - } - if w.HistoryLength != nil && *w.HistoryLength > 0 { - wf.HistoryLength = *w.HistoryLength - } else { - wf.HistoryLength = sdk.DefaultHistoryLength - } - - rand.Seed(time.Now().Unix()) - entries := w.Entries() - var attempt int - fakeID := rand.Int63n(5000) - // attempt is there to avoid infinite loop, but it should not happened becase we check validty and dependencies earlier - for len(entries) != 0 && attempt < 10000 { - for name, entry := range entries { - entry.ID = fakeID - ok, err := entry.processNode(name, wf) - if err != nil { - return nil, sdk.WrapError(err, "Unable to process node") - } - if ok { - delete(entries, name) - fakeID++ - } +func ParseWorkflow(exportWorkflow Workflow) (*sdk.Workflow, error) { + switch exportWorkflow.GetVersion() { + case WorkflowVersion2: + workflowV2, ok := exportWorkflow.(v2.Workflow) + if ok { + return workflowV2.GetWorkflow() } - attempt++ - } - if len(entries) > 0 { - return nil, sdk.WithStack(fmt.Errorf("Unable to process %+v", entries)) - } - - //Process hooks - wf.VisitNode(w.processHooks) - - //Compute permissions - wf.Groups = make([]sdk.GroupPermission, 0, len(w.Permissions)) - for g, p := range w.Permissions { - perm := sdk.GroupPermission{Group: sdk.Group{Name: g}, Permission: p} - wf.Groups = append(wf.Groups, perm) - } - - //Compute notifications - if err := w.processNotifications(wf); err != nil { - return nil, err - } - - // if there is a template instance id on the workflow export, add it - if w.Template != nil { - templatePath := strings.Split(*w.Template, "/") - if len(templatePath) != 2 { - return nil, sdk.WithStack(fmt.Errorf("Invalid template path")) - } - wf.Template = &sdk.WorkflowTemplate{ - Group: &sdk.Group{Name: templatePath[0]}, - Slug: templatePath[1], + case WorkflowVersion1: + workflowV1, ok := exportWorkflow.(v1.Workflow) + if ok { + return workflowV1.GetWorkflow() } + default: + return nil, sdk.WithStack(fmt.Errorf("exportentities workflow cannot be cast, unknown version %s", exportWorkflow.GetVersion())) } - - wf.SortNode() - - return wf, nil + return nil, sdk.WithStack(fmt.Errorf("exportentities workflow cannot be cast %+v", exportWorkflow)) } -func (e *NodeEntry) getNode(name string, w *sdk.Workflow) (*sdk.Node, error) { - var mutex bool - if e.OneAtATime != nil && *e.OneAtATime { - mutex = true - } - node := &sdk.Node{ - Name: name, - Ref: name, - Type: sdk.NodeTypeFork, - Context: &sdk.NodeContext{ - PipelineName: e.PipelineName, - ApplicationName: e.ApplicationName, - EnvironmentName: e.EnvironmentName, - ProjectIntegrationName: e.ProjectIntegrationName, - Mutex: mutex, - }, - } - - if e.PipelineName != "" { - node.Type = sdk.NodeTypePipeline - } else if e.OutgoingHookModelName != "" { - node.Type = sdk.NodeTypeOutGoingHook - } else if len(e.DependsOn) > 1 { - node.Type = sdk.NodeTypeJoin - node.JoinContext = make([]sdk.NodeJoin, 0, len(e.DependsOn)) - for _, parent := range e.DependsOn { - node.JoinContext = append(node.JoinContext, sdk.NodeJoin{ParentName: parent}) - } - } - - if len(e.Permissions) > 0 { - //Compute permissions - node.Groups = make([]sdk.GroupPermission, 0, len(e.Permissions)) - for g, p := range e.Permissions { - perm := sdk.GroupPermission{Group: sdk.Group{Name: g}, Permission: p} - node.Groups = append(node.Groups, perm) - } - } - - if e.Conditions != nil { - node.Context.Conditions = sdk.WorkflowNodeConditions{ - PlainConditions: make([]sdk.WorkflowNodeCondition, 0, len(e.Conditions.PlainConditions)), - LuaScript: e.Conditions.LuaScript, - } - for _, c := range e.Conditions.PlainConditions { - node.Context.Conditions.PlainConditions = append(node.Context.Conditions.PlainConditions, sdk.WorkflowNodeCondition{ - Variable: c.Variable, - Operator: c.Operator, - Value: c.Value, - }) - } - } - - if len(e.Payload) > 0 { - if len(e.DependsOn) > 0 { - return nil, sdk.WrapError(sdk.ErrInvalidNodeDefaultPayload, "Default payload cannot be set on another node than the first one (node : %s)", name) +func SetTemplate(w Workflow, path string) (Workflow, error) { + switch w.GetVersion() { + case WorkflowVersion2: + workflowV2, ok := w.(v2.Workflow) + if ok { + workflowV2.Template = path + return workflowV2, nil } - node.Context.DefaultPayload = e.Payload - } - - mapPipelineParameters := sdk.ParametersFromMap(e.Parameters) - node.Context.DefaultPipelineParameters = mapPipelineParameters - - for _, w := range e.When { - switch w { - case "success": - node.Context.Conditions.PlainConditions = append(node.Context.Conditions.PlainConditions, sdk.WorkflowNodeCondition{ - Operator: sdk.WorkflowConditionsOperatorEquals, - Value: sdk.StatusSuccess, - Variable: "cds.status", - }) - case "manual": - node.Context.Conditions.PlainConditions = append(node.Context.Conditions.PlainConditions, sdk.WorkflowNodeCondition{ - Operator: sdk.WorkflowConditionsOperatorEquals, - Value: "true", - Variable: "cds.manual", - }) - default: - return nil, fmt.Errorf("Unsupported when condition %s", w) + case WorkflowVersion1: + workflowV1, ok := w.(v1.Workflow) + if ok { + workflowV1.Template = path + return workflowV1, nil } } - - if e.OneAtATime != nil { - node.Context.Mutex = *e.OneAtATime - } - - if e.OutgoingHookModelName != "" { - node.Type = sdk.NodeTypeOutGoingHook - config := sdk.WorkflowNodeHookConfig{} - for k, v := range e.OutgoingHookConfig { - config[k] = sdk.WorkflowNodeHookConfigValue{ - Value: v, - } - } - node.OutGoingHookContext = &sdk.NodeOutGoingHook{ - Config: config, - HookModelName: e.OutgoingHookModelName, - } - } - return node, nil + return nil, sdk.WithStack(fmt.Errorf("exportentities workflow cannot be cast %+v", w)) } -func (w *Workflow) processHooks(n *sdk.Node, wf *sdk.Workflow) { - var addHooks = func(hooks []HookEntry) { - for _, h := range hooks { - cfg := make(sdk.WorkflowNodeHookConfig, len(h.Config)) - for k, v := range h.Config { - var hType string - switch h.Model { - case sdk.KafkaHookModelName, sdk.RabbitMQHookModelName: - if k == sdk.HookModelIntegration { - hType = sdk.HookConfigTypeIntegration - } else { - hType = sdk.HookConfigTypeString - } - default: - hType = sdk.HookConfigTypeString - } - cfg[k] = sdk.WorkflowNodeHookConfigValue{ - Value: v, - Configurable: true, - Type: hType, - } - } - - hook := sdk.NodeHook{ - Config: cfg, - HookModelName: h.Model, - } - - if h.Conditions != nil { - hook.Conditions = *h.Conditions - } - n.Hooks = append(n.Hooks, hook) - } - } - - if len(w.PipelineHooks) > 0 { - //Only one node workflow - addHooks(w.PipelineHooks) - return - } - - addHooks(w.Hooks[n.Name]) -} - -func (e *NodeEntry) processNode(name string, w *sdk.Workflow) (bool, error) { - // Find WorkflowNodeAncestors - exist, err := e.processNodeAncestors(name, w) +func NewWorkflow(ctx context.Context, w sdk.Workflow, opts ...v2.ExportOptions) (Workflow, error) { + workflowToExport, err := v2.NewWorkflow(ctx, w, WorkflowVersion2, opts...) if err != nil { - return false, err - } - - if exist { - return true, nil + return workflowToExport, err } - - return false, nil + return workflowToExport, nil } -func (e *NodeEntry) processNodeAncestors(name string, w *sdk.Workflow) (bool, error) { - var ancestorsExist = true - var ancestors []*sdk.Node - - if len(e.DependsOn) == 1 { - a := e.DependsOn[0] - //Looking for the ancestor - ancestor := w.WorkflowData.NodeByName(a) - if ancestor == nil { - ancestorsExist = false - } - ancestors = append(ancestors, ancestor) - } else { - for _, a := range e.DependsOn { - //Looking for the ancestor - ancestor := w.WorkflowData.NodeByName(a) - if ancestor == nil { - ancestorsExist = false - break - } - ancestors = append(ancestors, ancestor) - } - } - - if !ancestorsExist { - return false, nil - } - - n, err := e.getNode(name, w) - if err != nil { - return false, err - } - - switch len(ancestors) { - case 0: - w.WorkflowData.Node = *n - return true, nil - case 1: - w.AddTrigger(ancestors[0].Name, *n) - return true, nil - default: - if n != nil && n.Type == sdk.NodeTypeJoin && joinAsNode(n) { - w.WorkflowData.Joins = append(w.WorkflowData.Joins, *n) - return true, nil - } - } - - // Compute join - - // Try to find an existing join with the same references - var join *sdk.Node - for i := range w.WorkflowData.Joins { - j := &w.WorkflowData.Joins[i] - var joinFound = true - - for _, ref := range j.JoinContext { - var refFound bool - for _, a := range e.DependsOn { - if ref.ParentName == a { - refFound = true - break - } - } - if !refFound { - joinFound = false - break - } - } - - if joinFound { - j.Ref = fmt.Sprintf("fakeRef%d", e.ID) - join = j - } - } - - var appendJoin bool - if join == nil { - joinContext := make([]sdk.NodeJoin, 0, len(e.DependsOn)) - for _, d := range e.DependsOn { - joinContext = append(joinContext, sdk.NodeJoin{ - ParentName: d, - }) - } - join = &sdk.Node{ - JoinContext: joinContext, - Type: sdk.NodeTypeJoin, - Ref: fmt.Sprintf("fakeRef%d", e.ID), - } - appendJoin = true - } - - join.Triggers = append(join.Triggers, sdk.NodeTrigger{ - ChildNode: *n, - }) - - if appendJoin { - w.WorkflowData.Joins = append(w.WorkflowData.Joins, *join) +func InitWorkflow(workName, appName, pipName string) Workflow { + return v2.Workflow{ + Version: WorkflowVersion2, + Name: workName, + Workflow: map[string]v2.NodeEntry{ + pipName: { + ApplicationName: appName, + PipelineName: pipName, + }, + }, } - return true, nil } diff --git a/sdk/messages.go b/sdk/messages.go index ad0e5bc005..74b4fab458 100644 --- a/sdk/messages.go +++ b/sdk/messages.go @@ -20,78 +20,74 @@ var ( //Message list var ( - MsgAppCreated = &Message{"MsgAppCreated", trad{FR: "L'application %s a été créée avec succès", EN: "Application %s successfully created"}, nil} - MsgAppUpdated = &Message{"MsgAppUpdated", trad{FR: "L'application %s a été mise à jour avec succès", EN: "Application %s successfully updated"}, nil} - MsgPipelineCreated = &Message{"MsgPipelineCreated", trad{FR: "Le pipeline %s a été créé avec succès", EN: "Pipeline %s successfully created"}, nil} - MsgPipelineCreationAborted = &Message{"MsgPipelineCreationAborted", trad{FR: "La création du pipeline %s a été abandonnée", EN: "Pipeline %s creation aborted"}, nil} - MsgPipelineExists = &Message{"MsgPipelineExists", trad{FR: "Le pipeline %s existe déjà", EN: "Pipeline %s already exists"}, nil} - MsgPipelineAttached = &Message{"MsgPipelineAttached", trad{FR: "Le pipeline %s a été attaché à l'application %s", EN: "Pipeline %s has been attached to application %s"}, nil} - MsgPipelineTriggerCreated = &Message{"MsgPipelineTriggerCreated", trad{FR: "Le trigger du pipeline %s de l'application %s vers le pipeline %s l'application %s a été créé avec succès", EN: "Trigger from pipeline %s of application %s to pipeline %s attached to application %s successfully created"}, nil} - MsgAppGroupInheritPermission = &Message{"MsgAppGroupInheritPermission", trad{FR: "Les permissions du projet sont appliquées sur l'application %s", EN: "Application %s inherits project permissions"}, nil} - MsgAppGroupSetPermission = &Message{"MsgAppGroupSetPermission", trad{FR: "Permission accordée au groupe %s sur l'application %s", EN: "Permission applied to group %s to application %s"}, nil} - MsgAppVariablesCreated = &Message{"MsgAppVariablesCreated", trad{FR: "Les variables ont été ajoutées avec succès sur l'application %s", EN: "Application variables for %s are successfully created"}, nil} - MsgHookCreated = &Message{"MsgHookCreated", trad{FR: "Hook créé sur le depôt %s vers le pipeline %s", EN: "Hook created on repository %s to pipeline %s"}, nil} - MsgAppKeyCreated = &Message{"MsgAppKeyCreated", trad{FR: "La clé %s %s a été créée sur l'application %s", EN: "%s key %s created on application %s"}, nil} - MsgEnvironmentExists = &Message{"MsgEnvironmentExists", trad{FR: "L'environnement %s existe déjà", EN: "Environment %s already exists"}, nil} - MsgEnvironmentCreated = &Message{"MsgEnvironmentCreated", trad{FR: "L'environnement %s a été créé avec succès", EN: "Environment %s successfully created"}, nil} - MsgEnvironmentVariableUpdated = &Message{"MsgEnvironmentVariableUpdated", trad{FR: "La variable %s de l'environnement %s a été mise à jour", EN: "Variable %s on environment %s has been updated"}, nil} - MsgEnvironmentVariableCannotBeUpdated = &Message{"MsgEnvironmentVariableCannotBeUpdated", trad{FR: "La variable %s de l'environnement %s n'a pu être mise à jour : %s", EN: "Variable %s on environment %s cannot be updated: %s"}, nil} - MsgEnvironmentVariableCreated = &Message{"MsgEnvironmentVariableCreated", trad{FR: "La variable %s de l'environnement %s a été ajoutée", EN: "Variable %s on environment %s has been added"}, nil} - MsgEnvironmentVariableCannotBeCreated = &Message{"MsgEnvironmentVariableCannotBeCreated", trad{FR: "La variable %s de l'environnement %s n'a pu être ajoutée : %s", EN: "Variable %s on environment %s cannot be added: %s"}, nil} - MsgEnvironmentGroupUpdated = &Message{"MsgEnvironmentGroupUpdated", trad{FR: "Le groupe %s de l'environnement %s a été mis à jour", EN: "Group %s on environment %s has been updated"}, nil} - MsgEnvironmentGroupCannotBeUpdated = &Message{"MsgEnvironmentGroupCannotBeUpdated", trad{FR: "Le groupe %s de l'environnement %s n'a pu être mis à jour : %s", EN: "Group %s on environment %s cannot be updated: %s"}, nil} - MsgEnvironmentGroupCreated = &Message{"MsgEnvironmentGroupCreated", trad{FR: "Le groupe %s de l'environnement %s a été ajouté", EN: "Group %s on environment %s has been added"}, nil} - MsgEnvironmentGroupCannotBeCreated = &Message{"MsgEnvironmentGroupCannotBeCreated", trad{FR: "Le groupe %s de l'environnement %s n'a pu être ajouté : %s", EN: "Group %s on environment %s cannot be added: %s"}, nil} - MsgEnvironmentGroupDeleted = &Message{"MsgEnvironmentGroupDeleted", trad{FR: "Le groupe %s de l'environnement %s a été supprimé", EN: "Group %s on environment %s has been deleted"}, nil} - MsgEnvironmentGroupCannotBeDeleted = &Message{"MsgEnvironmentGMsgEnvironmentGroupCannotBeDeletedroupCannotBeCreated", trad{FR: "Le groupe %s de l'environnement %s n'a pu être supprimé : %s", EN: "Group %s on environment %s cannot be deleted: %s"}, nil} - MsgEnvironmentKeyCreated = &Message{"MsgEnvironmentKeyCreated", trad{FR: "La clé %s %s a été créée sur l'environnement %s", EN: "%s key %s created on environment %s"}, nil} - MsgJobNotValidActionNotFound = &Message{"MsgJobNotValidActionNotFound", trad{FR: "Erreur de validation du Job %s : L'action %s à l'étape %d n'a pas été trouvée", EN: "Job %s validation Failure: Unknown action %s on step #%d"}, nil} - MsgJobNotValidInvalidActionParameter = &Message{"MsgJobNotValidInvalidActionParameter", trad{FR: "Erreur de validation du Job %s : Le paramètre %s de l'étape %d - %s est invalide", EN: "Job %s validation Failure: Invalid parameter %s on step #%d %s"}, nil} - MsgPipelineGroupUpdated = &Message{"MsgPipelineGroupUpdated", trad{FR: "Les permissions du groupe %s sur le pipeline %s on été mises à jour", EN: "Permission for group %s on pipeline %s has been updated"}, nil} - MsgPipelineGroupAdded = &Message{"MsgPipelineGroupAdded", trad{FR: "Les permissions du groupe %s sur le pipeline %s on été ajoutées", EN: "Permission for group %s on pipeline %s has been added"}, nil} - MsgPipelineGroupDeleted = &Message{"MsgPipelineGroupDeleted", trad{FR: "Les permissions du groupe %s sur le pipeline %s on été supprimées", EN: "Permission for group %s on pipeline %s has been deleted"}, nil} - MsgPipelineStageUpdated = &Message{"MsgPipelineStageUpdated", trad{FR: "Le stage %s a été mis à jour", EN: "Stage %s updated"}, nil} - MsgPipelineStageUpdating = &Message{"MsgPipelineStageUpdating", trad{FR: "Mise à jour du stage %s en cours...", EN: "Updating stage %s ..."}, nil} - MsgPipelineStageDeletingOldJobs = &Message{"MsgPipelineStageDeletingOldJobs", trad{FR: "Suppression des anciens jobs du stage %s en cours...", EN: "Deleting old jobs in stage %s ..."}, nil} - MsgPipelineStageInsertingNewJobs = &Message{"MsgPipelineStageInsertingNewJobs", trad{FR: "Insertion des nouveaux jobs dans le stage %s en cours...", EN: "Inserting new jobs in stage %s ..."}, nil} - MsgPipelineStageAdded = &Message{"MsgPipelineStageAdded", trad{FR: "Le stage %s a été ajouté", EN: "Stage %s added"}, nil} - MsgPipelineStageDeleted = &Message{"MsgPipelineStageDeleted", trad{FR: "Le stage %s a été supprimé", EN: "Stage %s deleted"}, nil} - MsgPipelineJobUpdated = &Message{"MsgPipelineJobUpdated", trad{FR: "Le job %s du stage %s a été mis à jour", EN: "Job %s in stage %s updated"}, nil} - MsgPipelineJobAdded = &Message{"MsgPipelineJobAdded", trad{FR: "Le job %s du stage %s a été ajouté", EN: "Job %s in stage %s added"}, nil} - MsgPipelineJobDeleted = &Message{"MsgPipelineJobDeleted", trad{FR: "Le job %s du stage %s a été supprimé", EN: "Job %s in stage %s deleted"}, nil} - MsgSpawnInfoHatcheryStarts = &Message{"MsgSpawnInfoHatcheryStarts", trad{FR: "La Hatchery %s a démarré le lancement du worker avec le modèle %s", EN: "Hatchery %s starts spawn worker with model %s"}, nil} - MsgSpawnInfoHatcheryErrorSpawn = &Message{"MsgSpawnInfoHatcheryErrorSpawn", trad{FR: "Une erreur est survenue lorsque la Hatchery %s a démarré un worker avec le modèle %s après %s, err:%s", EN: "Error while Hatchery %s spawn worker with model %s after %s, err:%s"}, nil} - MsgSpawnInfoHatcheryStartsSuccessfully = &Message{"MsgSpawnInfoHatcheryStartsSuccessfully", trad{FR: "La Hatchery %s a démarré le worker %s avec succès en %s", EN: "Hatchery %s spawn worker %s successfully in %s"}, nil} - MsgSpawnInfoHatcheryStartDockerPull = &Message{"MsgSpawnInfoHatcheryStartDockerPull", trad{FR: "La Hatchery %s a démarré le docker pull de l'image %s...", EN: "Hatchery %s starts docker pull %s..."}, nil} - MsgSpawnInfoHatcheryEndDockerPull = &Message{"MsgSpawnInfoHatcheryEndDockerPull", trad{FR: "La Hatchery %s a terminé le docker pull de l'image %s", EN: "Hatchery %s docker pull %s done"}, nil} - MsgSpawnInfoHatcheryEndDockerPullErr = &Message{"MsgSpawnInfoHatcheryEndDockerPullErr", trad{FR: "⚠ La Hatchery %s a terminé le docker pull de l'image %s en erreur: %s", EN: "⚠ Hatchery %s - docker pull %s done with error: %v"}, nil} - MsgSpawnInfoDeprecatedModel = &Message{"MsgSpawnInfoDeprecatedModel", trad{FR: "⚠ Attention vous utilisez un worker model (%s) déprécié", EN: "⚠ Pay attention you are using a deprecated worker model (%s)"}, nil} - MsgSpawnInfoWorkerEnd = &Message{"MsgSpawnInfoWorkerEnd", trad{FR: "✓ Le worker %s a terminé et a passé %s à travailler sur les étapes", EN: "✓ Worker %s finished working on this job and took %s to work on the steps"}, nil} - MsgSpawnInfoJobInQueue = &Message{"MsgSpawnInfoJobInQueue", trad{FR: "✓ Le job a été mis en file d'attente", EN: "✓ Job has been queued"}, nil} - MsgSpawnInfoJobTaken = &Message{"MsgSpawnInfoJobTaken", trad{FR: "Le job %s a été pris par le worker %s", EN: "Job %s was taken by worker %s"}, nil} - MsgSpawnInfoJobTakenWorkerVersion = &Message{"MsgSpawnInfoJobTakenWorkerVersion", trad{FR: "Worker %s version:%s os:%s arch:%s", EN: "Worker %s version:%s os:%s arch:%s"}, nil} - MsgSpawnInfoWorkerForJob = &Message{"MsgSpawnInfoWorkerForJob", trad{FR: "Ce worker %s a été créé pour lancer ce job", EN: "This worker %s was created to take this action"}, nil} - MsgSpawnInfoWorkerForJobError = &Message{"MsgSpawnInfoWorkerForJobError", trad{FR: "⚠ Ce worker %s a été créé pour lancer ce job, mais ne possède pas tous les pré-requis. Vérifiez que les prérequis suivants:%s", EN: "⚠ This worker %s was created to take this action, but does not have all prerequisites. Please verify the following prerequisites:%s"}, nil} - MsgSpawnInfoJobError = &Message{"MsgSpawnInfoJobError", trad{FR: "⚠ Impossible de lancer ce job : %s", EN: "⚠ Unable to run this job: %s"}, nil} - MsgWorkflowStarting = &Message{"MsgWorkflowStarting", trad{FR: "Le workflow %s#%s a été démarré", EN: "Workflow %s#%s has been started"}, nil} - MsgWorkflowError = &Message{"MsgWorkflowError", trad{FR: "⚠ Une erreur est survenue: %v", EN: "⚠ An error has occurred: %v"}, nil} - MsgWorkflowConditionError = &Message{"MsgWorkflowConditionError", trad{FR: "Les conditions de lancement ne sont pas respectées.", EN: "Run conditions aren't ok."}, nil} - MsgWorkflowNodeStop = &Message{"MsgWorkflowNodeStop", trad{FR: "Le pipeline a été arrété par %s", EN: "The pipeline has been stopped by %s"}, nil} - MsgWorkflowNodeMutex = &Message{"MsgWorkflowNodeMutex", trad{FR: "Le pipeline %s est mis en attente tant qu'il est en cours sur un autre run", EN: "The pipeline %s is waiting while it's running on another run"}, nil} - MsgWorkflowNodeMutexRelease = &Message{"MsgWorkflowNodeMutexRelease", trad{FR: "Lancement du pipeline %s", EN: "Triggering pipeline %s"}, nil} - MsgWorkflowImportedUpdated = &Message{"MsgWorkflowImportedUpdated", trad{FR: "Le workflow %s a été mis à jour", EN: "Workflow %s has been updated"}, nil} - MsgWorkflowImportedInserted = &Message{"MsgWorkflowImportedInserted", trad{FR: "Le workflow %s a été créé", EN: "Workflow %s has been created"}, nil} - MsgSpawnInfoHatcheryCannotStartJob = &Message{"MsgSpawnInfoHatcheryCannotStart", trad{FR: "Aucune hatchery n'a pu démarrer de worker respectant vos pré-requis de job, merci de les vérifier.", EN: "No hatchery can spawn a worker corresponding your job's requirements. Please check your job's requirements."}, nil} - MsgWorkflowRunBranchDeleted = &Message{"MsgWorkflowRunBranchDeleted", trad{FR: "La branche %s a été supprimée", EN: "Branch %s has been deleted"}, nil} - MsgWorkflowTemplateImportedInserted = &Message{"MsgWorkflowTemplateImportedInserted", trad{FR: "Le template de workflow %s/%s a été créé", EN: "Workflow template %s/%s has been created"}, nil} - MsgWorkflowTemplateImportedUpdated = &Message{"MsgWorkflowTemplateImportedUpdated", trad{FR: "Le template de workflow %s/%s a été mis à jour", EN: "Workflow template %s/%s has been updated"}, nil} - MsgWorkflowErrorBadPipelineName = &Message{"MsgWorkflowErrorBadPipelineName", trad{FR: "Le pipeline %s indiqué dans votre fichier yaml de workflow n'existe pas", EN: "The pipeline %s mentioned in your workflow's yaml file doesn't exist"}, nil} - MsgWorkflowErrorBadApplicationName = &Message{"MsgWorkflowErrorBadApplicationName", trad{FR: "L'application %s indiquée dans votre fichier yaml de workflow n'existe pas ou ne correspond pas aux normes ^[a-zA-Z0-9._-]{1,}$", EN: "The application %s mentioned in your workflow's yaml file doesn't exist or is incorrect with ^[a-zA-Z0-9._-]{1,}$"}, nil} - MsgWorkflowErrorBadEnvironmentName = &Message{"MsgWorkflowErrorBadEnvironmentName", trad{FR: "L'environnement %s indiqué dans votre fichier yaml de workflow n'existe pas", EN: "The environment %s mentioned in your workflow's yaml file doesn't exist"}, nil} - MsgWorkflowErrorBadIntegrationName = &Message{"MsgWorkflowErrorBadIntegrationName", trad{FR: "L'intégration %s indiquée dans votre fichier yaml n'existe pas", EN: "The integration %s mentioned in your yaml file doesn't exist"}, nil} - MsgWorkflowErrorBadCdsDir = &Message{"MsgWorkflowErrorBadCdsDir", trad{FR: "Un problème est survenu avec votre répertoire .cds", EN: "A problem occurred about your .cds directory"}, nil} - MsgWorkflowErrorUnknownKey = &Message{"MsgWorkflowErrorUnknownKey", trad{FR: "La clé '%s' est incorrecte ou n'existe pas", EN: "The key '%s' is incorrect or doesn't exist"}, nil} - MsgWorkflowErrorBadVCSStrategy = &Message{"MsgWorkflowErrorBadVCSStrategy", trad{FR: "Vos informations vcs_* sont incorrectes", EN: "Your vcs_* fields are incorrects"}, nil} + MsgAppCreated = &Message{"MsgAppCreated", trad{FR: "L'application %s a été créée avec succès", EN: "Application %s successfully created"}, nil, RunInfoTypInfo} + MsgAppUpdated = &Message{"MsgAppUpdated", trad{FR: "L'application %s a été mise à jour avec succès", EN: "Application %s successfully updated"}, nil, RunInfoTypInfo} + MsgPipelineCreated = &Message{"MsgPipelineCreated", trad{FR: "Le pipeline %s a été créé avec succès", EN: "Pipeline %s successfully created"}, nil, RunInfoTypInfo} + MsgPipelineCreationAborted = &Message{"MsgPipelineCreationAborted", trad{FR: "La création du pipeline %s a été abandonnée", EN: "Pipeline %s creation aborted"}, nil, RunInfoTypeError} + MsgPipelineExists = &Message{"MsgPipelineExists", trad{FR: "Le pipeline %s existe déjà", EN: "Pipeline %s already exists"}, nil, RunInfoTypInfo} + MsgAppVariablesCreated = &Message{"MsgAppVariablesCreated", trad{FR: "Les variables ont été ajoutées avec succès sur l'application %s", EN: "Application variables for %s are successfully created"}, nil, RunInfoTypInfo} + MsgAppKeyCreated = &Message{"MsgAppKeyCreated", trad{FR: "La clé %s %s a été créée sur l'application %s", EN: "%s key %s created on application %s"}, nil, RunInfoTypInfo} + MsgEnvironmentExists = &Message{"MsgEnvironmentExists", trad{FR: "L'environnement %s existe déjà", EN: "Environment %s already exists"}, nil, RunInfoTypInfo} + MsgEnvironmentCreated = &Message{"MsgEnvironmentCreated", trad{FR: "L'environnement %s a été créé avec succès", EN: "Environment %s successfully created"}, nil, RunInfoTypInfo} + MsgEnvironmentVariableUpdated = &Message{"MsgEnvironmentVariableUpdated", trad{FR: "La variable %s de l'environnement %s a été mise à jour", EN: "Variable %s on environment %s has been updated"}, nil, RunInfoTypInfo} + MsgEnvironmentVariableCannotBeUpdated = &Message{"MsgEnvironmentVariableCannotBeUpdated", trad{FR: "La variable %s de l'environnement %s n'a pu être mise à jour : %s", EN: "Variable %s on environment %s cannot be updated: %s"}, nil, RunInfoTypeError} + MsgEnvironmentVariableCreated = &Message{"MsgEnvironmentVariableCreated", trad{FR: "La variable %s de l'environnement %s a été ajoutée", EN: "Variable %s on environment %s has been added"}, nil, RunInfoTypInfo} + MsgEnvironmentVariableCannotBeCreated = &Message{"MsgEnvironmentVariableCannotBeCreated", trad{FR: "La variable %s de l'environnement %s n'a pu être ajoutée : %s", EN: "Variable %s on environment %s cannot be added: %s"}, nil, RunInfoTypeError} + MsgEnvironmentGroupUpdated = &Message{"MsgEnvironmentGroupUpdated", trad{FR: "Le groupe %s de l'environnement %s a été mis à jour", EN: "Group %s on environment %s has been updated"}, nil, RunInfoTypInfo} + MsgEnvironmentGroupCannotBeUpdated = &Message{"MsgEnvironmentGroupCannotBeUpdated", trad{FR: "Le groupe %s de l'environnement %s n'a pu être mis à jour : %s", EN: "Group %s on environment %s cannot be updated: %s"}, nil, RunInfoTypeError} + MsgEnvironmentGroupCreated = &Message{"MsgEnvironmentGroupCreated", trad{FR: "Le groupe %s de l'environnement %s a été ajouté", EN: "Group %s on environment %s has been added"}, nil, RunInfoTypInfo} + MsgEnvironmentGroupCannotBeCreated = &Message{"MsgEnvironmentGroupCannotBeCreated", trad{FR: "Le groupe %s de l'environnement %s n'a pu être ajouté : %s", EN: "Group %s on environment %s cannot be added: %s"}, nil, RunInfoTypeError} + MsgEnvironmentGroupDeleted = &Message{"MsgEnvironmentGroupDeleted", trad{FR: "Le groupe %s de l'environnement %s a été supprimé", EN: "Group %s on environment %s has been deleted"}, nil, RunInfoTypInfo} + MsgEnvironmentGroupCannotBeDeleted = &Message{"MsgEnvironmentGMsgEnvironmentGroupCannotBeDeletedroupCannotBeCreated", trad{FR: "Le groupe %s de l'environnement %s n'a pu être supprimé : %s", EN: "Group %s on environment %s cannot be deleted: %s"}, nil, RunInfoTypeError} + MsgEnvironmentKeyCreated = &Message{"MsgEnvironmentKeyCreated", trad{FR: "La clé %s %s a été créée sur l'environnement %s", EN: "%s key %s created on environment %s"}, nil, RunInfoTypInfo} + MsgJobNotValidActionNotFound = &Message{"MsgJobNotValidActionNotFound", trad{FR: "Erreur de validation du Job %s : L'action %s à l'étape %d n'a pas été trouvée", EN: "Job %s validation Failure: Unknown action %s on step #%d"}, nil, RunInfoTypeError} + MsgJobNotValidInvalidActionParameter = &Message{"MsgJobNotValidInvalidActionParameter", trad{FR: "Erreur de validation du Job %s : Le paramètre %s de l'étape %d - %s est invalide", EN: "Job %s validation Failure: Invalid parameter %s on step #%d %s"}, nil, RunInfoTypeError} + MsgPipelineGroupUpdated = &Message{"MsgPipelineGroupUpdated", trad{FR: "Les permissions du groupe %s sur le pipeline %s on été mises à jour", EN: "Permission for group %s on pipeline %s has been updated"}, nil, RunInfoTypInfo} + MsgPipelineGroupAdded = &Message{"MsgPipelineGroupAdded", trad{FR: "Les permissions du groupe %s sur le pipeline %s on été ajoutées", EN: "Permission for group %s on pipeline %s has been added"}, nil, RunInfoTypInfo} + MsgPipelineGroupDeleted = &Message{"MsgPipelineGroupDeleted", trad{FR: "Les permissions du groupe %s sur le pipeline %s on été supprimées", EN: "Permission for group %s on pipeline %s has been deleted"}, nil, RunInfoTypInfo} + MsgPipelineStageUpdated = &Message{"MsgPipelineStageUpdated", trad{FR: "Le stage %s a été mis à jour", EN: "Stage %s updated"}, nil, RunInfoTypInfo} + MsgPipelineStageUpdating = &Message{"MsgPipelineStageUpdating", trad{FR: "Mise à jour du stage %s en cours...", EN: "Updating stage %s ..."}, nil, RunInfoTypInfo} + MsgPipelineStageDeletingOldJobs = &Message{"MsgPipelineStageDeletingOldJobs", trad{FR: "Suppression des anciens jobs du stage %s en cours...", EN: "Deleting old jobs in stage %s ..."}, nil, RunInfoTypInfo} + MsgPipelineStageInsertingNewJobs = &Message{"MsgPipelineStageInsertingNewJobs", trad{FR: "Insertion des nouveaux jobs dans le stage %s en cours...", EN: "Inserting new jobs in stage %s ..."}, nil, RunInfoTypInfo} + MsgPipelineStageAdded = &Message{"MsgPipelineStageAdded", trad{FR: "Le stage %s a été ajouté", EN: "Stage %s added"}, nil, RunInfoTypInfo} + MsgPipelineStageDeleted = &Message{"MsgPipelineStageDeleted", trad{FR: "Le stage %s a été supprimé", EN: "Stage %s deleted"}, nil, RunInfoTypInfo} + MsgPipelineJobUpdated = &Message{"MsgPipelineJobUpdated", trad{FR: "Le job %s du stage %s a été mis à jour", EN: "Job %s in stage %s updated"}, nil, RunInfoTypInfo} + MsgPipelineJobAdded = &Message{"MsgPipelineJobAdded", trad{FR: "Le job %s du stage %s a été ajouté", EN: "Job %s in stage %s added"}, nil, RunInfoTypInfo} + MsgPipelineJobDeleted = &Message{"MsgPipelineJobDeleted", trad{FR: "Le job %s du stage %s a été supprimé", EN: "Job %s in stage %s deleted"}, nil, RunInfoTypInfo} + MsgSpawnInfoHatcheryStarts = &Message{"MsgSpawnInfoHatcheryStarts", trad{FR: "La Hatchery %s a démarré le lancement du worker avec le modèle %s", EN: "Hatchery %s starts spawn worker with model %s"}, nil, RunInfoTypInfo} + MsgSpawnInfoHatcheryErrorSpawn = &Message{"MsgSpawnInfoHatcheryErrorSpawn", trad{FR: "Une erreur est survenue lorsque la Hatchery %s a démarré un worker avec le modèle %s après %s, err:%s", EN: "Error while Hatchery %s spawn worker with model %s after %s, err:%s"}, nil, RunInfoTypeError} + MsgSpawnInfoHatcheryStartsSuccessfully = &Message{"MsgSpawnInfoHatcheryStartsSuccessfully", trad{FR: "La Hatchery %s a démarré le worker %s avec succès en %s", EN: "Hatchery %s spawn worker %s successfully in %s"}, nil, RunInfoTypInfo} + MsgSpawnInfoHatcheryStartDockerPull = &Message{"MsgSpawnInfoHatcheryStartDockerPull", trad{FR: "La Hatchery %s a démarré le docker pull de l'image %s...", EN: "Hatchery %s starts docker pull %s..."}, nil, RunInfoTypInfo} + MsgSpawnInfoHatcheryEndDockerPull = &Message{"MsgSpawnInfoHatcheryEndDockerPull", trad{FR: "La Hatchery %s a terminé le docker pull de l'image %s", EN: "Hatchery %s docker pull %s done"}, nil, RunInfoTypInfo} + MsgSpawnInfoHatcheryEndDockerPullErr = &Message{"MsgSpawnInfoHatcheryEndDockerPullErr", trad{FR: "⚠ La Hatchery %s a terminé le docker pull de l'image %s en erreur: %s", EN: "⚠ Hatchery %s - docker pull %s done with error: %v"}, nil, RunInfoTypeError} + MsgSpawnInfoDeprecatedModel = &Message{"MsgSpawnInfoDeprecatedModel", trad{FR: "⚠ Attention vous utilisez un worker model (%s) déprécié", EN: "⚠ Pay attention you are using a deprecated worker model (%s)"}, nil, RunInfoTypeWarning} + MsgSpawnInfoWorkerEnd = &Message{"MsgSpawnInfoWorkerEnd", trad{FR: "✓ Le worker %s a terminé et a passé %s à travailler sur les étapes", EN: "✓ Worker %s finished working on this job and took %s to work on the steps"}, nil, RunInfoTypInfo} + MsgSpawnInfoJobInQueue = &Message{"MsgSpawnInfoJobInQueue", trad{FR: "✓ Le job a été mis en file d'attente", EN: "✓ Job has been queued"}, nil, RunInfoTypInfo} + MsgSpawnInfoJobTaken = &Message{"MsgSpawnInfoJobTaken", trad{FR: "Le job %s a été pris par le worker %s", EN: "Job %s was taken by worker %s"}, nil, RunInfoTypInfo} + MsgSpawnInfoJobTakenWorkerVersion = &Message{"MsgSpawnInfoJobTakenWorkerVersion", trad{FR: "Worker %s version:%s os:%s arch:%s", EN: "Worker %s version:%s os:%s arch:%s"}, nil, RunInfoTypInfo} + MsgSpawnInfoWorkerForJob = &Message{"MsgSpawnInfoWorkerForJob", trad{FR: "Ce worker %s a été créé pour lancer ce job", EN: "This worker %s was created to take this action"}, nil, RunInfoTypInfo} + MsgSpawnInfoWorkerForJobError = &Message{"MsgSpawnInfoWorkerForJobError", trad{FR: "⚠ Ce worker %s a été créé pour lancer ce job, mais ne possède pas tous les pré-requis. Vérifiez que les prérequis suivants:%s", EN: "⚠ This worker %s was created to take this action, but does not have all prerequisites. Please verify the following prerequisites:%s"}, nil, RunInfoTypeError} + MsgSpawnInfoJobError = &Message{"MsgSpawnInfoJobError", trad{FR: "⚠ Impossible de lancer ce job : %s", EN: "⚠ Unable to run this job: %s"}, nil, RunInfoTypInfo} + MsgWorkflowStarting = &Message{"MsgWorkflowStarting", trad{FR: "Le workflow %s#%s a été démarré", EN: "Workflow %s#%s has been started"}, nil, RunInfoTypInfo} + MsgWorkflowError = &Message{"MsgWorkflowError", trad{FR: "⚠ Une erreur est survenue: %v", EN: "⚠ An error has occurred: %v"}, nil, RunInfoTypeError} + MsgWorkflowConditionError = &Message{"MsgWorkflowConditionError", trad{FR: "Les conditions de lancement ne sont pas respectées.", EN: "Run conditions aren't ok."}, nil, RunInfoTypInfo} + MsgWorkflowNodeStop = &Message{"MsgWorkflowNodeStop", trad{FR: "Le pipeline a été arrété par %s", EN: "The pipeline has been stopped by %s"}, nil, RunInfoTypInfo} + MsgWorkflowNodeMutex = &Message{"MsgWorkflowNodeMutex", trad{FR: "Le pipeline %s est mis en attente tant qu'il est en cours sur un autre run", EN: "The pipeline %s is waiting while it's running on another run"}, nil, RunInfoTypInfo} + MsgWorkflowNodeMutexRelease = &Message{"MsgWorkflowNodeMutexRelease", trad{FR: "Lancement du pipeline %s", EN: "Triggering pipeline %s"}, nil, RunInfoTypInfo} + MsgWorkflowImportedUpdated = &Message{"MsgWorkflowImportedUpdated", trad{FR: "Le workflow %s a été mis à jour", EN: "Workflow %s has been updated"}, nil, RunInfoTypInfo} + MsgWorkflowImportedInserted = &Message{"MsgWorkflowImportedInserted", trad{FR: "Le workflow %s a été créé", EN: "Workflow %s has been created"}, nil, RunInfoTypInfo} + MsgSpawnInfoHatcheryCannotStartJob = &Message{"MsgSpawnInfoHatcheryCannotStart", trad{FR: "Aucune hatchery n'a pu démarrer de worker respectant vos pré-requis de job, merci de les vérifier.", EN: "No hatchery can spawn a worker corresponding your job's requirements. Please check your job's requirements."}, nil, RunInfoTypeWarning} + MsgWorkflowRunBranchDeleted = &Message{"MsgWorkflowRunBranchDeleted", trad{FR: "La branche %s a été supprimée", EN: "Branch %s has been deleted"}, nil, RunInfoTypInfo} + MsgWorkflowTemplateImportedInserted = &Message{"MsgWorkflowTemplateImportedInserted", trad{FR: "Le template de workflow %s/%s a été créé", EN: "Workflow template %s/%s has been created"}, nil, RunInfoTypInfo} + MsgWorkflowTemplateImportedUpdated = &Message{"MsgWorkflowTemplateImportedUpdated", trad{FR: "Le template de workflow %s/%s a été mis à jour", EN: "Workflow template %s/%s has been updated"}, nil, RunInfoTypInfo} + MsgWorkflowErrorBadPipelineName = &Message{"MsgWorkflowErrorBadPipelineName", trad{FR: "Le pipeline %s indiqué dans votre fichier yaml de workflow n'existe pas", EN: "The pipeline %s mentioned in your workflow's yaml file doesn't exist"}, nil, RunInfoTypeError} + MsgWorkflowErrorBadApplicationName = &Message{"MsgWorkflowErrorBadApplicationName", trad{FR: "L'application %s indiquée dans votre fichier yaml de workflow n'existe pas ou ne correspond pas aux normes ^[a-zA-Z0-9._-]{1,}$", EN: "The application %s mentioned in your workflow's yaml file doesn't exist or is incorrect with ^[a-zA-Z0-9._-]{1,}$"}, nil, RunInfoTypeError} + MsgWorkflowErrorBadEnvironmentName = &Message{"MsgWorkflowErrorBadEnvironmentName", trad{FR: "L'environnement %s indiqué dans votre fichier yaml de workflow n'existe pas", EN: "The environment %s mentioned in your workflow's yaml file doesn't exist"}, nil, RunInfoTypeError} + MsgWorkflowErrorBadIntegrationName = &Message{"MsgWorkflowErrorBadIntegrationName", trad{FR: "L'intégration %s indiquée dans votre fichier yaml n'existe pas", EN: "The integration %s mentioned in your yaml file doesn't exist"}, nil, RunInfoTypeError} + MsgWorkflowErrorBadCdsDir = &Message{"MsgWorkflowErrorBadCdsDir", trad{FR: "Un problème est survenu avec votre répertoire .cds", EN: "A problem occurred about your .cds directory"}, nil, RunInfoTypeError} + MsgWorkflowErrorUnknownKey = &Message{"MsgWorkflowErrorUnknownKey", trad{FR: "La clé '%s' est incorrecte ou n'existe pas", EN: "The key '%s' is incorrect or doesn't exist"}, nil, RunInfoTypeError} + MsgWorkflowErrorBadVCSStrategy = &Message{"MsgWorkflowErrorBadVCSStrategy", trad{FR: "Vos informations vcs_* sont incorrectes", EN: "Your vcs_* fields are incorrects"}, nil, RunInfoTypeError} + MsgWorkflowDeprecatedVersion = &Message{"MsgWorkflowDeprecatedVersion", trad{FR: "La configuration yaml de votre workflow est dans un format déprécié. Exportez le avec la CLI `cdsctl workflow export %s %s`", EN: "The yaml workflow configuration format is deprecated. Export your workflow with CLI `cdsctl workflow export %s %s`"}, nil, RunInfoTypeWarning} ) // Messages contains all sdk Messages @@ -101,12 +97,7 @@ var Messages = map[string]*Message{ MsgPipelineCreated.ID: MsgPipelineCreated, MsgPipelineCreationAborted.ID: MsgPipelineCreationAborted, MsgPipelineExists.ID: MsgPipelineExists, - MsgPipelineAttached.ID: MsgPipelineAttached, - MsgPipelineTriggerCreated.ID: MsgPipelineTriggerCreated, - MsgAppGroupInheritPermission.ID: MsgAppGroupInheritPermission, - MsgAppGroupSetPermission.ID: MsgAppGroupSetPermission, MsgAppVariablesCreated.ID: MsgAppVariablesCreated, - MsgHookCreated.ID: MsgHookCreated, MsgAppKeyCreated.ID: MsgAppKeyCreated, MsgEnvironmentExists.ID: MsgEnvironmentExists, MsgEnvironmentCreated.ID: MsgEnvironmentCreated, @@ -168,6 +159,7 @@ var Messages = map[string]*Message{ MsgWorkflowErrorBadCdsDir.ID: MsgWorkflowErrorBadCdsDir, MsgWorkflowErrorUnknownKey.ID: MsgWorkflowErrorUnknownKey, MsgWorkflowErrorBadVCSStrategy.ID: MsgWorkflowErrorBadVCSStrategy, + MsgWorkflowDeprecatedVersion.ID: MsgWorkflowDeprecatedVersion, } //Message represent a struc format translated messages @@ -175,6 +167,7 @@ type Message struct { ID string Format trad Args []interface{} + Type string } //NewMessage instanciantes a new message @@ -183,6 +176,7 @@ func NewMessage(m *Message, args ...interface{}) Message { Format: m.Format, Args: args, ID: m.ID, + Type: m.Type, } } diff --git a/sdk/workflow_run.go b/sdk/workflow_run.go index eb2817e981..28df40d3c3 100644 --- a/sdk/workflow_run.go +++ b/sdk/workflow_run.go @@ -182,6 +182,12 @@ func (r *WorkflowRun) GetOutgoingHookRun(uuid string) *WorkflowNodeRun { return nil } +const ( + RunInfoTypInfo = "Info" + RunInfoTypeWarning = "Warning" + RunInfoTypeError = "Error" +) + //WorkflowRunInfo is an info on workflow run type WorkflowRunInfo struct { APITime time.Time `json:"api_time,omitempty" db:"-"` @@ -189,7 +195,7 @@ type WorkflowRunInfo struct { // UserMessage contains msg translated for end user UserMessage string `json:"user_message,omitempty" db:"-"` SubNumber int64 `json:"sub_number,omitempty" db:"-"` - IsError bool `json:"is_error" db:"-"` + Type string `json:"type" db:"-"` } //WorkflowRunTag is a tag on workflow run diff --git a/tests/04_sc_workflow_edit_scheduler.yml b/tests/04_sc_workflow_edit_scheduler.yml index a3e026f0cb..8aaa938703 100644 --- a/tests/04_sc_workflow_edit_scheduler.yml +++ b/tests/04_sc_workflow_edit_scheduler.yml @@ -28,7 +28,7 @@ testcases: - result.code ShouldEqual 0 - script: "{{.cdsctl}} -f {{.cdsctl.config}} workflow export ITSCWRKFLW16 ITSCWRKFLW16-WORKFLOW --format json" assertions: - - result.systemoutjson.pipeline_hooks.pipeline_hooks0.config.cron ShouldEqual '10 * * * *' + - result.systemoutjson.hooks.itscwrkflw16-pipeline.itscwrkflw16-pipeline0.config.cron ShouldEqual '10 * * * *' - name: get-imported-workflow steps: @@ -49,7 +49,7 @@ testcases: - result.code ShouldEqual 0 - script: "{{.cdsctl}} -f {{.cdsctl.config}} workflow export ITSCWRKFLW16 ITSCWRKFLW16-WORKFLOW --format json" assertions: - - result.systemoutjson.pipeline_hooks.pipeline_hooks0.config.cron ShouldEqual '20 * * * *' + - result.systemoutjson.hooks.itscwrkflw16-pipeline.itscwrkflw16-pipeline0.config.cron ShouldEqual '20 * * * *' - name: get-updated-workflow steps: diff --git a/tests/fixtures/ITSCWRKFLW10/ITSCWRKFLW10.yml b/tests/fixtures/ITSCWRKFLW10/ITSCWRKFLW10.yml index 5d82a7e01e..863e80dfa8 100644 --- a/tests/fixtures/ITSCWRKFLW10/ITSCWRKFLW10.yml +++ b/tests/fixtures/ITSCWRKFLW10/ITSCWRKFLW10.yml @@ -1,5 +1,5 @@ name: "ITSCWRKFLW10-WORKFLOW" -version: v1.0 +version: v2.0 workflow: echo: depends_on: diff --git a/tests/fixtures/ITSCWRKFLW3/ITSCWRKFLW3.yml b/tests/fixtures/ITSCWRKFLW3/ITSCWRKFLW3.yml index 6fd5cd7ccf..468532135c 100644 --- a/tests/fixtures/ITSCWRKFLW3/ITSCWRKFLW3.yml +++ b/tests/fixtures/ITSCWRKFLW3/ITSCWRKFLW3.yml @@ -1,5 +1,5 @@ name: ITSCWRKFLW3-WORKFLOW -version: v1.0 +version: v2.0 workflow: root: pipeline: root diff --git a/ui/src/app/model/pipeline.model.ts b/ui/src/app/model/pipeline.model.ts index 6c750ad07b..44b268512e 100644 --- a/ui/src/app/model/pipeline.model.ts +++ b/ui/src/app/model/pipeline.model.ts @@ -152,10 +152,16 @@ export class PipelineRunRequest { export class SpawnInfo { api_time: Date; remote_time: Date; - is_error: boolean; + type: string; + message: SpawnInfoMessage; user_message: string; } +export class SpawnInfoMessage { + args: Array; + id: string; +} + export class BuildResult { status: string; step_logs: Log; diff --git a/ui/src/app/views/workflow/run/errors.ts b/ui/src/app/views/workflow/run/errors.ts index 0be6a31b20..6c21f08d99 100644 --- a/ui/src/app/views/workflow/run/errors.ts +++ b/ui/src/app/views/workflow/run/errors.ts @@ -32,6 +32,13 @@ export const ErrorMessageMap: { [key: string]: Message } = { }, }; +export const WarningMessageMap: { [key: string]: Message } = { + 'MsgWorkflowDeprecatedVersion': { + title: 'workflow_warning_deprecated_yaml', + link: 'https://ovh.github.io/cds/docs/components/cdsctl/workflow/export/' + }, +}; + interface Message { title: string; description?: string; diff --git a/ui/src/app/views/workflow/run/workflow.run.component.ts b/ui/src/app/views/workflow/run/workflow.run.component.ts index 77bc5c1c8c..49b8c171c2 100644 --- a/ui/src/app/views/workflow/run/workflow.run.component.ts +++ b/ui/src/app/views/workflow/run/workflow.run.component.ts @@ -3,7 +3,7 @@ import { Title } from '@angular/platform-browser'; import { ActivatedRoute } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { Store } from '@ngxs/store'; -import { PipelineStatus } from 'app/model/pipeline.model'; +import { PipelineStatus, SpawnInfo } from 'app/model/pipeline.model'; import { Project } from 'app/model/project.model'; import { WorkflowRun } from 'app/model/workflow.run.model'; import { NotificationService } from 'app/service/notification/notification.service'; @@ -14,7 +14,7 @@ import { ChangeToRunView, GetWorkflowRun } from 'app/store/workflow.action'; import { WorkflowState, WorkflowStateModel } from 'app/store/workflow.state'; import { Subscription } from 'rxjs'; import { filter } from 'rxjs/operators'; -import { ErrorMessageMap } from './errors'; +import { ErrorMessageMap, WarningMessageMap } from './errors'; @Component({ selector: 'app-workflow-run', @@ -39,7 +39,9 @@ export class WorkflowRunComponent implements OnInit { dataSubs: Subscription; paramsSubs: Subscription; loadingRun = true; + warningsMap = WarningMessageMap; errorsMap = ErrorMessageMap; + warnings: Array; displayError = false; // id, status, workflows, infos, num @@ -115,7 +117,8 @@ export class WorkflowRunComponent implements OnInit { this.workflowRunData['num'] = s.workflowRun.num; if (s.workflowRun.infos && s.workflowRun.infos.length > 0) { - this.displayError = s.workflowRun.infos.some((info) => info.is_error); + this.displayError = s.workflowRun.infos.some((info) => info.type === 'Error'); + this.warnings = s.workflowRun.infos.filter(i => i.type === 'Warning'); } this.updateTitle(s.workflowRun); diff --git a/ui/src/app/views/workflow/run/workflow.run.html b/ui/src/app/views/workflow/run/workflow.run.html index d41f4d92d2..840964986a 100644 --- a/ui/src/app/views/workflow/run/workflow.run.html +++ b/ui/src/app/views/workflow/run/workflow.run.html @@ -3,11 +3,6 @@ [class.above]="workflowRunData['status'] === pipelineStatusEnum.PENDING"> - - - -
@@ -18,7 +13,7 @@ - +
@@ -44,6 +39,44 @@
+ +
+
+
+
+ + + + +
+ + + {{warningsMap[info.message.id].title | translate}} + {{info.message.id}} +
+
+

+ {{info.user_message}}. +

+

+ {{'common_find_help' | translate}} {{'common_here' | translate}} +

+
+
+
+
+
+
+
+
+ + + +
diff --git a/ui/src/assets/i18n/en.json b/ui/src/assets/i18n/en.json index b7a4c40823..22991fb54d 100644 --- a/ui/src/assets/i18n/en.json +++ b/ui/src/assets/i18n/en.json @@ -926,6 +926,7 @@ "workflow_template_help_edit_from": "Current workflow template is synchronized from URL. You can't edit it.", "workflow_template_import_from_url": "Import from URL", "workflow_template_imported_from_url": "Imported from URL", + "workflow_warning_deprecated_yaml": "Yaml workflow configuration is deprecated", "application_repository_help_line_1": "Your application was not imported from your code.", "pipeline_repository_help_line_1": "Your pipeline was not imported from your code.", "workflow_repository_help_line_1": "Your workflow was not imported from your code.", diff --git a/ui/src/assets/i18n/fr.json b/ui/src/assets/i18n/fr.json index e599321e76..e4500f886a 100644 --- a/ui/src/assets/i18n/fr.json +++ b/ui/src/assets/i18n/fr.json @@ -932,6 +932,7 @@ "workflow_ascode_updated": "Brouillon mis à jour", "workflow_updated": "Workflow mis à jour", "workflow_vcs_resynced": "Statuts du dépot distant resynchronisés", + "workflow_warning_deprecated_yaml": "Configuration yaml du workflow dépréciée", "workflow_wizard_description": "Choisissez vos options de workflow", "workflow_wizard_repo_analyse": "Analyse du dépôt...", "workflow_wizard_select_repo_loading": "Chargement des dépots...",