diff --git a/engine/api/api.go b/engine/api/api.go index 6bc42a8d68..597ccc3cf6 100644 --- a/engine/api/api.go +++ b/engine/api/api.go @@ -713,6 +713,10 @@ func (a *API) Serve(ctx context.Context) error { return migrate.RefactorProjectIntegrationCrypto(ctx, a.DBConnectionFactory.GetDBMap()) }}) + migrate.Add(ctx, sdk.Migration{Name: "AsCodeEventsWorkflowHolder", Release: "0.44.0", Blocker: false, Automatic: true, ExecFunc: func(ctx context.Context) error { + return migrate.RefactorAsCodeEventsWorkflowHolder(ctx, a.DBConnectionFactory.GetDBMap()) + }}) + isFreshInstall, errF := version.IsFreshInstall(a.mustDB()) if errF != nil { return sdk.WrapError(errF, "Unable to check if it's a fresh installation of CDS") diff --git a/engine/api/api_routes.go b/engine/api/api_routes.go index 6bf67fe72a..8ee544c605 100644 --- a/engine/api/api_routes.go +++ b/engine/api/api_routes.go @@ -182,9 +182,6 @@ func (api *API) InitRouter() { r.Handle("/project/{permProjectKey}/keys", Scope(sdk.AuthConsumerScopeProject), r.GET(api.getKeysInProjectHandler), r.POST(api.addKeyInProjectHandler)) r.Handle("/project/{permProjectKey}/keys/{name}", Scope(sdk.AuthConsumerScopeProject), r.DELETE(api.deleteKeyInProjectHandler)) - // As Code - r.Handle("/project/{key}/ascode/events/resync", Scope(sdk.AuthConsumerScopeProject), r.POST(api.postResyncPRAsCodeHandler, EnableTracing())) - // Import Application r.Handle("/project/{permProjectKey}/import/application", Scope(sdk.AuthConsumerScopeProject), r.POST(api.postApplicationImportHandler)) // Export Application @@ -239,6 +236,7 @@ func (api *API) InitRouter() { r.Handle("/project/{key}/workflows/{permWorkflowName}/eventsintegration/{integrationID}", Scope(sdk.AuthConsumerScopeProject), r.DELETE(api.deleteWorkflowEventsIntegrationHandler)) r.Handle("/project/{key}/workflows/{permWorkflowName}/icon", Scope(sdk.AuthConsumerScopeProject), r.PUT(api.putWorkflowIconHandler), r.DELETE(api.deleteWorkflowIconHandler)) r.Handle("/project/{key}/workflows/{permWorkflowName}/ascode", Scope(sdk.AuthConsumerScopeProject), r.POST(api.postWorkflowAsCodeHandler)) + r.Handle("/project/{key}/workflows/{permWorkflowName}/ascode/events/resync", Scope(sdk.AuthConsumerScopeProject), r.POST(api.postWorkflowAsCodeEventsResyncHandler, EnableTracing())) r.Handle("/project/{key}/workflows/{permWorkflowName}/ascode/{uuid}", Scope(sdk.AuthConsumerScopeProject), r.GET(api.getWorkflowAsCodeHandler)) r.Handle("/project/{key}/workflows/{permWorkflowName}/label", Scope(sdk.AuthConsumerScopeProject), r.POST(api.postWorkflowLabelHandler)) r.Handle("/project/{key}/workflows/{permWorkflowName}/label/{labelID}", Scope(sdk.AuthConsumerScopeProject), r.DELETE(api.deleteWorkflowLabelHandler)) diff --git a/engine/api/ascode.go b/engine/api/ascode.go index d8ce8fcfa9..0cef459c94 100644 --- a/engine/api/ascode.go +++ b/engine/api/ascode.go @@ -7,8 +7,7 @@ import ( "github.com/gorilla/mux" - "github.com/ovh/cds/engine/api/application" - "github.com/ovh/cds/engine/api/ascode/sync" + "github.com/ovh/cds/engine/api/ascode" "github.com/ovh/cds/engine/api/event" "github.com/ovh/cds/engine/api/operation" "github.com/ovh/cds/engine/api/project" @@ -88,12 +87,9 @@ func (api *API) postImportAsCodeHandler() service.Handler { func (api *API) getImportAsCodeHandler() service.Handler { return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) - uuid := vars["uuid"] - - var ope = new(sdk.Operation) - ope.UUID = uuid - if err := operation.GetRepositoryOperation(ctx, api.mustDB(), ope); err != nil { - return sdk.WrapError(err, "Cannot get repository operation status") + ope, err := operation.GetRepositoryOperation(ctx, api.mustDB(), vars["uuid"]) + if err != nil { + return sdk.WrapError(err, "cannot get repository operation status") } return service.WriteJSON(w, ope, http.StatusOK) } @@ -123,11 +119,9 @@ func (api *API) postPerformImportAsCodeHandler() service.Handler { return sdk.WrapError(errp, "postPerformImportAsCodeHandler> Cannot load project %s", key) } - var ope = new(sdk.Operation) - ope.UUID = uuid - - if err := operation.GetRepositoryOperation(ctx, api.mustDB(), ope); err != nil { - return sdk.WrapError(err, "Unable to get repository operation") + ope, err := operation.GetRepositoryOperation(ctx, api.mustDB(), uuid) + if err != nil { + return sdk.WrapError(err, "unable to get repository operation") } if ope.Status != sdk.OperationStatusDone { @@ -167,11 +161,14 @@ func (api *API) postPerformImportAsCodeHandler() service.Handler { if opt.FromRepository != "" { mods = append(mods, workflowtemplate.TemplateRequestModifiers.DefaultNameAndRepositories(ctx, api.mustDB(), api.Cache, *proj, opt.FromRepository)) } - wti, err := workflowtemplate.CheckAndExecuteTemplate(ctx, api.mustDB(), *consumer, *proj, &data, mods...) + var allMsg []sdk.Message + msgTemplate, wti, err := workflowtemplate.CheckAndExecuteTemplate(ctx, api.mustDB(), *consumer, *proj, &data, mods...) + allMsg = append(allMsg, msgTemplate...) if err != nil { return err } - allMsg, wrkflw, _, err := workflow.Push(ctx, api.mustDB(), api.Cache, proj, data, opt, getAPIConsumer(ctx), project.DecryptWithBuiltinKey) + msgPush, wrkflw, _, err := workflow.Push(ctx, api.mustDB(), api.Cache, proj, data, opt, getAPIConsumer(ctx), project.DecryptWithBuiltinKey) + allMsg = append(allMsg, msgPush...) if err != nil { return sdk.WrapError(err, "unable to push workflow") } @@ -203,43 +200,38 @@ func (api *API) postPerformImportAsCodeHandler() service.Handler { } } -func (api *API) postResyncPRAsCodeHandler() service.Handler { +func (api *API) postWorkflowAsCodeEventsResyncHandler() service.Handler { return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) - key := vars["key"] - appName := FormString(r, "appName") - fromRepo := FormString(r, "repo") + projectKey := vars["key"] + workflowName := vars["permWorkflowName"] - proj, errP := project.Load(api.mustDB(), key, + proj, err := project.Load(api.mustDB(), projectKey, project.LoadOptions.WithApplicationWithDeploymentStrategies, project.LoadOptions.WithPipelines, project.LoadOptions.WithEnvironments, project.LoadOptions.WithIntegrations, - project.LoadOptions.WithClearKeys) - if errP != nil { - return sdk.WrapError(errP, "unable to load project") - } - var app sdk.Application - switch { - case appName != "": - appP, err := application.LoadByName(api.mustDB(), key, appName) - if err != nil { - return err - } - app = *appP - case fromRepo != "": - wkf, err := workflow.LoadByRepo(ctx, api.Cache, api.mustDB(), *proj, fromRepo, workflow.LoadOptions{}) - if err != nil { - return err - } - app = wkf.Applications[wkf.WorkflowData.Node.Context.ApplicationID] - default: - return sdk.WrapError(sdk.ErrWrongRequest, "Missing appName or repo query parameter") + project.LoadOptions.WithClearKeys, + ) + if err != nil { + return err } - if _, _, err := sync.SyncAsCodeEvent(ctx, api.mustDB(), api.Cache, *proj, app, getAPIConsumer(ctx).AuthentifiedUser); err != nil { + wf, err := workflow.Load(ctx, api.mustDB(), api.Cache, *proj, workflowName, workflow.LoadOptions{}) + if err != nil { return err } + + res, err := ascode.SyncEvents(ctx, api.mustDB(), api.Cache, *proj, *wf, getAPIConsumer(ctx).AuthentifiedUser) + if err != nil { + return err + } + if res.Merged { + if err := workflow.UpdateFromRepository(api.mustDB(), wf.ID, res.FromRepository); err != nil { + return err + } + } + return nil } } diff --git a/engine/api/ascode/ascode_pr.go b/engine/api/ascode/ascode_pr.go deleted file mode 100644 index 6ea0797a81..0000000000 --- a/engine/api/ascode/ascode_pr.go +++ /dev/null @@ -1,168 +0,0 @@ -package ascode - -import ( - "context" - "time" - - "github.com/go-gorp/gorp" - - "github.com/ovh/cds/engine/api/cache" - "github.com/ovh/cds/engine/api/operation" - "github.com/ovh/cds/engine/api/repositoriesmanager" - "github.com/ovh/cds/sdk" - "github.com/ovh/cds/sdk/log" -) - -const ( - AsCodePipeline = "pipeline" - AsCodeWorkflow = "workflow" -) - -type EntityData struct { - Type string - ID int64 - Name string - FromRepo string - Operation *sdk.Operation -} - -// UpdateAsCodeResult pulls repositories operation and the create pullrequest + update workflow -func UpdateAsCodeResult(ctx context.Context, db *gorp.DbMap, store cache.Store, proj sdk.Project, app sdk.Application, ed EntityData, u sdk.Identifiable) *sdk.AsCodeEvent { - tick := time.NewTicker(2 * time.Second) - ctx, cancel := context.WithTimeout(ctx, 1*time.Minute) - defer func() { - cancel() - ed.Operation.RepositoryStrategy.SSHKeyContent = "" - _ = store.SetWithTTL(cache.Key(operation.CacheOperationKey, ed.Operation.UUID), ed.Operation, 300) - }() -forLoop: - for { - select { - case <-ctx.Done(): - ed.Operation.Status = sdk.OperationStatusError - ed.Operation.Error = "Unable to enable workflow as code" - return nil - case <-tick.C: - if err := operation.GetRepositoryOperation(ctx, db, ed.Operation); err != nil { - log.Error(ctx, "unable to get repository operation %s: %v", ed.Operation.UUID, err) - continue - } - - if ed.Operation.Status == sdk.OperationStatusError { - log.Error(ctx, "operation in error %s: %s", ed.Operation.UUID, ed.Operation.Error) - break forLoop - } - if ed.Operation.Status == sdk.OperationStatusDone { - vcsServer := repositoriesmanager.GetProjectVCSServer(proj, app.VCSServer) - if vcsServer == nil { - log.Error(ctx, "postWorkflowAsCodeHandler> No vcsServer found") - ed.Operation.Status = sdk.OperationStatusError - ed.Operation.Error = "No vcsServer found" - return nil - } - client, errclient := repositoriesmanager.AuthorizedClient(ctx, db, store, proj.Key, vcsServer) - if errclient != nil { - log.Error(ctx, "postWorkflowAsCodeHandler> unable to create repositories manager client: %v", errclient) - ed.Operation.Status = sdk.OperationStatusError - ed.Operation.Error = "unable to create repositories manager client" - return nil - } - - request := sdk.VCSPullRequest{ - Title: ed.Operation.Setup.Push.Message, - Head: sdk.VCSPushEvent{ - Branch: sdk.VCSBranch{ - DisplayID: ed.Operation.Setup.Push.FromBranch, - }, - Repo: app.RepositoryFullname, - }, - Base: sdk.VCSPushEvent{ - Branch: sdk.VCSBranch{ - DisplayID: ed.Operation.Setup.Push.ToBranch, - }, - Repo: app.RepositoryFullname, - }, - } - pr, err := client.PullRequestCreate(ctx, app.RepositoryFullname, request) - if err != nil { - log.Error(ctx, "postWorkflowAsCodeHandler> unable to create pull request: %v", err) - ed.Operation.Status = sdk.OperationStatusError - ed.Operation.Error = "unable to create pull request" - return nil - } - if pr.URL == "" { - prs, err := client.PullRequests(ctx, app.RepositoryFullname) - if err != nil { - log.Error(ctx, "postWorkflowAsCodeHandler> unable to list pull request: %v", err) - ed.Operation.Status = sdk.OperationStatusError - ed.Operation.Error = "unable to list pull request" - return nil - } - for _, prItem := range prs { - if prItem.Base.Branch.DisplayID == ed.Operation.Setup.Push.ToBranch && prItem.Head.Branch.DisplayID == ed.Operation.Setup.Push.FromBranch { - pr = prItem - break - } - } - } - ed.Operation.Setup.Push.PRLink = pr.URL - - // Find existing ascode event with this pullrequest - asCodeEvent, err := LoadAsCodeByPRID(ctx, db, int64(pr.ID)) - if err != nil && err != sdk.ErrNotFound { - log.Error(ctx, "UpdateAsCodeResult> unable to save pull request: %v", err) - return nil - } - if asCodeEvent.ID == 0 { - asCodeEvent = sdk.AsCodeEvent{ - PullRequestID: int64(pr.ID), - PullRequestURL: pr.URL, - Username: u.GetUsername(), - CreateDate: time.Now(), - FromRepo: ed.FromRepo, - Migrate: !ed.Operation.Setup.Push.Update, - } - } - - switch ed.Type { - case AsCodeWorkflow: - if asCodeEvent.Data.Workflows == nil { - asCodeEvent.Data.Workflows = make(map[int64]string) - } - found := false - for k := range asCodeEvent.Data.Workflows { - if k == ed.ID { - found = true - break - } - } - if !found { - asCodeEvent.Data.Workflows[ed.ID] = ed.Name - } - case AsCodePipeline: - if asCodeEvent.Data.Pipelines == nil { - asCodeEvent.Data.Pipelines = make(map[int64]string) - } - found := false - for k := range asCodeEvent.Data.Pipelines { - if k == ed.ID { - found = true - break - } - } - if !found { - asCodeEvent.Data.Pipelines[ed.ID] = ed.Name - } - } - if err := InsertOrUpdateAsCodeEvent(db, &asCodeEvent); err != nil { - log.Error(ctx, "postWorkflowAsCodeHandler> unable to insert as code event: %v", err) - ed.Operation.Status = sdk.OperationStatusError - ed.Operation.Error = "unable to insert as code event" - return nil - } - return &asCodeEvent - } - } - } - return nil -} diff --git a/engine/api/ascode/dao.go b/engine/api/ascode/dao.go index 494741932b..a841cad928 100644 --- a/engine/api/ascode/dao.go +++ b/engine/api/ascode/dao.go @@ -10,75 +10,70 @@ import ( "github.com/ovh/cds/sdk" ) -// LoadAsCodeByPRID Load as code events for the given pullrequest id -func LoadAsCodeByPRID(ctx context.Context, db gorp.SqlExecutor, ID int64) (sdk.AsCodeEvent, error) { - query := gorpmapping.NewQuery("SELECT * FROM as_code_events WHERE pullrequest_id = $1").Args(ID) - var event dbAsCodeEvents - if _, err := gorpmapping.Get(ctx, db, query, &event); err != nil { +// LoadEventByWorkflowIDAndPullRequest returns a as code event if exists for given workflow holder and pull request info. +func LoadEventByWorkflowIDAndPullRequest(ctx context.Context, db gorp.SqlExecutor, workflowID int64, pullRequestRepo string, pullRequestID int64) (*sdk.AsCodeEvent, error) { + query := gorpmapping.NewQuery(` + SELECT * + FROM as_code_events + WHERE workflow_id = $1 AND from_repository = $2 AND pullrequest_id = $3 + `).Args(workflowID, pullRequestRepo, pullRequestID) + var dbEvent dbAsCodeEvents + if _, err := gorpmapping.Get(ctx, db, query, &dbEvent); err != nil { if err == sql.ErrNoRows { - return sdk.AsCodeEvent{}, sdk.WithStack(sdk.ErrNotFound) + return nil, sdk.WithStack(sdk.ErrNotFound) } - return sdk.AsCodeEvent{}, sdk.WrapError(err, "Unable to load as code event") + return nil, sdk.WrapError(err, "unable to load as code event") } - return sdk.AsCodeEvent(event), nil + event := sdk.AsCodeEvent(dbEvent) + return &event, nil } -// LoadAsCodeEventByWorkflowID Load as code events for the given workflow -func LoadAsCodeEventByWorkflowID(ctx context.Context, db gorp.SqlExecutor, workflowID int64) ([]sdk.AsCodeEvent, error) { - query := gorpmapping.NewQuery("SELECT * FROM as_code_events where (data->'workflows')::jsonb ? $1").Args(workflowID) - var events []dbAsCodeEvents - if err := gorpmapping.GetAll(ctx, db, query, &events); err != nil { - return nil, sdk.WrapError(err, "Unable to load as code events") +// LoadEventsByWorkflowID returns as code events for the given workflow. +func LoadEventsByWorkflowID(ctx context.Context, db gorp.SqlExecutor, workflowID int64) ([]sdk.AsCodeEvent, error) { + query := gorpmapping.NewQuery(` + SELECT * + FROM as_code_events + WHERE workflow_id = $1 + `).Args(workflowID) + var dbEvents []dbAsCodeEvents + if err := gorpmapping.GetAll(ctx, db, query, &dbEvents); err != nil { + return nil, sdk.WrapError(err, "unable to load as code events") } - - asCodeEvents := make([]sdk.AsCodeEvent, len(events)) - for i := range events { - asCodeEvents[i] = sdk.AsCodeEvent(events[i]) - } - return asCodeEvents, nil -} - -// LoadAsCodeEventByRepo Load as code events for the given repo -func LoadAsCodeEventByRepo(ctx context.Context, db gorp.SqlExecutor, fromRepo string) ([]sdk.AsCodeEvent, error) { - query := gorpmapping.NewQuery("SELECT * FROM as_code_events where from_repository = $1").Args(fromRepo) - var events []dbAsCodeEvents - if err := gorpmapping.GetAll(ctx, db, query, &events); err != nil { - return nil, sdk.WrapError(err, "Unable to load as code events") - } - - asCodeEvents := make([]sdk.AsCodeEvent, len(events)) - for i := range events { - asCodeEvents[i] = sdk.AsCodeEvent(events[i]) + events := make([]sdk.AsCodeEvent, len(dbEvents)) + for i := range dbEvents { + events[i] = sdk.AsCodeEvent(dbEvents[i]) } - return asCodeEvents, nil + return events, nil } -func InsertOrUpdateAsCodeEvent(db gorp.SqlExecutor, asCodeEvent *sdk.AsCodeEvent) error { - if asCodeEvent.ID == 0 { - return insertAsCodeEvent(db, asCodeEvent) +// UpsertEvent insert or update given ascode event. +func UpsertEvent(db gorp.SqlExecutor, event *sdk.AsCodeEvent) error { + if event.ID == 0 { + return insertEvent(db, event) } - return updateAsCodeEvent(db, asCodeEvent) + return UpdateEvent(db, event) } -func insertAsCodeEvent(db gorp.SqlExecutor, asCodeEvent *sdk.AsCodeEvent) error { - dbEvent := dbAsCodeEvents(*asCodeEvent) +func insertEvent(db gorp.SqlExecutor, event *sdk.AsCodeEvent) error { + dbEvent := dbAsCodeEvents(*event) if err := gorpmapping.Insert(db, &dbEvent); err != nil { return sdk.WrapError(err, "unable to insert as code event") } - asCodeEvent.ID = dbEvent.ID + event.ID = dbEvent.ID return nil } -func updateAsCodeEvent(db gorp.SqlExecutor, asCodeEvent *sdk.AsCodeEvent) error { - dbEvent := dbAsCodeEvents(*asCodeEvent) +// UpdateEvent in database. +func UpdateEvent(db gorp.SqlExecutor, event *sdk.AsCodeEvent) error { + dbEvent := dbAsCodeEvents(*event) if err := gorpmapping.Update(db, &dbEvent); err != nil { return sdk.WrapError(err, "unable to update as code event") } return nil } -func DeleteAsCodeEvent(db gorp.SqlExecutor, asCodeEvent sdk.AsCodeEvent) error { - dbEvent := dbAsCodeEvents(asCodeEvent) +func deleteEvent(db gorp.SqlExecutor, event *sdk.AsCodeEvent) error { + dbEvent := dbAsCodeEvents(*event) if err := gorpmapping.Delete(db, &dbEvent); err != nil { return sdk.WrapError(err, "unable to delete as code event") } diff --git a/engine/api/ascode/pull_request.go b/engine/api/ascode/pull_request.go new file mode 100644 index 0000000000..ff7ad3f1ea --- /dev/null +++ b/engine/api/ascode/pull_request.go @@ -0,0 +1,194 @@ +package ascode + +import ( + "context" + "fmt" + "time" + + "github.com/go-gorp/gorp" + "github.com/sirupsen/logrus" + + "github.com/ovh/cds/engine/api/cache" + "github.com/ovh/cds/engine/api/operation" + "github.com/ovh/cds/engine/api/repositoriesmanager" + "github.com/ovh/cds/sdk" + "github.com/ovh/cds/sdk/log" +) + +// EventType type for as code events. +type EventType string + +// AsCodeEventType values. +const ( + PipelineEvent EventType = "pipeline" + WorkflowEvent EventType = "workflow" +) + +type EntityData struct { + Type EventType + ID int64 + Name string + FromRepo string + OperationUUID string +} + +// UpdateAsCodeResult pulls repositories operation and the create pullrequest + update workflow +func UpdateAsCodeResult(ctx context.Context, db *gorp.DbMap, store cache.Store, proj sdk.Project, workflowHolderID int64, rootApp sdk.Application, ed EntityData, u sdk.Identifiable) *sdk.AsCodeEvent { + tick := time.NewTicker(2 * time.Second) + ctx, cancel := context.WithTimeout(ctx, 1*time.Minute) + defer cancel() + + var asCodeEvent *sdk.AsCodeEvent + globalOperation := sdk.Operation{ + UUID: ed.OperationUUID, + } + var globalErr error + +forLoop: + for { + select { + case <-ctx.Done(): + globalErr = sdk.NewErrorFrom(sdk.ErrRepoOperationTimeout, "updating repository take too much time") + break forLoop + case <-tick.C: + ope, err := operation.GetRepositoryOperation(ctx, db, ed.OperationUUID) + if err != nil { + globalErr = sdk.NewErrorFrom(err, "unable to get repository operation %s", ed.OperationUUID) + break forLoop + } + + if ope.Status == sdk.OperationStatusError { + globalErr = sdk.NewErrorFrom(sdk.ErrUnknownError, "repository operation in error: %s", ope.Error) + break forLoop + } + if ope.Status == sdk.OperationStatusDone { + ae, err := createPullRequest(ctx, db, store, proj, workflowHolderID, rootApp, ed, u, ope.Setup) + if err != nil { + globalErr = err + break forLoop + } + asCodeEvent = ae + globalOperation.Status = sdk.OperationStatusDone + globalOperation.Setup.Push.PRLink = ae.PullRequestURL + break forLoop + } + } + } + if globalErr != nil { + httpErr := sdk.ExtractHTTPError(globalErr, "") + isErrWithStack := sdk.IsErrorWithStack(globalErr) + fields := logrus.Fields{} + if isErrWithStack { + fields["stack_trace"] = fmt.Sprintf("%+v", globalErr) + } + log.ErrorWithFields(ctx, fields, "%s", globalErr) + + globalOperation.Status = sdk.OperationStatusError + globalOperation.Error = httpErr.Error() + } + + _ = store.SetWithTTL(cache.Key(operation.CacheOperationKey, globalOperation.UUID), globalOperation, 300) + + return asCodeEvent +} + +func createPullRequest(ctx context.Context, db *gorp.DbMap, store cache.Store, proj sdk.Project, workflowHolderID int64, rootApp sdk.Application, ed EntityData, u sdk.Identifiable, opeSetup sdk.OperationSetup) (*sdk.AsCodeEvent, error) { + vcsServer := repositoriesmanager.GetProjectVCSServer(proj, rootApp.VCSServer) + if vcsServer == nil { + return nil, sdk.NewErrorFrom(sdk.ErrNotFound, "no vcs server found on application %s", rootApp.Name) + } + client, err := repositoriesmanager.AuthorizedClient(ctx, db, store, proj.Key, vcsServer) + if err != nil { + return nil, sdk.NewErrorFrom(err, "unable to create repositories manager client") + } + + request := sdk.VCSPullRequest{ + Title: opeSetup.Push.Message, + Head: sdk.VCSPushEvent{ + Branch: sdk.VCSBranch{ + DisplayID: opeSetup.Push.FromBranch, + }, + Repo: rootApp.RepositoryFullname, + }, + Base: sdk.VCSPushEvent{ + Branch: sdk.VCSBranch{ + DisplayID: opeSetup.Push.ToBranch, + }, + Repo: rootApp.RepositoryFullname, + }, + } + + // Try to reuse a PR for the branche if exists else create a new one + var pr *sdk.VCSPullRequest + prs, err := client.PullRequests(ctx, rootApp.RepositoryFullname, sdk.VCSRequestModifierWithState(sdk.VCSPullRequestStateOpen)) + if err != nil { + return nil, sdk.NewErrorFrom(err, "unable to list pull request") + } + for _, prItem := range prs { + if prItem.Base.Branch.DisplayID == opeSetup.Push.ToBranch && prItem.Head.Branch.DisplayID == opeSetup.Push.FromBranch { + pr = &prItem + break + } + } + if pr == nil { + newPR, err := client.PullRequestCreate(ctx, rootApp.RepositoryFullname, request) + if err != nil { + return nil, sdk.NewErrorFrom(err, "unable to create pull request") + } + pr = &newPR + } + + // Find existing ascode event with this pull request info + asCodeEvent, err := LoadEventByWorkflowIDAndPullRequest(ctx, db, workflowHolderID, rootApp.RepositoryFullname, int64(pr.ID)) + if err != nil && sdk.ErrorIs(err, sdk.ErrNotFound) { + return nil, sdk.NewErrorFrom(err, "unable to save pull request") + } + if asCodeEvent.ID == 0 { + asCodeEvent = &sdk.AsCodeEvent{ + WorkflowID: workflowHolderID, + FromRepo: ed.FromRepo, + PullRequestID: int64(pr.ID), + PullRequestURL: pr.URL, + Username: u.GetUsername(), + CreateDate: time.Now(), + Migrate: !opeSetup.Push.Update, + } + } + + switch ed.Type { + case WorkflowEvent: + if asCodeEvent.Data.Workflows == nil { + asCodeEvent.Data.Workflows = make(map[int64]string) + } + found := false + for k := range asCodeEvent.Data.Workflows { + if k == ed.ID { + found = true + break + } + } + if !found { + asCodeEvent.Data.Workflows[ed.ID] = ed.Name + } + case PipelineEvent: + if asCodeEvent.Data.Pipelines == nil { + asCodeEvent.Data.Pipelines = make(map[int64]string) + } + found := false + for k := range asCodeEvent.Data.Pipelines { + if k == ed.ID { + found = true + break + } + } + if !found { + asCodeEvent.Data.Pipelines[ed.ID] = ed.Name + } + } + + if err := UpsertEvent(db, asCodeEvent); err != nil { + return nil, sdk.NewErrorFrom(err, "unable to insert as code event") + } + + return asCodeEvent, nil +} diff --git a/engine/api/ascode/sync.go b/engine/api/ascode/sync.go new file mode 100644 index 0000000000..e031c7a07c --- /dev/null +++ b/engine/api/ascode/sync.go @@ -0,0 +1,106 @@ +package ascode + +import ( + "context" + + "github.com/go-gorp/gorp" + + "github.com/ovh/cds/engine/api/cache" + "github.com/ovh/cds/engine/api/event" + "github.com/ovh/cds/engine/api/repositoriesmanager" + "github.com/ovh/cds/sdk" + "github.com/ovh/cds/sdk/log" +) + +type SyncResult struct { + FromRepository string + Merged bool +} + +// SyncEvents checks if workflow as to become ascode. +func SyncEvents(ctx context.Context, db *gorp.DbMap, store cache.Store, proj sdk.Project, workflowHolder sdk.Workflow, u sdk.Identifiable) (SyncResult, error) { + var res SyncResult + + if workflowHolder.WorkflowData.Node.Context.ApplicationID == 0 { + return res, sdk.NewErrorFrom(sdk.ErrWrongRequest, "no application found on the root node of the workflow") + } + rootApp := workflowHolder.Applications[workflowHolder.WorkflowData.Node.Context.ApplicationID] + + vcsServer := repositoriesmanager.GetProjectVCSServer(proj, rootApp.VCSServer) + if vcsServer == nil { + return res, sdk.NewErrorFrom(sdk.ErrNotFound, "no vcs server found on application %s", rootApp.Name) + } + client, err := repositoriesmanager.AuthorizedClient(ctx, db, store, proj.Key, vcsServer) + if err != nil { + return res, err + } + + fromRepo := rootApp.FromRepository + if fromRepo == "" { + repo, err := client.RepoByFullname(ctx, rootApp.RepositoryFullname) + if err != nil { + return res, sdk.WrapError(err, "cannot get repo %s", rootApp.RepositoryFullname) + } + if rootApp.RepositoryStrategy.ConnectionType == "ssh" { + fromRepo = repo.SSHCloneURL + } else { + fromRepo = repo.HTTPCloneURL + } + } + res.FromRepository = fromRepo + + tx, err := db.Begin() + if err != nil { + return res, sdk.WithStack(err) + } + defer tx.Rollback() //nolint + + asCodeEvents, err := LoadEventsByWorkflowID(ctx, tx, workflowHolder.ID) + if err != nil { + return res, err + } + + eventLeft := make([]sdk.AsCodeEvent, 0) + eventToDelete := make([]sdk.AsCodeEvent, 0) + for _, ascodeEvt := range asCodeEvents { + pr, err := client.PullRequest(ctx, rootApp.RepositoryFullname, int(ascodeEvt.PullRequestID)) + if err != nil && !sdk.ErrorIs(err, sdk.ErrNotFound) { + return res, sdk.WrapError(err, "unable to check pull request status") + } + prNotFound := sdk.ErrorIs(err, sdk.ErrNotFound) + + if prNotFound { + log.Debug("Pull request %s #%d not found", rootApp.RepositoryFullname, int(ascodeEvt.PullRequestID)) + } + + // If the PR was merged we want to set the repo url on the workflow + if ascodeEvt.Migrate && len(ascodeEvt.Data.Workflows) == 1 { + if pr.Merged { + res.Merged = true + } + } + + // If event ended, delete it from db + if prNotFound || pr.Merged || pr.Closed { + eventToDelete = append(eventToDelete, ascodeEvt) + } else { + eventLeft = append(eventLeft, ascodeEvt) + } + } + + for _, ascodeEvt := range eventToDelete { + if err := deleteEvent(tx, &ascodeEvt); err != nil { + return res, err + } + } + + if err := tx.Commit(); err != nil { + return res, sdk.WithStack(err) + } + + for _, ed := range eventToDelete { + event.PublishAsCodeEvent(ctx, proj.Key, ed, u) + } + + return res, nil +} diff --git a/engine/api/ascode/sync/sync.go b/engine/api/ascode/sync/sync.go deleted file mode 100644 index 5d44971d59..0000000000 --- a/engine/api/ascode/sync/sync.go +++ /dev/null @@ -1,99 +0,0 @@ -package sync - -import ( - "context" - - "github.com/go-gorp/gorp" - - "github.com/ovh/cds/engine/api/ascode" - "github.com/ovh/cds/engine/api/cache" - "github.com/ovh/cds/engine/api/event" - "github.com/ovh/cds/engine/api/repositoriesmanager" - "github.com/ovh/cds/engine/api/workflow" - "github.com/ovh/cds/sdk" - "github.com/ovh/cds/sdk/log" -) - -// SyncAsCodeEvent checks if workflow as to become ascode -func SyncAsCodeEvent(ctx context.Context, db *gorp.DbMap, store cache.Store, proj sdk.Project, app sdk.Application, u sdk.Identifiable) ([]sdk.AsCodeEvent, string, error) { - vcsServer := repositoriesmanager.GetProjectVCSServer(proj, app.VCSServer) - if vcsServer == nil { - return nil, "", sdk.NewErrorFrom(sdk.ErrNotFound, "no vcs server found on application %s", app.Name) - } - client, err := repositoriesmanager.AuthorizedClient(ctx, db, store, proj.Key, vcsServer) - if err != nil { - return nil, "", err - } - - fromRepo := app.FromRepository - if fromRepo == "" { - repo, err := client.RepoByFullname(ctx, app.RepositoryFullname) - if err != nil { - return nil, fromRepo, sdk.WrapError(err, "cannot get repo %s", app.RepositoryFullname) - } - if app.RepositoryStrategy.ConnectionType == "ssh" { - fromRepo = repo.SSHCloneURL - } else { - fromRepo = repo.HTTPCloneURL - } - } - - tx, err := db.Begin() - if err != nil { - return nil, fromRepo, sdk.WithStack(err) - } - defer tx.Rollback() //nolint - - asCodeEvents, err := ascode.LoadAsCodeEventByRepo(ctx, tx, fromRepo) - if err != nil { - return nil, fromRepo, err - } - - eventLeft := make([]sdk.AsCodeEvent, 0) - eventToDelete := make([]sdk.AsCodeEvent, 0) - for _, ascodeEvt := range asCodeEvents { - pr, err := client.PullRequest(ctx, app.RepositoryFullname, int(ascodeEvt.PullRequestID)) - if err != nil && !sdk.ErrorIs(err, sdk.ErrNotFound) { - return nil, fromRepo, sdk.WrapError(err, "unable to check pull request status") - } - prNotFound := sdk.ErrorIs(err, sdk.ErrNotFound) - - if prNotFound { - log.Debug("Pull request %s #%d not found", app.RepositoryFullname, int(ascodeEvt.PullRequestID)) - } - - // If the PR was merged we want to set the repo url on the workflow - if ascodeEvt.Migrate && len(ascodeEvt.Data.Workflows) == 1 { - for id := range ascodeEvt.Data.Workflows { - if pr.Merged { - if err := workflow.UpdateFromRepository(tx, id, fromRepo); err != nil { - return nil, fromRepo, err - } - } - } - } - - // If event ended, delete it from db - if prNotFound || pr.Merged || pr.Closed { - eventToDelete = append(eventToDelete, ascodeEvt) - } else { - eventLeft = append(eventLeft, ascodeEvt) - } - } - - for _, ascodeEvt := range eventToDelete { - if err := ascode.DeleteAsCodeEvent(tx, ascodeEvt); err != nil { - return nil, fromRepo, err - } - } - - if err := tx.Commit(); err != nil { - return nil, fromRepo, sdk.WithStack(err) - } - - for _, ed := range eventToDelete { - event.PublishAsCodeEvent(ctx, proj.Key, ed, u) - } - - return eventLeft, fromRepo, nil -} diff --git a/engine/api/ascode_test.go b/engine/api/ascode_test.go index 6aa7f717e6..d5f69c3f14 100644 --- a/engine/api/ascode_test.go +++ b/engine/api/ascode_test.go @@ -316,13 +316,6 @@ func Test_postResyncPRAsCodeHandler(t *testing.T) { pkey := sdk.RandomString(10) p := assets.InsertTestProject(t, db, api.Cache, pkey, pkey) - // Clean as code event - as, err := ascode.LoadAsCodeEventByRepo(context.TODO(), db, repoURL) - assert.NoError(t, err) - for _, a := range as { - assert.NoError(t, ascode.DeleteAsCodeEvent(db, a)) - } - assert.NoError(t, repositoriesmanager.InsertForProject(db, p, &sdk.ProjectVCSServer{ Name: "github", Data: map[string]string{ @@ -417,6 +410,7 @@ vcs_ssh_key: proj-blabla // Add some events to resync asCodeEvent := sdk.AsCodeEvent{ + WorkflowID: wf.ID, Username: u.GetUsername(), CreateDate: time.Now(), FromRepo: repoURL, @@ -435,10 +429,11 @@ vcs_ssh_key: proj-blabla }, }, } - assert.NoError(t, ascode.InsertOrUpdateAsCodeEvent(db, &asCodeEvent)) + assert.NoError(t, ascode.UpsertEvent(db, &asCodeEvent)) - uri := api.Router.GetRoute("POST", api.postResyncPRAsCodeHandler, map[string]string{ - "key": pkey, + uri := api.Router.GetRoute("POST", api.postWorkflowAsCodeEventsResyncHandler, map[string]string{ + "key": pkey, + "permWorkflowName": wf.Name, }) uri = fmt.Sprintf("%s?repo=%s", uri, repoURL) @@ -453,7 +448,7 @@ vcs_ssh_key: proj-blabla t.Logf(w.Body.String()) // Check there is no more events in db - assDB, err := ascode.LoadAsCodeEventByRepo(context.TODO(), db, repoURL) + assDB, err := ascode.LoadEventsByWorkflowID(context.TODO(), db, wf.ID) assert.NoError(t, err) assert.Equal(t, 0, len(assDB)) diff --git a/engine/api/migrate/refactor_ascode_events_workflow_holder.go b/engine/api/migrate/refactor_ascode_events_workflow_holder.go new file mode 100644 index 0000000000..4bc738479b --- /dev/null +++ b/engine/api/migrate/refactor_ascode_events_workflow_holder.go @@ -0,0 +1,113 @@ +package migrate + +import ( + "context" + "database/sql" + + "github.com/go-gorp/gorp" + + "github.com/ovh/cds/engine/api/ascode" + "github.com/ovh/cds/sdk" + "github.com/ovh/cds/sdk/log" +) + +func RefactorAsCodeEventsWorkflowHolder(ctx context.Context, db *gorp.DbMap) error { + query := "SELECT id FROM as_code_events WHERE workflow_id IS NULL" + rows, err := db.Query(query) + if err == sql.ErrNoRows { + return nil + } + if err != nil { + return sdk.WithStack(err) + } + + var ids []int64 + for rows.Next() { + var id int64 + if err := rows.Scan(&id); err != nil { + rows.Close() // nolint + return sdk.WithStack(err) + } + ids = append(ids, id) + } + + if err := rows.Close(); err != nil { + return sdk.WithStack(err) + } + + var mError = new(sdk.MultiError) + for _, id := range ids { + if err := refactorAsCodeEventsWorkflowHolder(ctx, db, id); err != nil { + mError.Append(err) + log.Error(ctx, "migrate.RefactorAsCodeEventsWorkflowHolder> unable to migrate as_code_event %d: %v", id, err) + } + } + + if mError.IsEmpty() { + return nil + } + return mError +} + +func refactorAsCodeEventsWorkflowHolder(ctx context.Context, db *gorp.DbMap, eventID int64) error { + log.Info(ctx, "migrate.refactorAsCodeEventsWorkflowHolder> as_code_event %d migration begin", eventID) + + tx, err := db.Begin() + if err != nil { + return sdk.WithStack(err) + } + + queryEvent := ` + SELECT id, pullrequest_id, pullrequest_url, username, creation_date, from_repository, migrate, data + FROM as_code_events + WHERE id = $1 + AND workflow_id IS NULL + FOR UPDATE SKIP LOCKED + ` + + defer tx.Rollback() // nolint + + var asCodeEvent sdk.AsCodeEvent + if err := tx.QueryRow(queryEvent, eventID).Scan( + &asCodeEvent.ID, + &asCodeEvent.PullRequestID, + &asCodeEvent.PullRequestURL, + &asCodeEvent.Username, + &asCodeEvent.CreateDate, + &asCodeEvent.FromRepo, + &asCodeEvent.Migrate, + &asCodeEvent.Data, + ); err != nil { + if err == sql.ErrNoRows { + return nil + } + return sdk.WrapError(err, "unable to select and lock as code event with id: %d", eventID) + } + + queryWorkflow := ` + SELECT id + FROM workflow + WHERE from_repository = $1 + LIMIT 1 + ` + + if err := tx.QueryRow(queryWorkflow, asCodeEvent.FromRepo).Scan( + &asCodeEvent.WorkflowID, + ); err != nil { + if err == sql.ErrNoRows { + return nil + } + return sdk.WrapError(err, "unable to select a workflow that match event repository address: %s for event %d", asCodeEvent.FromRepo, eventID) + } + + if err := ascode.UpdateEvent(tx, &asCodeEvent); err != nil { + return sdk.WithStack(err) + } + + if err := tx.Commit(); err != nil { + return sdk.WithStack(err) + } + + log.Info(ctx, "migrate.refactorAsCodeEventsWorkflowHolder> as_code_event %d migration end", eventID) + return nil +} diff --git a/engine/api/operation/operation.go b/engine/api/operation/operation.go index c85b7a81cb..d892dbbd4c 100644 --- a/engine/api/operation/operation.go +++ b/engine/api/operation/operation.go @@ -17,55 +17,35 @@ import ( var CacheOperationKey = cache.Key("repositories", "operation", "push") -func PushOperation(ctx context.Context, db gorp.SqlExecutor, store cache.Store, proj sdk.Project, wp exportentities.WorkflowComponents, vcsServerName, repoFullname, branch, message string, vcsStrategy sdk.RepositoryStrategy, isUpdate bool, u sdk.Identifiable) (*sdk.Operation, error) { - if vcsStrategy.SSHKey != "" { - key := proj.GetSSHKey(vcsStrategy.SSHKey) +func pushOperation(ctx context.Context, db gorp.SqlExecutor, store cache.Store, proj sdk.Project, data exportentities.WorkflowComponents, ope sdk.Operation) (*sdk.Operation, error) { + if ope.RepositoryStrategy.SSHKey != "" { + key := proj.GetSSHKey(ope.RepositoryStrategy.SSHKey) if key == nil { - return nil, sdk.WithStack(fmt.Errorf("unable to find key %s on project %s", vcsStrategy.SSHKey, proj.Key)) + return nil, sdk.WithStack(fmt.Errorf("unable to find key %s on project %s", ope.RepositoryStrategy.SSHKey, proj.Key)) } - vcsStrategy.SSHKeyContent = key.Private + ope.RepositoryStrategy.SSHKeyContent = key.Private } - // Create VCS Operation - ope := sdk.Operation{ - VCSServer: vcsServerName, - RepoFullName: repoFullname, - URL: "", - RepositoryStrategy: vcsStrategy, - Setup: sdk.OperationSetup{ - Push: sdk.OperationPush{ - FromBranch: branch, - Message: message, - Update: isUpdate, - }, - }, - } - ope.User.Email = u.GetEmail() - ope.User.Username = u.GetFullname() - ope.User.Username = u.GetUsername() - - vcsServer := repositoriesmanager.GetProjectVCSServer(proj, vcsServerName) + vcsServer := repositoriesmanager.GetProjectVCSServer(proj, ope.VCSServer) if vcsServer == nil { - return nil, sdk.WithStack(fmt.Errorf("no vcsServer found")) + return nil, sdk.NewErrorFrom(sdk.ErrNotFound, "no vcs server found on project %s for given name %s", proj.Key, ope.VCSServer) } - client, errC := repositoriesmanager.AuthorizedClient(ctx, db, store, proj.Key, vcsServer) - if errC != nil { - return nil, errC + client, err := repositoriesmanager.AuthorizedClient(ctx, db, store, proj.Key, vcsServer) + if err != nil { + return nil, err } - - repo, errR := client.RepoByFullname(ctx, repoFullname) - if errR != nil { - return nil, sdk.WrapError(errR, "cannot get repo %s", repoFullname) + repo, err := client.RepoByFullname(ctx, ope.RepoFullName) + if err != nil { + return nil, sdk.WrapError(err, "cannot get repo %s", ope.RepoFullName) } - - if vcsStrategy.ConnectionType == "ssh" { + if ope.RepositoryStrategy.ConnectionType == "ssh" { ope.URL = repo.SSHCloneURL } else { ope.URL = repo.HTTPCloneURL } buf := new(bytes.Buffer) - if err := exportentities.TarWorkflowComponents(ctx, wp, buf); err != nil { + if err := exportentities.TarWorkflowComponents(ctx, data, buf); err != nil { return nil, sdk.WrapError(err, "cannot tar pulled workflow") } @@ -76,11 +56,51 @@ func PushOperation(ctx context.Context, db gorp.SqlExecutor, store cache.Store, if err := PostRepositoryOperation(ctx, db, proj, &ope, multipartData); err != nil { return nil, sdk.WrapError(err, "unable to post repository operation") } + ope.RepositoryStrategy.SSHKeyContent = "" _ = store.SetWithTTL(cache.Key(CacheOperationKey, ope.UUID), ope, 300) return &ope, nil } +func PushOperation(ctx context.Context, db gorp.SqlExecutor, store cache.Store, proj sdk.Project, data exportentities.WorkflowComponents, vcsServerName, repoFullname, branch, message string, vcsStrategy sdk.RepositoryStrategy, u sdk.Identifiable) (*sdk.Operation, error) { + ope := sdk.Operation{ + VCSServer: vcsServerName, + RepoFullName: repoFullname, + RepositoryStrategy: vcsStrategy, + Setup: sdk.OperationSetup{ + Push: sdk.OperationPush{ + FromBranch: branch, + Message: message, + }, + }, + } + ope.User.Email = u.GetEmail() + ope.User.Fullname = u.GetFullname() + ope.User.Username = u.GetUsername() + + return pushOperation(ctx, db, store, proj, data, ope) +} + +func PushOperationUpdate(ctx context.Context, db gorp.SqlExecutor, store cache.Store, proj sdk.Project, data exportentities.WorkflowComponents, vcsServerName, repoFullname, branch, message string, vcsStrategy sdk.RepositoryStrategy, u sdk.Identifiable) (*sdk.Operation, error) { + ope := sdk.Operation{ + VCSServer: vcsServerName, + RepoFullName: repoFullname, + RepositoryStrategy: vcsStrategy, + Setup: sdk.OperationSetup{ + Push: sdk.OperationPush{ + FromBranch: branch, + Message: message, + Update: true, + }, + }, + } + ope.User.Email = u.GetEmail() + ope.User.Fullname = u.GetFullname() + ope.User.Username = u.GetUsername() + + return pushOperation(ctx, db, store, proj, data, ope) +} + // PostRepositoryOperation creates a new repository operation func PostRepositoryOperation(ctx context.Context, db gorp.SqlExecutor, prj sdk.Project, ope *sdk.Operation, multipartData *services.MultiPartData) error { srvs, err := services.LoadAllByType(ctx, db, services.TypeRepositories) @@ -114,20 +134,20 @@ func PostRepositoryOperation(ctx context.Context, db gorp.SqlExecutor, prj sdk.P return nil } if _, err := services.NewClient(db, srvs).DoMultiPartRequest(ctx, http.MethodPost, "/operations", multipartData, ope, ope); err != nil { - return sdk.WrapError(err, "Unable to perform multipart operation") + return sdk.WrapError(err, "unable to perform multipart operation") } return nil } -// GetRepositoryOperation get repository operation status -func GetRepositoryOperation(ctx context.Context, db gorp.SqlExecutor, ope *sdk.Operation) error { +// GetRepositoryOperation get repository operation status. +func GetRepositoryOperation(ctx context.Context, db gorp.SqlExecutor, uuid string) (*sdk.Operation, error) { srvs, err := services.LoadAllByType(ctx, db, services.TypeRepositories) if err != nil { - return sdk.WrapError(err, "Unable to found repositories service") + return nil, sdk.WrapError(err, "unable to found repositories service") } - - if _, _, err := services.NewClient(db, srvs).DoJSONRequest(ctx, http.MethodGet, "/operations/"+ope.UUID, nil, ope); err != nil { - return sdk.WrapError(err, "Unable to get operation") + var ope sdk.Operation + if _, _, err := services.NewClient(db, srvs).DoJSONRequest(ctx, http.MethodGet, "/operations/"+uuid, nil, &ope); err != nil { + return nil, sdk.WrapError(err, "unable to get operation") } - return nil + return &ope, nil } diff --git a/engine/api/pipeline.go b/engine/api/pipeline.go index 126a83dfb3..0032963a06 100644 --- a/engine/api/pipeline.go +++ b/engine/api/pipeline.go @@ -12,11 +12,13 @@ import ( "github.com/ovh/cds/engine/api/application" "github.com/ovh/cds/engine/api/ascode" "github.com/ovh/cds/engine/api/event" + "github.com/ovh/cds/engine/api/operation" "github.com/ovh/cds/engine/api/pipeline" "github.com/ovh/cds/engine/api/project" "github.com/ovh/cds/engine/api/workflow" "github.com/ovh/cds/engine/service" "github.com/ovh/cds/sdk" + "github.com/ovh/cds/sdk/exportentities" "github.com/ovh/cds/sdk/log" ) @@ -81,20 +83,26 @@ func (api *API) updateAsCodePipelineHandler() service.Handler { } u := getAPIConsumer(ctx) - ope, err := pipeline.UpdatePipelineAsCode(ctx, api.Cache, api.mustDB(), *proj, p, rootApp.VCSServer, rootApp.RepositoryFullname, branch, message, rootApp.RepositoryStrategy, u) + + wpi := exportentities.NewPipelineV1(p) + wp := exportentities.WorkflowComponents{ + Pipelines: []exportentities.PipelineV1{wpi}, + } + + ope, err := operation.PushOperationUpdate(ctx, api.mustDB(), api.Cache, *proj, wp, rootApp.VCSServer, rootApp.RepositoryFullname, branch, message, rootApp.RepositoryStrategy, u) if err != nil { return err } sdk.GoRoutine(context.Background(), fmt.Sprintf("UpdateAsCodePipelineHandler-%s", ope.UUID), func(ctx context.Context) { ed := ascode.EntityData{ - FromRepo: pipelineDB.FromRepository, - Type: ascode.AsCodePipeline, - ID: pipelineDB.ID, - Name: pipelineDB.Name, - Operation: ope, + FromRepo: pipelineDB.FromRepository, + Type: ascode.PipelineEvent, + ID: pipelineDB.ID, + Name: pipelineDB.Name, + OperationUUID: ope.UUID, } - asCodeEvent := ascode.UpdateAsCodeResult(ctx, api.mustDB(), api.Cache, *proj, *rootApp, ed, u) + asCodeEvent := ascode.UpdateAsCodeResult(ctx, api.mustDB(), api.Cache, *proj, wkHolder.ID, *rootApp, ed, u) if asCodeEvent != nil { event.PublishAsCodeEvent(ctx, proj.Key, *asCodeEvent, u) } @@ -314,23 +322,6 @@ func (api *API) getPipelineHandler() service.Handler { return sdk.WrapError(err, "cannot load pipeline %s", pipelineName) } - if withAsCodeEvent { - events, errE := ascode.LoadAsCodeEventByRepo(ctx, api.mustDB(), p.FromRepository) - if errE != nil { - return errE - } - p.AsCodeEvents = events - } - - if withWorkflows { - wf, errW := workflow.LoadByPipelineName(ctx, api.mustDB(), projectKey, pipelineName) - if errW != nil { - return sdk.WrapError(errW, "getPipelineHandler> Cannot load workflows using pipeline %s", p.Name) - } - p.Usage = &sdk.Usage{} - p.Usage.Workflows = wf - } - if p.FromRepository != "" { wkAscodeHolder, err := workflow.LoadByRepo(ctx, api.Cache, api.mustDB(), *proj, p.FromRepository, workflow.LoadOptions{ WithTemplate: true, @@ -346,6 +337,23 @@ func (api *API) getPipelineHandler() service.Handler { } } + if withAsCodeEvent && p.WorkflowAscodeHolder != nil { + events, err := ascode.LoadEventsByWorkflowID(ctx, api.mustDB(), p.WorkflowAscodeHolder.ID) + if err != nil { + return err + } + p.AsCodeEvents = events + } + + if withWorkflows { + wf, err := workflow.LoadByPipelineName(ctx, api.mustDB(), projectKey, pipelineName) + if err != nil { + return sdk.WrapError(err, "cannot load workflows using pipeline %s", p.Name) + } + p.Usage = &sdk.Usage{} + p.Usage.Workflows = wf + } + return service.WriteJSON(w, p, http.StatusOK) } } diff --git a/engine/api/pipeline/pipeline_ascode.go b/engine/api/pipeline/pipeline_ascode.go deleted file mode 100644 index d7ddc90b88..0000000000 --- a/engine/api/pipeline/pipeline_ascode.go +++ /dev/null @@ -1,21 +0,0 @@ -package pipeline - -import ( - "context" - - "github.com/go-gorp/gorp" - - "github.com/ovh/cds/engine/api/cache" - "github.com/ovh/cds/engine/api/operation" - "github.com/ovh/cds/sdk" - "github.com/ovh/cds/sdk/exportentities" -) - -// UpdateWorkflowAsCode update an as code workflow -func UpdatePipelineAsCode(ctx context.Context, store cache.Store, db gorp.SqlExecutor, proj sdk.Project, p sdk.Pipeline, vcsServerName, repoFullname, branch, message string, vcsStrategy sdk.RepositoryStrategy, u sdk.Identifiable) (*sdk.Operation, error) { - wpi := exportentities.NewPipelineV1(p) - wp := exportentities.WorkflowComponents{ - Pipelines: []exportentities.PipelineV1{wpi}, - } - return operation.PushOperation(ctx, db, store, proj, wp, vcsServerName, repoFullname, branch, message, vcsStrategy, true, u) -} diff --git a/engine/api/pipeline_test.go b/engine/api/pipeline_test.go index 3e75435566..85e474bf4e 100644 --- a/engine/api/pipeline_test.go +++ b/engine/api/pipeline_test.go @@ -92,6 +92,11 @@ func TestUpdateAsCodePipelineHandler(t *testing.T) { if err := enc.Encode(hook); err != nil { return writeError(w, err) } + case "/vcs/github/repos/foo/myrepo/pullrequests?state=open": + vcsPRs := []sdk.VCSPullRequest{} + if err := enc.Encode(vcsPRs); err != nil { + return writeError(w, err) + } case "/vcs/github/repos/foo/myrepo/pullrequests": vcsPR := sdk.VCSPullRequest{ URL: "myURL", diff --git a/engine/api/repositoriesmanager/events.go b/engine/api/repositoriesmanager/events.go index e47ab7b464..10e89db786 100644 --- a/engine/api/repositoriesmanager/events.go +++ b/engine/api/repositoriesmanager/events.go @@ -50,7 +50,7 @@ func RetryEvent(e *sdk.Event, err error, store cache.Store) error { } func processEvent(ctx context.Context, db *gorp.DbMap, event sdk.Event, store cache.Store) error { - var c sdk.VCSAuthorizedClient + var c sdk.VCSAuthorizedClientService var errC error if event.EventType != fmt.Sprintf("%T", sdk.EventRunWorkflowNode{}) { @@ -70,8 +70,8 @@ func processEvent(ctx context.Context, db *gorp.DbMap, event sdk.Event, store ca return fmt.Errorf("repositoriesmanager>processEvent> AuthorizedClient (%s, %s) > err:%s", event.ProjectKey, eventWNR.RepositoryManagerName, err) } - c, errC = AuthorizedClient(ctx, db, store, event.ProjectKey, vcsServer) - if errC != nil { + c, err = AuthorizedClient(ctx, db, store, event.ProjectKey, vcsServer) + if err != nil { return fmt.Errorf("repositoriesmanager>processEvent> AuthorizedClient (%s, %s) > err:%s", event.ProjectKey, eventWNR.RepositoryManagerName, errC) } diff --git a/engine/api/repositoriesmanager/repositories_manager.go b/engine/api/repositoriesmanager/repositories_manager.go index a711dd3fdd..9083a2f2b8 100644 --- a/engine/api/repositoriesmanager/repositories_manager.go +++ b/engine/api/repositoriesmanager/repositories_manager.go @@ -131,7 +131,7 @@ func GetReposForProjectVCSServer(ctx context.Context, db gorp.SqlExecutor, store } // NewVCSServerConsumer returns a sdk.VCSServer wrapping vcs µServices calls -func NewVCSServerConsumer(dbFunc func(ctx context.Context) *gorp.DbMap, store cache.Store, name string) (sdk.VCSServer, error) { +func NewVCSServerConsumer(dbFunc func(ctx context.Context) *gorp.DbMap, store cache.Store, name string) (sdk.VCSServerService, error) { return &vcsConsumer{name: name, dbFunc: dbFunc}, nil } @@ -173,7 +173,7 @@ func (c *vcsConsumer) AuthorizeToken(ctx context.Context, token string, secret s return res["token"], res["secret"], nil } -func (c *vcsConsumer) GetAuthorizedClient(ctx context.Context, token, secret string, created int64) (sdk.VCSAuthorizedClient, error) { +func (c *vcsConsumer) GetAuthorizedClient(ctx context.Context, token, secret string, created int64) (sdk.VCSAuthorizedClientService, error) { s := GetProjectVCSServer(*c.proj, c.name) if s == nil { return nil, sdk.ErrNoReposManagerClientAuth @@ -197,9 +197,9 @@ func (c *vcsConsumer) GetAuthorizedClient(ctx context.Context, token, secret str } //AuthorizedClient returns an implementation of AuthorizedClient wrapping calls to vcs uService -func AuthorizedClient(ctx context.Context, db gorp.SqlExecutor, store cache.Store, projectKey string, repo *sdk.ProjectVCSServer) (sdk.VCSAuthorizedClient, error) { +func AuthorizedClient(ctx context.Context, db gorp.SqlExecutor, store cache.Store, projectKey string, repo *sdk.ProjectVCSServer) (sdk.VCSAuthorizedClientService, error) { if repo == nil { - return nil, sdk.ErrNoReposManagerClientAuth + return nil, sdk.WithStack(sdk.ErrNoReposManagerClientAuth) } srvs, err := services.LoadAllByType(ctx, db, services.TypeVCS) @@ -373,7 +373,7 @@ func (c *vcsClient) Branch(ctx context.Context, fullname string, branchName stri } // DefaultBranch get default branch from given repository -func DefaultBranch(ctx context.Context, c sdk.VCSAuthorizedClient, fullname string) (sdk.VCSBranch, error) { +func DefaultBranch(ctx context.Context, c sdk.VCSAuthorizedClientCommon, fullname string) (sdk.VCSBranch, error) { branches, err := c.Branches(ctx, fullname) if err != nil { return sdk.VCSBranch{}, sdk.WrapError(err, "Unable to list branches on repository %s", fullname) @@ -431,10 +431,16 @@ func (c *vcsClient) PullRequest(ctx context.Context, fullname string, ID int) (s return pr, nil } -func (c *vcsClient) PullRequests(ctx context.Context, fullname string) ([]sdk.VCSPullRequest, error) { +func (c *vcsClient) PullRequests(ctx context.Context, fullname string, mods ...sdk.VCSRequestModifier) ([]sdk.VCSPullRequest, error) { + req, err := http.NewRequest("GET", fmt.Sprintf("/vcs/%s/repos/%s/pullrequests", c.name, fullname), nil) + if err != nil { + return nil, sdk.WithStack(err) + } + for _, m := range mods { + m(req) + } prs := []sdk.VCSPullRequest{} - path := fmt.Sprintf("/vcs/%s/repos/%s/pullrequests", c.name, fullname) - if _, err := c.doJSONRequest(ctx, "GET", path, nil, &prs); err != nil { + if _, err := c.doJSONRequest(ctx, "GET", req.URL.String(), nil, &prs); err != nil { return nil, sdk.WrapError(err, "unable to find pullrequests on repository %s from %s", fullname, c.name) } return prs, nil @@ -624,7 +630,7 @@ type WebhooksInfos struct { } // GetWebhooksInfos returns webhooks_supported, webhooks_disabled, webhooks_creation_supported, webhooks_creation_disabled for a vcs server -func GetWebhooksInfos(ctx context.Context, c sdk.VCSAuthorizedClient) (WebhooksInfos, error) { +func GetWebhooksInfos(ctx context.Context, c sdk.VCSAuthorizedClientService) (WebhooksInfos, error) { client, ok := c.(*vcsClient) if !ok { return WebhooksInfos{}, fmt.Errorf("Polling infos cast error") @@ -644,7 +650,7 @@ type PollingInfos struct { } // GetPollingInfos returns polling_supported and polling_disabled for a vcs server -func GetPollingInfos(ctx context.Context, c sdk.VCSAuthorizedClient, prj sdk.Project) (PollingInfos, error) { +func GetPollingInfos(ctx context.Context, c sdk.VCSAuthorizedClientService, prj sdk.Project) (PollingInfos, error) { client, ok := c.(*vcsClient) if !ok { return PollingInfos{}, fmt.Errorf("Polling infos cast error") diff --git a/engine/api/templates.go b/engine/api/templates.go index 4b494ff382..d00c495339 100644 --- a/engine/api/templates.go +++ b/engine/api/templates.go @@ -13,8 +13,11 @@ import ( "github.com/gorilla/mux" + "github.com/ovh/cds/engine/api/application" + "github.com/ovh/cds/engine/api/ascode" "github.com/ovh/cds/engine/api/event" "github.com/ovh/cds/engine/api/group" + "github.com/ovh/cds/engine/api/operation" "github.com/ovh/cds/engine/api/project" "github.com/ovh/cds/engine/api/workflow" "github.com/ovh/cds/engine/api/workflowtemplate" @@ -322,6 +325,8 @@ func (api *API) postTemplateApplyHandler() service.Handler { } withImport := FormBool(r, "import") + branch := FormString(r, "branch") + message := FormString(r, "message") // parse and check request var req sdk.WorkflowTemplateRequest @@ -360,7 +365,9 @@ func (api *API) postTemplateApplyHandler() service.Handler { project.LoadOptions.WithEnvironments, project.LoadOptions.WithPipelines, project.LoadOptions.WithApplicationWithDeploymentStrategies, - project.LoadOptions.WithIntegrations) + project.LoadOptions.WithIntegrations, + project.LoadOptions.WithClearKeys, + ) if err != nil { return err } @@ -395,8 +402,37 @@ func (api *API) postTemplateApplyHandler() service.Handler { return err } if existingWorkflow.FromRepository != "" { - // TODO we need to create a PR - return sdk.NewErrorFrom(sdk.ErrNotImplemented, "update a workflow ascode with template not yet available") + var rootApp *sdk.Application + if existingWorkflow.WorkflowData.Node.Context != nil && existingWorkflow.WorkflowData.Node.Context.ApplicationID != 0 { + rootApp, err = application.LoadByIDWithClearVCSStrategyPassword(api.mustDB(), existingWorkflow.WorkflowData.Node.Context.ApplicationID) + if err != nil { + return err + } + } + if rootApp == nil { + return sdk.NewErrorFrom(sdk.ErrWrongRequest, "cannot find the root application of the workflow") + } + + ope, err := operation.PushOperationUpdate(ctx, api.mustDB(), api.Cache, *p, data, rootApp.VCSServer, rootApp.RepositoryFullname, branch, message, rootApp.RepositoryStrategy, consumer) + if err != nil { + return err + } + + sdk.GoRoutine(context.Background(), fmt.Sprintf("UpdateAsCodeResult-%s", ope.UUID), func(ctx context.Context) { + ed := ascode.EntityData{ + Name: existingWorkflow.Name, + ID: existingWorkflow.ID, + Type: ascode.WorkflowEvent, + FromRepo: existingWorkflow.FromRepository, + OperationUUID: ope.UUID, + } + asCodeEvent := ascode.UpdateAsCodeResult(ctx, api.mustDB(), api.Cache, *p, existingWorkflow.ID, *rootApp, ed, consumer) + if asCodeEvent != nil { + event.PublishAsCodeEvent(ctx, p.Key, *asCodeEvent, consumer) + } + }, api.PanicDump()) + + return service.WriteJSON(w, ope, http.StatusOK) } } } @@ -407,7 +443,7 @@ func (api *API) postTemplateApplyHandler() service.Handler { if req.Detached { mods = append(mods, workflowtemplate.TemplateRequestModifiers.Detached) } - wti, err := workflowtemplate.CheckAndExecuteTemplate(ctx, api.mustDB(), *consumer, *p, &data, mods...) + _, wti, err := workflowtemplate.CheckAndExecuteTemplate(ctx, api.mustDB(), *consumer, *p, &data, mods...) if err != nil { return err } @@ -469,6 +505,9 @@ func (api *API) postTemplateBulkHandler() service.Handler { return err } + branch := FormString(r, "branch") + message := FormString(r, "message") + // check all requests var req sdk.WorkflowTemplateBulk if err := service.UnmarshalBody(r, &req); err != nil { @@ -526,7 +565,7 @@ func (api *API) postTemplateBulkHandler() service.Handler { errorDefer := func(err error) error { if err != nil { - log.Error(ctx, "%v", err) + log.Error(ctx, "%+v", err) bulk.Operations[i].Status = sdk.OperationStatusError bulk.Operations[i].Error = fmt.Sprintf("%s", sdk.Cause(err)) if err := workflowtemplate.UpdateBulk(api.mustDB(), &bulk); err != nil { @@ -544,7 +583,9 @@ func (api *API) postTemplateBulkHandler() service.Handler { project.LoadOptions.WithEnvironments, project.LoadOptions.WithPipelines, project.LoadOptions.WithApplicationWithDeploymentStrategies, - project.LoadOptions.WithIntegrations) + project.LoadOptions.WithIntegrations, + project.LoadOptions.WithClearKeys, + ) if err != nil { if errD := errorDefer(err); errD != nil { log.Error(ctx, "%v", errD) @@ -581,11 +622,52 @@ func (api *API) postTemplateBulkHandler() service.Handler { continue } if existingWorkflow.FromRepository != "" { - // TODO we need to create a PR - if errD := errorDefer(sdk.NewErrorFrom(sdk.ErrNotImplemented, "update a workflow ascode with template not yet available")); errD != nil { - log.Error(ctx, "%v", errD) + var rootApp *sdk.Application + if existingWorkflow.WorkflowData.Node.Context != nil && existingWorkflow.WorkflowData.Node.Context.ApplicationID != 0 { + rootApp, err = application.LoadByIDWithClearVCSStrategyPassword(api.mustDB(), existingWorkflow.WorkflowData.Node.Context.ApplicationID) + if err != nil { + if errD := errorDefer(err); errD != nil { + log.Error(ctx, "%v", errD) + return + } + continue + } + } + if rootApp == nil { + if errD := errorDefer(sdk.NewErrorFrom(sdk.ErrWrongRequest, "cannot find the root application of the workflow")); errD != nil { + log.Error(ctx, "%v", errD) + return + } + continue + } + + ope, err := operation.PushOperationUpdate(ctx, api.mustDB(), api.Cache, *p, data, rootApp.VCSServer, rootApp.RepositoryFullname, branch, message, rootApp.RepositoryStrategy, consumer) + if err != nil { + if errD := errorDefer(err); errD != nil { + log.Error(ctx, "%v", errD) + return + } + continue + } + + ed := ascode.EntityData{ + Name: existingWorkflow.Name, + ID: existingWorkflow.ID, + Type: ascode.WorkflowEvent, + FromRepo: existingWorkflow.FromRepository, + OperationUUID: ope.UUID, + } + asCodeEvent := ascode.UpdateAsCodeResult(ctx, api.mustDB(), api.Cache, *p, existingWorkflow.ID, *rootApp, ed, consumer) + if asCodeEvent != nil { + event.PublishAsCodeEvent(ctx, p.Key, *asCodeEvent, consumer) + } + + bulk.Operations[i].Status = sdk.OperationStatusDone + if err := workflowtemplate.UpdateBulk(api.mustDB(), &bulk); err != nil { + log.Error(ctx, "%v", err) return } + continue } } @@ -593,7 +675,7 @@ func (api *API) postTemplateBulkHandler() service.Handler { mods := []workflowtemplate.TemplateRequestModifierFunc{ workflowtemplate.TemplateRequestModifiers.DefaultKeys(*p), } - wti, err = workflowtemplate.CheckAndExecuteTemplate(ctx, api.mustDB(), *consumer, *p, &data, mods...) + _, wti, err = workflowtemplate.CheckAndExecuteTemplate(ctx, api.mustDB(), *consumer, *p, &data, mods...) if err != nil { if errD := errorDefer(err); errD != nil { log.Error(ctx, "%v", errD) diff --git a/engine/api/workflow/as_code.go b/engine/api/workflow/as_code.go deleted file mode 100644 index 39cfb730fb..0000000000 --- a/engine/api/workflow/as_code.go +++ /dev/null @@ -1,65 +0,0 @@ -package workflow - -import ( - "context" - "fmt" - "time" - - v2 "github.com/ovh/cds/sdk/exportentities/v2" - - "github.com/go-gorp/gorp" - - "github.com/ovh/cds/engine/api/cache" - "github.com/ovh/cds/engine/api/operation" - "github.com/ovh/cds/sdk" - "github.com/ovh/cds/sdk/exportentities" -) - -// UpdateWorkflowAsCode update an as code workflow. -func UpdateWorkflowAsCode(ctx context.Context, store cache.Store, db gorp.SqlExecutor, proj sdk.Project, wf sdk.Workflow, vcsServerName, repoFullname, branch, message string, vcsStrategy sdk.RepositoryStrategy, u *sdk.AuthentifiedUser) (*sdk.Operation, error) { - if err := RenameNode(ctx, db, &wf); err != nil { - return nil, err - } - if err := IsValid(ctx, store, db, &wf, proj, LoadOptions{DeepPipeline: true}); err != nil { - return nil, err - } - - var wp exportentities.WorkflowComponents - var err error - wp.Workflow, err = exportentities.NewWorkflow(ctx, wf, v2.WorkflowSkipIfOnlyOneRepoWebhook) - if err != nil { - return nil, sdk.WrapError(err, "unable to export workflow") - } - - if wf.WorkflowData.Node.Context == nil || wf.WorkflowData.Node.Context.ApplicationID == 0 { - return nil, sdk.WithStack(sdk.ErrNotFound) - } - - return operation.PushOperation(ctx, db, store, proj, wp, vcsServerName, repoFullname, branch, message, vcsStrategy, true, u) -} - -// MigrateAsCode does a workflow pull and start an operation to push cds files into the git repository -func MigrateAsCode(ctx context.Context, db *gorp.DbMap, store cache.Store, proj sdk.Project, wf *sdk.Workflow, u sdk.Identifiable, encryptFunc sdk.EncryptFunc, vcsServerName, repoFullname, branch, message string, vcsStrategy sdk.RepositoryStrategy) (*sdk.Operation, error) { - // Get repository - if wf.WorkflowData.Node.Context == nil || wf.WorkflowData.Node.Context.ApplicationID == 0 { - return nil, sdk.WithStack(sdk.ErrNotFound) - } - - // Export workflow - pull, err := Pull(ctx, db, store, proj, wf.Name, encryptFunc, v2.WorkflowSkipIfOnlyOneRepoWebhook) - if err != nil { - return nil, sdk.WrapError(err, "cannot pull workflow") - } - - if message == "" { - if wf.FromRepository == "" { - message = fmt.Sprintf("feat: Enable workflow as code [@%s]", u.GetUsername()) - } else { - message = fmt.Sprintf("chore: Update workflow [@%s]", u.GetUsername()) - } - } - if branch == "" { - branch = fmt.Sprintf("cdsAsCode-%d", time.Now().Unix()) - } - return operation.PushOperation(ctx, db, store, proj, pull, vcsServerName, repoFullname, branch, message, vcsStrategy, false, u) -} diff --git a/engine/api/workflow/dao.go b/engine/api/workflow/dao.go index 4c63bc0afe..998454d595 100644 --- a/engine/api/workflow/dao.go +++ b/engine/api/workflow/dao.go @@ -563,17 +563,11 @@ func load(ctx context.Context, db gorp.SqlExecutor, proj sdk.Project, opts LoadO } if opts.WithAsCodeUpdateEvent { - var asCodeEvents []sdk.AsCodeEvent - var errAS error _, next = observability.Span(ctx, "workflow.load.AddCodeUpdateEvents") - if res.FromRepository != "" { - asCodeEvents, errAS = ascode.LoadAsCodeEventByRepo(ctx, db, res.FromRepository) - } else { - asCodeEvents, errAS = ascode.LoadAsCodeEventByWorkflowID(ctx, db, res.ID) - } + asCodeEvents, err := ascode.LoadEventsByWorkflowID(ctx, db, res.ID) next() - if errAS != nil { - return nil, sdk.WrapError(errAS, "Load> unable to load as code update events") + if err != nil { + return nil, sdk.WrapError(err, "unable to load as code update events") } res.AsCodeEvent = asCodeEvents } diff --git a/engine/api/workflow/dao_run_test.go b/engine/api/workflow/dao_run_test.go index 3c063f0d0d..05ee137549 100644 --- a/engine/api/workflow/dao_run_test.go +++ b/engine/api/workflow/dao_run_test.go @@ -9,12 +9,11 @@ import ( "testing" "time" - "github.com/ovh/cds/engine/api/authentication" - "github.com/stretchr/testify/assert" "gopkg.in/yaml.v2" "github.com/ovh/cds/engine/api/application" + "github.com/ovh/cds/engine/api/authentication" "github.com/ovh/cds/engine/api/bootstrap" "github.com/ovh/cds/engine/api/event" "github.com/ovh/cds/engine/api/pipeline" diff --git a/engine/api/workflow/repository.go b/engine/api/workflow/repository.go index 1562edba24..e50599d601 100644 --- a/engine/api/workflow/repository.go +++ b/engine/api/workflow/repository.go @@ -43,16 +43,17 @@ func CreateFromRepository(ctx context.Context, db *gorp.DbMap, store cache.Store ctx, end := observability.Span(ctx, "workflow.CreateFromRepository") defer end() - ope, err := createOperationRequest(*wf, opts) + newOperation, err := createOperationRequest(*wf, opts) if err != nil { return nil, sdk.WrapError(err, "unable to create operation request") } - if err := operation.PostRepositoryOperation(ctx, db, *p, &ope, nil); err != nil { + if err := operation.PostRepositoryOperation(ctx, db, *p, &newOperation, nil); err != nil { return nil, sdk.WrapError(err, "unable to post repository operation") } - if err := pollRepositoryOperation(ctx, db, store, &ope); err != nil { + ope, err := pollRepositoryOperation(ctx, db, store, newOperation.UUID) + if err != nil { return nil, sdk.WrapError(err, "cannot analyse repository") } @@ -68,7 +69,7 @@ func CreateFromRepository(ctx context.Context, db *gorp.DbMap, store cache.Store } } } - return extractWorkflow(ctx, db, store, p, wf, ope, u, decryptFunc, uuid) + return extractWorkflow(ctx, db, store, p, wf, *ope, u, decryptFunc, uuid) } func extractWorkflow(ctx context.Context, db *gorp.DbMap, store cache.Store, p *sdk.Project, wf *sdk.Workflow, @@ -108,12 +109,19 @@ func extractWorkflow(ctx context.Context, db *gorp.DbMap, store cache.Store, p * if opt.FromRepository != "" { mods = append(mods, workflowtemplate.TemplateRequestModifiers.DefaultNameAndRepositories(ctx, db, store, *p, opt.FromRepository)) } - wti, err := workflowtemplate.CheckAndExecuteTemplate(ctx, db, consumer, *p, &data, mods...) + msgTemplate, wti, err := workflowtemplate.CheckAndExecuteTemplate(ctx, db, consumer, *p, &data, mods...) + allMsgs = append(allMsgs, msgTemplate...) if err != nil { return allMsgs, err } - msgList, workflowPushed, _, err := Push(ctx, db, store, p, data, opt, consumer, decryptFunc) - allMsgs = append(allMsgs, msgList...) + msgPush, workflowPushed, _, err := Push(ctx, db, store, p, data, opt, consumer, decryptFunc) + // Filter workflow push message if generated from template + for i := range msgPush { + if wti != nil && msgPush[i].ID == sdk.MsgWorkflowDeprecatedVersion.ID { + continue + } + allMsgs = append(allMsgs, msgPush[i]) + } if err != nil { return allMsgs, sdk.WrapError(err, "unable to get workflow from file") } @@ -160,7 +168,7 @@ func ReadCDSFiles(files map[string][]byte) (*tar.Reader, error) { return tar.NewReader(buf), nil } -func pollRepositoryOperation(c context.Context, db gorp.SqlExecutor, store cache.Store, ope *sdk.Operation) error { +func pollRepositoryOperation(c context.Context, db gorp.SqlExecutor, store cache.Store, operationUUID string) (*sdk.Operation, error) { tickTimeout := time.NewTicker(10 * time.Minute) tickPoll := time.NewTicker(2 * time.Second) defer tickTimeout.Stop() @@ -168,22 +176,23 @@ func pollRepositoryOperation(c context.Context, db gorp.SqlExecutor, store cache select { case <-c.Done(): if c.Err() != nil { - return sdk.WrapError(c.Err(), "exiting") + return nil, sdk.WrapError(c.Err(), "exiting") } case <-tickTimeout.C: - return sdk.WrapError(sdk.ErrRepoOperationTimeout, "timeout analyzing repository") + return nil, sdk.WrapError(sdk.ErrRepoOperationTimeout, "timeout analyzing repository") case <-tickPoll.C: - if err := operation.GetRepositoryOperation(c, db, ope); err != nil { - return sdk.WrapError(err, "cannot get repository operation status") + ope, err := operation.GetRepositoryOperation(c, db, operationUUID) + if err != nil { + return nil, sdk.WrapError(err, "cannot get repository operation status") } switch ope.Status { case sdk.OperationStatusError: opeTrusted := *ope opeTrusted.RepositoryStrategy.SSHKeyContent = "***" opeTrusted.RepositoryStrategy.Password = "***" - return sdk.WrapError(fmt.Errorf("%s", ope.Error), "getImportAsCodeHandler> Operation in error. %+v", opeTrusted) + return nil, sdk.WrapError(fmt.Errorf("%s", ope.Error), "getImportAsCodeHandler> Operation in error. %+v", opeTrusted) case sdk.OperationStatusDone: - return nil + return ope, nil } continue } diff --git a/engine/api/workflow/workflow_run_event.go b/engine/api/workflow/workflow_run_event.go index 231288cb5b..71782e3844 100644 --- a/engine/api/workflow/workflow_run_event.go +++ b/engine/api/workflow/workflow_run_event.go @@ -18,7 +18,7 @@ import ( type VCSEventMessenger struct { commitsStatuses map[string][]sdk.VCSCommitStatus - vcsClient sdk.VCSAuthorizedClient + vcsClient sdk.VCSAuthorizedClientService } // ResyncCommitStatus resync commit status for a workflow run diff --git a/engine/api/workflow_ascode.go b/engine/api/workflow_ascode.go index 0af0b0fad7..02578e5bdf 100644 --- a/engine/api/workflow_ascode.go +++ b/engine/api/workflow_ascode.go @@ -9,7 +9,6 @@ import ( "github.com/ovh/cds/engine/api/application" "github.com/ovh/cds/engine/api/ascode" - "github.com/ovh/cds/engine/api/ascode/sync" "github.com/ovh/cds/engine/api/cache" "github.com/ovh/cds/engine/api/event" "github.com/ovh/cds/engine/api/operation" @@ -17,6 +16,8 @@ import ( "github.com/ovh/cds/engine/api/workflow" "github.com/ovh/cds/engine/service" "github.com/ovh/cds/sdk" + "github.com/ovh/cds/sdk/exportentities" + v2 "github.com/ovh/cds/sdk/exportentities/v2" "github.com/ovh/cds/sdk/log" ) @@ -96,29 +97,41 @@ func (api *API) postWorkflowAsCodeHandler() service.Handler { return sdk.NewErrorFrom(sdk.ErrForbidden, "cannot update a workflow that was generated by a template") } - var wk sdk.Workflow - if err := service.UnmarshalBody(r, &wk); err != nil { + var wf sdk.Workflow + if err := service.UnmarshalBody(r, &wf); err != nil { return err } - ope, err := workflow.UpdateWorkflowAsCode(ctx, api.Cache, api.mustDB(), *p, wk, rootApp.VCSServer, rootApp.RepositoryFullname, branch, message, rootApp.RepositoryStrategy, u.AuthentifiedUser) + if err := workflow.RenameNode(ctx, api.mustDB(), &wf); err != nil { + return err + } + if err := workflow.IsValid(ctx, api.Cache, api.mustDB(), &wf, *p, workflow.LoadOptions{DeepPipeline: true}); err != nil { + return err + } + + var data exportentities.WorkflowComponents + data.Workflow, err = exportentities.NewWorkflow(ctx, wf, v2.WorkflowSkipIfOnlyOneRepoWebhook) + if err != nil { + return err + } + + ope, err := operation.PushOperationUpdate(ctx, api.mustDB(), api.Cache, *p, data, rootApp.VCSServer, rootApp.RepositoryFullname, branch, message, rootApp.RepositoryStrategy, u) if err != nil { return err } sdk.GoRoutine(context.Background(), fmt.Sprintf("UpdateAsCodeResult-%s", ope.UUID), func(ctx context.Context) { ed := ascode.EntityData{ - Operation: ope, - Name: wk.Name, - ID: wk.ID, - Type: ascode.AsCodeWorkflow, - FromRepo: wk.FromRepository, + Name: wfDB.Name, + ID: wfDB.ID, + Type: ascode.WorkflowEvent, + FromRepo: wfDB.FromRepository, + OperationUUID: ope.UUID, } - asCodeEvent := ascode.UpdateAsCodeResult(ctx, api.mustDB(), api.Cache, *p, *rootApp, ed, u) + asCodeEvent := ascode.UpdateAsCodeResult(ctx, api.mustDB(), api.Cache, *p, wfDB.ID, *rootApp, ed, u) if asCodeEvent != nil { event.PublishAsCodeEvent(ctx, p.Key, *asCodeEvent, u) } - event.PublishWorkflowUpdate(ctx, p.Key, wk, wk, u) }, api.PanicDump()) return service.WriteJSON(w, ope, http.StatusOK) @@ -128,15 +141,6 @@ func (api *API) postWorkflowAsCodeHandler() service.Handler { func (api *API) migrateWorkflowAsCode(ctx context.Context, w http.ResponseWriter, proj sdk.Project, wf *sdk.Workflow, app sdk.Application, branch, message string) error { u := getAPIConsumer(ctx) - // Sync as code event - if len(wf.AsCodeEvent) > 0 { - eventsLeft, _, err := sync.SyncAsCodeEvent(ctx, api.mustDB(), api.Cache, proj, app, getAPIConsumer(ctx).AuthentifiedUser) - if err != nil { - return err - } - wf.AsCodeEvent = eventsLeft - } - if wf.FromRepository != "" || (wf.FromRepository == "" && len(wf.AsCodeEvent) > 0) { return sdk.WithStack(sdk.ErrWorkflowAlreadyAsCode) } @@ -161,21 +165,25 @@ func (api *API) migrateWorkflowAsCode(ctx context.Context, w http.ResponseWriter } } - // Export workflow + push + create pull request - ope, err := workflow.MigrateAsCode(ctx, api.mustDB(), api.Cache, proj, wf, u, project.EncryptWithBuiltinKey, app.VCSServer, app.RepositoryFullname, branch, message, app.RepositoryStrategy) + data, err := workflow.Pull(ctx, api.mustDB(), api.Cache, proj, wf.Name, project.EncryptWithBuiltinKey, v2.WorkflowSkipIfOnlyOneRepoWebhook) + if err != nil { + return err + } + + ope, err := operation.PushOperation(ctx, api.mustDB(), api.Cache, proj, data, app.VCSServer, app.RepositoryFullname, branch, message, app.RepositoryStrategy, u) if err != nil { - return sdk.WrapError(err, "unable to migrate workflow as code") + return err } sdk.GoRoutine(context.Background(), fmt.Sprintf("MigrateWorkflowAsCodeResult-%s", ope.UUID), func(ctx context.Context) { ed := ascode.EntityData{ - FromRepo: ope.URL, - Type: ascode.AsCodeWorkflow, - ID: wf.ID, - Name: wf.Name, - Operation: ope, + FromRepo: ope.URL, + Type: ascode.WorkflowEvent, + ID: wf.ID, + Name: wf.Name, + OperationUUID: ope.UUID, } - asCodeEvent := ascode.UpdateAsCodeResult(ctx, api.mustDB(), api.Cache, proj, app, ed, u) + asCodeEvent := ascode.UpdateAsCodeResult(ctx, api.mustDB(), api.Cache, proj, wf.ID, app, ed, u) if asCodeEvent != nil { event.PublishAsCodeEvent(ctx, proj.Key, *asCodeEvent, u) } diff --git a/engine/api/workflow_ascode_test.go b/engine/api/workflow_ascode_test.go index c9d922af6f..861a9eb203 100644 --- a/engine/api/workflow_ascode_test.go +++ b/engine/api/workflow_ascode_test.go @@ -109,6 +109,11 @@ func TestPostUpdateWorkflowAsCodeHandler(t *testing.T) { if err := enc.Encode(hook); err != nil { return writeError(w, err) } + case "/vcs/github/repos/foo/myrepo/pullrequests?state=open": + vcsPRs := []sdk.VCSPullRequest{} + if err := enc.Encode(vcsPRs); err != nil { + return writeError(w, err) + } case "/vcs/github/repos/foo/myrepo/pullrequests": vcsPR := sdk.VCSPullRequest{ URL: "myURL", @@ -295,6 +300,11 @@ func TestPostMigrateWorkflowAsCodeHandler(t *testing.T) { if err := enc.Encode(hook); err != nil { return writeError(w, err) } + case "/vcs/github/repos/foo/myrepo/pullrequests?state=open": + vcsPRs := []sdk.VCSPullRequest{} + if err := enc.Encode(vcsPRs); err != nil { + return writeError(w, err) + } case "/vcs/github/repos/foo/myrepo/pullrequests": vcsPR := sdk.VCSPullRequest{ URL: "myURL", diff --git a/engine/api/workflow_ascode_with_hooks_test.go b/engine/api/workflow_ascode_with_hooks_test.go index 3a3be16a1b..60a1cc14b7 100644 --- a/engine/api/workflow_ascode_with_hooks_test.go +++ b/engine/api/workflow_ascode_with_hooks_test.go @@ -557,12 +557,6 @@ func Test_WorkflowAsCodeWithDefaultHookAndAScheduler_ShouldGive_TheSameRepoWebHo "permProjectKey": proj.Key, }) - UUID := sdk.UUID() - - servicesClients.EXPECT(). - DoJSONRequest(gomock.Any(), "POST", "/operations", gomock.Any(), gomock.Any()). - Return(nil, 201, nil).Times(2) - servicesClients.EXPECT(). DoJSONRequest(gomock.Any(), "GET", "/vcs/github/repos/fsamin/go-repo", gomock.Any(), gomock.Any(), gomock.Any()).MinTimes(0) @@ -594,13 +588,24 @@ func Test_WorkflowAsCodeWithDefaultHookAndAScheduler_ShouldGive_TheSameRepoWebHo }, ).MaxTimes(3) + operationUUID := sdk.UUID() + servicesClients.EXPECT(). - DoJSONRequest(gomock.Any(), "GET", gomock.Any(), gomock.Any(), gomock.Any()). + DoJSONRequest(gomock.Any(), "POST", "/operations", gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, method, path string, in interface{}, out interface{}) (http.Header, int, error) { + ope := new(sdk.Operation) + ope.UUID = operationUUID + *(out.(*sdk.Operation)) = *ope + return nil, 200, nil + }).Times(2) + + servicesClients.EXPECT(). + DoJSONRequest(gomock.Any(), "GET", "/operations/"+operationUUID, gomock.Any(), gomock.Any(), gomock.Any()). DoAndReturn( func(ctx context.Context, method, path string, in interface{}, out interface{}) (http.Header, int, error) { ope := new(sdk.Operation) ope.URL = "https://github.com/fsamin/go-repo.git" - ope.UUID = UUID + ope.UUID = operationUUID ope.Status = sdk.OperationStatusDone ope.VCSServer = "github" ope.RepoFullName = "fsamin/go-repo" @@ -745,7 +750,7 @@ version: v1.0`), uri = api.Router.GetRoute("POST", api.postPerformImportAsCodeHandler, map[string]string{ "permProjectKey": prjKey, - "uuid": UUID, + "uuid": operationUUID, }) req, err = http.NewRequest("POST", uri, nil) require.NoError(t, err) diff --git a/engine/api/workflow_import.go b/engine/api/workflow_import.go index c4d572b7ce..856d0f4276 100644 --- a/engine/api/workflow_import.go +++ b/engine/api/workflow_import.go @@ -315,11 +315,14 @@ func (api *API) postWorkflowPushHandler() service.Handler { if pushOptions != nil && pushOptions.FromRepository != "" { mods = append(mods, workflowtemplate.TemplateRequestModifiers.DefaultNameAndRepositories(ctx, api.mustDB(), api.Cache, *proj, pushOptions.FromRepository)) } - wti, err := workflowtemplate.CheckAndExecuteTemplate(ctx, api.mustDB(), *consumer, *proj, &data, mods...) + var allMsg []sdk.Message + msgTemplate, wti, err := workflowtemplate.CheckAndExecuteTemplate(ctx, api.mustDB(), *consumer, *proj, &data, mods...) + allMsg = append(allMsg, msgTemplate...) if err != nil { return err } - allMsg, wrkflw, oldWrkflw, err := workflow.Push(ctx, db, api.Cache, proj, data, pushOptions, u, project.DecryptWithBuiltinKey) + msgPush, wrkflw, oldWrkflw, err := workflow.Push(ctx, db, api.Cache, proj, data, pushOptions, u, project.DecryptWithBuiltinKey) + allMsg = append(allMsg, msgPush...) if err != nil { return err } diff --git a/engine/api/workflow_run.go b/engine/api/workflow_run.go index 7170193e82..b040b02a28 100644 --- a/engine/api/workflow_run.go +++ b/engine/api/workflow_run.go @@ -14,7 +14,7 @@ import ( "github.com/gorilla/mux" "github.com/sirupsen/logrus" - ascodesync "github.com/ovh/cds/engine/api/ascode/sync" + "github.com/ovh/cds/engine/api/ascode" "github.com/ovh/cds/engine/api/cache" "github.com/ovh/cds/engine/api/event" "github.com/ovh/cds/engine/api/integration" @@ -945,24 +945,23 @@ func (api *API) initWorkflowRun(ctx context.Context, projKey string, wf *sdk.Wor }() if wfRun.Status == sdk.StatusPending { - // Become as code ? - if wf.FromRepository == "" && len(wf.AsCodeEvent) > 0 { - if wf.WorkflowData.Node.Context.ApplicationID == 0 { - r1 := failInitWorkflowRun(ctx, api.mustDB(), wfRun, sdk.WrapError(sdk.ErrNotFound, "unable to find application on root node")) - report.Merge(ctx, r1) - return - } - app := wf.Applications[wf.WorkflowData.Node.Context.ApplicationID] - - _, fromRepo, err := ascodesync.SyncAsCodeEvent(ctx, api.mustDB(), api.Cache, *p, app, u.AuthentifiedUser) + // Sync as code event to remove events in case where a PR was merged + if len(wf.AsCodeEvent) > 0 { + res, err := ascode.SyncEvents(ctx, api.mustDB(), api.Cache, *p, *wf, u.AuthentifiedUser) if err != nil { r := failInitWorkflowRun(ctx, api.mustDB(), wfRun, sdk.WrapError(err, "unable to sync as code event")) report.Merge(ctx, r) return } - event.PublishWorkflowUpdate(ctx, p.Key, *wf, *wf, u) - - wf.FromRepository = fromRepo + if res.Merged { + if err := workflow.UpdateFromRepository(api.mustDB(), wf.ID, res.FromRepository); err != nil { + r := failInitWorkflowRun(ctx, api.mustDB(), wfRun, sdk.WrapError(err, "unable to sync as code event")) + report.Merge(ctx, r) + return + } + wf.FromRepository = res.FromRepository + event.PublishWorkflowUpdate(ctx, p.Key, *wf, *wf, u) + } } // If the workflow is as code we need to reimport it. @@ -993,17 +992,16 @@ func (api *API) initWorkflowRun(ctx context.Context, projKey string, wf *sdk.Wor log.Debug("workflow.CreateFromRepository> %s", wf.Name) oldWf := *wf asCodeInfosMsg, err := workflow.CreateFromRepository(ctx, api.mustDB(), api.Cache, p1, wf, *opts, *u, project.DecryptWithBuiltinKey) - if err != nil { - infos := make([]sdk.SpawnMsg, len(asCodeInfosMsg)) - for i, msg := range asCodeInfosMsg { - infos[i] = sdk.SpawnMsg{ - ID: msg.ID, - Args: msg.Args, - Type: msg.Type, - } - + infos := make([]sdk.SpawnMsg, len(asCodeInfosMsg)) + for i, msg := range asCodeInfosMsg { + infos[i] = sdk.SpawnMsg{ + ID: msg.ID, + Args: msg.Args, + Type: msg.Type, } - workflow.AddWorkflowRunInfo(wfRun, infos...) + } + workflow.AddWorkflowRunInfo(wfRun, infos...) + if err != nil { r1 := failInitWorkflowRun(ctx, api.mustDB(), wfRun, sdk.WrapError(err, "unable to get workflow from repository")) report.Merge(ctx, r1) return diff --git a/engine/api/workflow_run_test.go b/engine/api/workflow_run_test.go index d5c757cb2c..03764606db 100644 --- a/engine/api/workflow_run_test.go +++ b/engine/api/workflow_run_test.go @@ -996,12 +996,6 @@ func Test_postWorkflowRunAsyncFailedHandler(t *testing.T) { key := sdk.RandomString(10) proj := assets.InsertTestProject(t, db, api.Cache, key, key) - // Clean ascode event - evts, _ := ascode.LoadAsCodeEventByRepo(context.TODO(), db, "ssh:/cloneurl") - for _, e := range evts { - _ = ascode.DeleteAsCodeEvent(db, e) // nolint - } - assert.NoError(t, repositoriesmanager.InsertForProject(db, proj, &sdk.ProjectVCSServer{ Name: "github", Data: map[string]string{ @@ -1122,6 +1116,11 @@ func Test_postWorkflowRunAsyncFailedHandler(t *testing.T) { if err := enc.Encode(h); err != nil { return writeError(w, err) } + case "/vcs/github/repos/foo/myrepo/pullrequests?state=open": + vcsPRs := []sdk.VCSPullRequest{} + if err := enc.Encode(vcsPRs); err != nil { + return writeError(w, err) + } case "/vcs/github/repos/foo/myrepo/pullrequests": pr := sdk.VCSPullRequest{ Title: "blabla", @@ -1133,7 +1132,6 @@ func Test_postWorkflowRunAsyncFailedHandler(t *testing.T) { } case "/vcs/github/repos/foo/myrepo/pullrequests/1": return writeError(w, fmt.Errorf("error for test")) - case "/task/bulk": hooks := map[string]sdk.NodeHook{} hooks["123"] = sdk.NodeHook{ @@ -1168,14 +1166,14 @@ func Test_postWorkflowRunAsyncFailedHandler(t *testing.T) { }, } ed := ascode.EntityData{ - FromRepo: "ssh:/cloneurl", - Operation: &ope, - Name: w1.Name, - ID: w1.ID, - Type: ascode.AsCodeWorkflow, + FromRepo: "ssh:/cloneurl", + Name: w1.Name, + ID: w1.ID, + Type: ascode.WorkflowEvent, + OperationUUID: ope.UUID, } - x := ascode.UpdateAsCodeResult(context.TODO(), api.mustDB(), api.Cache, *proj, app, ed, u) + x := ascode.UpdateAsCodeResult(context.TODO(), api.mustDB(), api.Cache, *proj, w1.ID, app, ed, u) require.NotNil(t, x, "ascodeEvent should not be nil, but it was") //Prepare request diff --git a/engine/api/workflowtemplate/instance.go b/engine/api/workflowtemplate/instance.go index 596e0cfcee..228453c841 100644 --- a/engine/api/workflowtemplate/instance.go +++ b/engine/api/workflowtemplate/instance.go @@ -113,20 +113,22 @@ func requestModifyDefaultNameAndRepositories(ctx context.Context, db gorp.SqlExe // CheckAndExecuteTemplate will execute the workflow template if given workflow components contains a template instance. // When detached is set this will not create/update any template instance in database (this is useful for workflow ascode branches). func CheckAndExecuteTemplate(ctx context.Context, db *gorp.DbMap, consumer sdk.AuthConsumer, p sdk.Project, - data *exportentities.WorkflowComponents, mods ...TemplateRequestModifierFunc) (*sdk.WorkflowTemplateInstance, error) { + data *exportentities.WorkflowComponents, mods ...TemplateRequestModifierFunc) ([]sdk.Message, *sdk.WorkflowTemplateInstance, error) { + var allMsgs []sdk.Message + if data.Template.From == "" { - return nil, nil + return allMsgs, nil, nil } groupName, templateSlug, templateVersion, err := data.Template.ParseFrom() if err != nil { - return nil, err + return allMsgs, nil, err } // check that group exists grp, err := group.LoadByName(ctx, db, groupName) if err != nil { - return nil, err + return allMsgs, nil, err } var groupPermissionValid bool @@ -144,21 +146,23 @@ func CheckAndExecuteTemplate(ctx context.Context, db *gorp.DbMap, consumer sdk.A } } if !groupPermissionValid { - return nil, sdk.NewErrorFrom(sdk.ErrWrongRequest, "could not find given workflow template") + return allMsgs, nil, sdk.NewErrorFrom(sdk.ErrWrongRequest, "could not find given workflow template") } wt, err := LoadBySlugAndGroupID(ctx, db, templateSlug, grp.ID, LoadOptions.Default) if err != nil { - return nil, sdk.NewErrorFrom(err, "could not find a template with slug %s in group %s", templateSlug, grp.Name) + return allMsgs, nil, sdk.NewErrorFrom(err, "could not find a template with slug %s in group %s", templateSlug, grp.Name) } if templateVersion > 0 { wta, err := LoadAuditByTemplateIDAndVersion(ctx, db, wt.ID, templateVersion) if err != nil { - return nil, err + return allMsgs, nil, err } wt = &wta.DataAfter } + allMsgs = append(allMsgs, sdk.NewMessage(sdk.MsgWorkflowGeneratedFromTemplateVersion, wt.PathWithVersion())) + req := sdk.WorkflowTemplateRequest{ ProjectKey: p.Key, WorkflowName: data.Template.Name, @@ -166,12 +170,12 @@ func CheckAndExecuteTemplate(ctx context.Context, db *gorp.DbMap, consumer sdk.A } for i := range mods { if err := mods[i](*wt, &req); err != nil { - return nil, err + return allMsgs, nil, err } } if err := wt.CheckParams(req); err != nil { - return nil, err + return allMsgs, nil, err } var result exportentities.WorkflowComponents @@ -188,24 +192,24 @@ func CheckAndExecuteTemplate(ctx context.Context, db *gorp.DbMap, consumer sdk.A // execute template with request result, err = Execute(*wt, *wti) if err != nil { - return nil, err + return allMsgs, nil, err } // do not return an instance if detached *data = result - return nil, nil + return allMsgs, wti, nil } tx, err := db.Begin() if err != nil { - return nil, sdk.WrapError(err, "cannot start transaction") + return allMsgs, nil, sdk.WrapError(err, "cannot start transaction") } defer tx.Rollback() // nolint // try to get a instance not assign to a workflow but with the same slug wti, err := LoadInstanceByTemplateIDAndProjectIDAndRequestWorkflowName(ctx, tx, wt.ID, p.ID, req.WorkflowName) if err != nil && !sdk.ErrorIs(err, sdk.ErrNotFound) { - return nil, err + return allMsgs, nil, err } // if a previous instance exist for the same workflow update it, else create a new one @@ -216,7 +220,7 @@ func CheckAndExecuteTemplate(ctx context.Context, db *gorp.DbMap, consumer sdk.A wti.WorkflowTemplateVersion = wt.Version wti.Request = req if err := UpdateInstance(tx, wti); err != nil { - return nil, err + return allMsgs, nil, err } } else { wti = &sdk.WorkflowTemplateInstance{ @@ -227,47 +231,47 @@ func CheckAndExecuteTemplate(ctx context.Context, db *gorp.DbMap, consumer sdk.A } // only store the new instance if request is not for a detached workflow if err := InsertInstance(tx, wti); err != nil { - return nil, err + return allMsgs, nil, err } } // execute template with request result, err = Execute(*wt, *wti) if err != nil { - return nil, err + return allMsgs, nil, err } // 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 wti.WorkflowName = result.Workflow.GetName() if err := UpdateInstance(tx, wti); err != nil { - return nil, err + return allMsgs, nil, err } if old != nil { if err := CreateAuditInstanceUpdate(tx, *old, *wti, consumer); err != nil { - return nil, err + return allMsgs, nil, err } } else if !req.Detached { if err := CreateAuditInstanceAdd(tx, *wti, consumer); err != nil { - return nil, err + return allMsgs, nil, err } } if err := tx.Commit(); err != nil { - return nil, sdk.WithStack(err) + return allMsgs, nil, sdk.WithStack(err) } // if the template was successfully executed we want to return only the a file with template instance data *data = result - return wti, nil + return allMsgs, wti, nil } // UpdateTemplateInstanceWithWorkflow will perform some action after a successful workflow push, if it was generated // from a template we want to set the workflow id on generated template instance. func UpdateTemplateInstanceWithWorkflow(ctx context.Context, db gorp.SqlExecutor, w sdk.Workflow, u sdk.Identifiable, wti *sdk.WorkflowTemplateInstance) error { - if wti == nil { + if wti == nil || wti.Request.Detached { return nil } diff --git a/engine/api/workflowtemplate/instance_test.go b/engine/api/workflowtemplate/instance_test.go index b723f71758..4b7773eee4 100644 --- a/engine/api/workflowtemplate/instance_test.go +++ b/engine/api/workflowtemplate/instance_test.go @@ -68,7 +68,7 @@ version: v2.0`)), Data exportentities.WorkflowComponents Detached bool ErrorExists bool - InstanceExists bool + InstanceStored bool ExpectedInstance sdk.WorkflowTemplateInstance WorkflowExists bool ExpectedWorkflow exportentities.Workflow @@ -91,7 +91,7 @@ version: v2.0`)), Parameters: map[string]string{"param1": "value1"}, }, }, - InstanceExists: true, + InstanceStored: true, ExpectedInstance: sdk.WorkflowTemplateInstance{ Request: sdk.WorkflowTemplateRequest{ WorkflowName: "my-workflow", @@ -110,7 +110,7 @@ version: v2.0`)), Parameters: map[string]string{"param1": "value1"}, }, }, - InstanceExists: true, + InstanceStored: true, ExpectedInstance: sdk.WorkflowTemplateInstance{ Request: sdk.WorkflowTemplateRequest{ WorkflowName: "my-workflow", @@ -149,16 +149,16 @@ version: v2.0`)), if c.Detached { mods = append(mods, workflowtemplate.TemplateRequestModifiers.Detached) } - wti, err := workflowtemplate.CheckAndExecuteTemplate(context.TODO(), db, *consumer, *proj, &c.Data, mods...) + _, wti, err := workflowtemplate.CheckAndExecuteTemplate(context.TODO(), db, *consumer, *proj, &c.Data, mods...) if c.ErrorExists { require.Error(t, err) } else { require.NoError(t, err) } - instanceExists := wti != nil - require.Equal(t, c.InstanceExists, instanceExists, "Instance exists should be %t buf is %t", c.InstanceExists, instanceExists) - if instanceExists { + instanceStored := wti != nil && !wti.Request.Detached + require.Equal(t, c.InstanceStored, instanceStored, "Instance stored should be %t buf is %t", c.InstanceStored, instanceStored) + if instanceStored { assert.Equal(t, c.ExpectedInstance.Request, wti.Request) } @@ -214,7 +214,7 @@ name: Pipeline-[[.id]]`)), Parameters: map[string]string{"param1": "value1"}, }, } - wti, err := workflowtemplate.CheckAndExecuteTemplate(context.TODO(), db, *consumer, *proj, &data) + _, wti, err := workflowtemplate.CheckAndExecuteTemplate(context.TODO(), db, *consumer, *proj, &data) require.NoError(t, err) _, wkf, _, err := workflow.Push(context.TODO(), db, cache, proj, data, nil, consumer, project.DecryptWithBuiltinKey) diff --git a/engine/repositories/processor_push.go b/engine/repositories/processor_push.go index b000ae62ae..ecdd19e223 100644 --- a/engine/repositories/processor_push.go +++ b/engine/repositories/processor_push.go @@ -30,6 +30,11 @@ func (s *Service) processPush(ctx context.Context, op *sdk.Operation) error { return sdk.WrapError(err, "unable to process gitclone") } + // FIXME create Fetch and FetchTags method in go repo + if err := gitRepo.FetchRemoteBranch("origin", op.RepositoryInfo.DefaultBranch); err != nil { + return sdk.WrapError(err, "cannot fetch changes from remote at %s", op.RepositoryInfo.FetchURL) + } + if op.Setup.Push.ToBranch == "" { op.Setup.Push.ToBranch = op.RepositoryInfo.DefaultBranch } @@ -69,7 +74,7 @@ func (s *Service) processPush(ctx context.Context, op *sdk.Operation) error { } // Create files if err := os.Mkdir(filepath.Join(path, ".cds"), os.ModePerm); err != nil { - return sdk.WrapError(err, "error creating cds directory") + return sdk.WrapError(err, "error creating .cds directory") } } diff --git a/engine/sql/202_ascode_event_workflow_holder.sql b/engine/sql/202_ascode_event_workflow_holder.sql new file mode 100644 index 0000000000..1b7d12fce3 --- /dev/null +++ b/engine/sql/202_ascode_event_workflow_holder.sql @@ -0,0 +1,7 @@ +-- +migrate Up +ALTER TABLE "as_code_events" ADD COLUMN IF NOT EXISTS workflow_id BIGINT; +SELECT create_foreign_key_idx_cascade('FK_AS_CODE_EVENTS_WORKFLOW', 'as_code_events', 'workflow', 'workflow_id', 'id'); + +-- +migrate Down +ALTER TABLE "as_code_events" DROP COLUMN workflow_id; + diff --git a/engine/vcs/bitbucketcloud/client_pull_request.go b/engine/vcs/bitbucketcloud/client_pull_request.go index 6114da8377..9f262f421d 100644 --- a/engine/vcs/bitbucketcloud/client_pull_request.go +++ b/engine/vcs/bitbucketcloud/client_pull_request.go @@ -32,10 +32,20 @@ func (client *bitbucketcloudClient) PullRequest(ctx context.Context, fullname st } // PullRequests fetch all the pull request for a repository -func (client *bitbucketcloudClient) PullRequests(ctx context.Context, fullname string) ([]sdk.VCSPullRequest, error) { +func (client *bitbucketcloudClient) PullRequests(ctx context.Context, fullname string, opts sdk.VCSPullRequestOptions) ([]sdk.VCSPullRequest, error) { var pullrequests []PullRequest path := fmt.Sprintf("/repositories/%s/pullrequests", fullname) params := url.Values{} + + switch opts.State { + case sdk.VCSPullRequestStateOpen: + params.Set("state", "OPEN") + case sdk.VCSPullRequestStateMerged: + params.Set("state", "MERGED") + case sdk.VCSPullRequestStateClosed: + params.Set("state", "DECLINED") + } + params.Set("pagelen", "50") nextPage := 1 for { diff --git a/engine/vcs/bitbucketserver/client_pull_request.go b/engine/vcs/bitbucketserver/client_pull_request.go index 2942f7e423..a48bf6a293 100644 --- a/engine/vcs/bitbucketserver/client_pull_request.go +++ b/engine/vcs/bitbucketserver/client_pull_request.go @@ -32,7 +32,7 @@ func (b *bitbucketClient) PullRequest(ctx context.Context, repo string, id int) return pr, nil } -func (b *bitbucketClient) PullRequests(ctx context.Context, repo string) ([]sdk.VCSPullRequest, error) { +func (b *bitbucketClient) PullRequests(ctx context.Context, repo string, opts sdk.VCSPullRequestOptions) ([]sdk.VCSPullRequest, error) { project, slug, err := getRepo(repo) if err != nil { return nil, sdk.WithStack(err) @@ -43,6 +43,15 @@ func (b *bitbucketClient) PullRequests(ctx context.Context, repo string) ([]sdk. path := fmt.Sprintf("/projects/%s/repos/%s/pull-requests", project, slug) params := url.Values{} + switch opts.State { + case sdk.VCSPullRequestStateOpen: + params.Set("state", "OPEN") + case sdk.VCSPullRequestStateMerged: + params.Set("state", "MERGED") + case sdk.VCSPullRequestStateClosed: + params.Set("state", "DECLINED") + } + nextPage := 0 for { if ctx.Err() != nil { diff --git a/engine/vcs/bitbucketserver/client_pull_request_test.go b/engine/vcs/bitbucketserver/client_pull_request_test.go index abb358e9a2..d1ff4dc62b 100644 --- a/engine/vcs/bitbucketserver/client_pull_request_test.go +++ b/engine/vcs/bitbucketserver/client_pull_request_test.go @@ -12,7 +12,7 @@ import ( func TestPullRequests(t *testing.T) { client := getAuthorizedClient(t) - prs, err := client.PullRequests(context.Background(), "CDS/images") + prs, err := client.PullRequests(context.Background(), "CDS/images", sdk.VCSPullRequestOptions{}) test.NoError(t, err) assert.NotEmpty(t, prs) t.Logf("%v", prs) @@ -20,7 +20,7 @@ func TestPullRequests(t *testing.T) { func TestPullRequestComment(t *testing.T) { client := getAuthorizedClient(t) - prs, err := client.PullRequests(context.Background(), "CDS/images") + prs, err := client.PullRequests(context.Background(), "CDS/images", sdk.VCSPullRequestOptions{}) test.NoError(t, err) assert.NotEmpty(t, prs) t.Logf("%v", prs) diff --git a/engine/vcs/gerrit/client_pull_request.go b/engine/vcs/gerrit/client_pull_request.go index f90742fd95..11e0a151f1 100644 --- a/engine/vcs/gerrit/client_pull_request.go +++ b/engine/vcs/gerrit/client_pull_request.go @@ -13,7 +13,7 @@ func (c *gerritClient) PullRequest(ctx context.Context, repo string, id int) (sd } // PullRequests fetch all the pull request for a repository -func (c *gerritClient) PullRequests(context.Context, string) ([]sdk.VCSPullRequest, error) { +func (c *gerritClient) PullRequests(ctx context.Context, repo string, opts sdk.VCSPullRequestOptions) ([]sdk.VCSPullRequest, error) { return []sdk.VCSPullRequest{}, nil } diff --git a/engine/vcs/github/client_pull_request.go b/engine/vcs/github/client_pull_request.go index 7b5e97ed87..8f19c14eeb 100644 --- a/engine/vcs/github/client_pull_request.go +++ b/engine/vcs/github/client_pull_request.go @@ -75,10 +75,10 @@ func (g *githubClient) PullRequest(ctx context.Context, fullname string, id int) } // PullRequests fetch all the pull request for a repository -func (g *githubClient) PullRequests(ctx context.Context, fullname string) ([]sdk.VCSPullRequest, error) { +func (g *githubClient) PullRequests(ctx context.Context, fullname string, opts sdk.VCSPullRequestOptions) ([]sdk.VCSPullRequest, error) { var pullRequests = []PullRequest{} cacheKey := cache.Key("vcs", "github", "pullrequests", g.OAuthToken, "/repos/"+fullname+"/pulls") - opts := []getArgFunc{withETag} + githubOpts := []getArgFunc{withETag} var nextPage = "/repos/" + fullname + "/pulls" for nextPage != "" { @@ -86,7 +86,7 @@ func (g *githubClient) PullRequests(ctx context.Context, fullname string) ([]sdk break } - status, body, headers, err := g.get(ctx, nextPage, opts...) + status, body, headers, err := g.get(ctx, nextPage, githubOpts...) if err != nil { log.Warning(ctx, "githubClient.PullRequests> Error %s", err) return nil, err @@ -94,7 +94,7 @@ func (g *githubClient) PullRequests(ctx context.Context, fullname string) ([]sdk if status >= 400 { return nil, sdk.NewError(sdk.ErrUnknownError, errorAPI(body)) } - opts[0] = withETag + githubOpts[0] = withETag nextPullRequests := []PullRequest{} //Github may return 304 status because we are using conditional request with ETag based headers @@ -105,7 +105,7 @@ func (g *githubClient) PullRequests(ctx context.Context, fullname string) ([]sdk log.Error(ctx, "cannot get from cache %s: %v", cacheKey, err) } if !find { - opts[0] = withoutETag + githubOpts[0] = withoutETag log.Error(ctx, "Unable to get pullrequest (%s) from the cache", strings.ReplaceAll(cacheKey, g.OAuthToken, "")) continue } @@ -130,6 +130,21 @@ func (g *githubClient) PullRequests(ctx context.Context, fullname string) ([]sdk prResults := []sdk.VCSPullRequest{} for _, pullr := range pullRequests { + // If a state is given we want to filter PRs + switch opts.State { + case sdk.VCSPullRequestStateOpen: + if pullr.State == "closed" || pullr.Merged { + continue + } + case sdk.VCSPullRequestStateMerged: + if !pullr.Merged { + continue + } + case sdk.VCSPullRequestStateClosed: + if pullr.State != "closed" || pullr.Merged { + continue + } + } pr := pullr.ToVCSPullRequest() prResults = append(prResults, pr) } diff --git a/engine/vcs/github/client_pull_request_test.go b/engine/vcs/github/client_pull_request_test.go index 861c00fc24..ae877e1d6c 100644 --- a/engine/vcs/github/client_pull_request_test.go +++ b/engine/vcs/github/client_pull_request_test.go @@ -2,17 +2,17 @@ package github import ( "context" - "github.com/ovh/cds/sdk" "testing" "github.com/stretchr/testify/assert" "github.com/ovh/cds/engine/api/test" + "github.com/ovh/cds/sdk" ) func TestPullRequests(t *testing.T) { client := getNewAuthorizedClient(t) - prs, err := client.PullRequests(context.Background(), "ovh/cds") + prs, err := client.PullRequests(context.Background(), "ovh/cds", sdk.VCSPullRequestOptions{}) test.NoError(t, err) assert.NotEmpty(t, prs) t.Logf("%v", prs) @@ -20,7 +20,7 @@ func TestPullRequests(t *testing.T) { func TestPullRequestComment(t *testing.T) { client := getNewAuthorizedClient(t) - prs, err := client.PullRequests(context.Background(), "ovh/cds") + prs, err := client.PullRequests(context.Background(), "ovh/cds", sdk.VCSPullRequestOptions{}) test.NoError(t, err) assert.NotEmpty(t, prs) t.Logf("%v", prs) diff --git a/engine/vcs/gitlab/client_pull_request.go b/engine/vcs/gitlab/client_pull_request.go index 939b439b45..783b100f76 100644 --- a/engine/vcs/gitlab/client_pull_request.go +++ b/engine/vcs/gitlab/client_pull_request.go @@ -14,33 +14,31 @@ func (c *gitlabClient) PullRequest(ctx context.Context, repo string, id int) (sd return sdk.VCSPullRequest{}, sdk.NewErrorWithStack(err, sdk.NewErrorFrom(sdk.ErrNotFound, "cannot found a merge request for repo %s with id %d", repo, id)) } - return sdk.VCSPullRequest{ - ID: mr.IID, - Base: sdk.VCSPushEvent{ - Repo: repo, - Branch: sdk.VCSBranch{ - LatestCommit: mr.DiffRefs.BaseSha, - }, - }, - Head: sdk.VCSPushEvent{ - Repo: repo, - Branch: sdk.VCSBranch{ - LatestCommit: mr.DiffRefs.HeadSha, - }, - }, - URL: mr.WebURL, - User: sdk.VCSAuthor{ - DisplayName: mr.Author.Username, - Name: mr.Author.Name, - }, - Closed: mr.State == "closed", - Merged: mr.State == "merged", - }, nil + return toSDKPullRequest(repo, *mr), nil } // PullRequests fetch all the pull request for a repository -func (c *gitlabClient) PullRequests(context.Context, string) ([]sdk.VCSPullRequest, error) { - return []sdk.VCSPullRequest{}, nil +func (c *gitlabClient) PullRequests(ctx context.Context, repo string, opts sdk.VCSPullRequestOptions) ([]sdk.VCSPullRequest, error) { + var gitlabOpts gitlab.ListProjectMergeRequestsOptions + + switch opts.State { + case sdk.VCSPullRequestStateOpen: + gitlabOpts.State = gitlab.String("opened") + case sdk.VCSPullRequestStateMerged: + gitlabOpts.State = gitlab.String("merged") + case sdk.VCSPullRequestStateClosed: + gitlabOpts.State = gitlab.String("closed") + } + + mrs, _, err := c.client.MergeRequests.ListProjectMergeRequests(repo, &gitlabOpts) + if err != nil { + return nil, sdk.WithStack(err) + } + res := make([]sdk.VCSPullRequest, 0, len(mrs)) + for i := range mrs { + res = append(res, toSDKPullRequest(repo, *mrs[i])) + } + return res, nil } // PullRequestComment push a new comment on a pull request @@ -58,22 +56,23 @@ func (c *gitlabClient) PullRequestCreate(ctx context.Context, repo string, pr sd if err != nil { return sdk.VCSPullRequest{}, sdk.WithStack(err) } + return toSDKPullRequest(repo, *mr), nil +} +func toSDKPullRequest(repo string, mr gitlab.MergeRequest) sdk.VCSPullRequest { return sdk.VCSPullRequest{ ID: mr.IID, Base: sdk.VCSPushEvent{ Repo: repo, Branch: sdk.VCSBranch{ - ID: pr.Base.Branch.DisplayID, - DisplayID: pr.Base.Branch.DisplayID, + DisplayID: mr.TargetBranch, LatestCommit: mr.DiffRefs.BaseSha, }, }, Head: sdk.VCSPushEvent{ Repo: repo, Branch: sdk.VCSBranch{ - ID: pr.Head.Branch.DisplayID, - DisplayID: pr.Head.Branch.DisplayID, + DisplayID: mr.SourceBranch, LatestCommit: mr.DiffRefs.HeadSha, }, }, @@ -84,5 +83,5 @@ func (c *gitlabClient) PullRequestCreate(ctx context.Context, repo string, pr sd }, Closed: mr.State == "closed", Merged: mr.State == "merged", - }, nil + } } diff --git a/engine/vcs/vcs_handlers.go b/engine/vcs/vcs_handlers.go index 227259f33f..9e929fd0cf 100644 --- a/engine/vcs/vcs_handlers.go +++ b/engine/vcs/vcs_handlers.go @@ -22,6 +22,11 @@ func muxVar(r *http.Request, s string) string { return vars[s] } +// QueryString return a string from a query parameter +func QueryString(r *http.Request, s string) string { + return r.FormValue(s) +} + func (s *Service) getAllVCSServersHandler() service.Handler { return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { servers := make(map[string]sdk.VCSConfiguration, len(s.Cfg.Servers)) @@ -670,6 +675,11 @@ func (s *Service) getPullRequestsHandler() service.Handler { owner := muxVar(r, "owner") repo := muxVar(r, "repo") + state := sdk.VCSPullRequestState(QueryString(r, "state")) + if state != "" && !state.IsValid() { + return sdk.NewErrorFrom(sdk.ErrWrongRequest, "invalid given pull request state %s", state) + } + accessToken, accessTokenSecret, created, ok := getAccessTokens(ctx) if !ok { return sdk.WrapError(sdk.ErrUnauthorized, "VCS> getPullRequestsHandler> Unable to get access token headers %s %s/%s", name, owner, repo) @@ -689,7 +699,9 @@ func (s *Service) getPullRequestsHandler() service.Handler { w.Header().Set(sdk.HeaderXAccessToken, client.GetAccessToken(ctx)) } - c, err := client.PullRequests(ctx, fmt.Sprintf("%s/%s", owner, repo)) + c, err := client.PullRequests(ctx, fmt.Sprintf("%s/%s", owner, repo), sdk.VCSPullRequestOptions{ + State: state, + }) if err != nil { return sdk.WrapError(err, "Unable to get pull requests on %s/%s", owner, repo) } diff --git a/go.mod b/go.mod index e8598c3694..167e985439 100644 --- a/go.mod +++ b/go.mod @@ -106,7 +106,6 @@ require ( github.com/kr/pty v1.1.8 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/lib/pq v1.0.0 - github.com/lytics/logrus v0.0.0-20170528191427-4389a17ed024 // indirect github.com/magiconair/properties v1.8.1 // indirect github.com/mailru/easyjson v0.0.0-20171120080333-32fa128f234d // indirect github.com/marstr/guid v1.1.0 // indirect diff --git a/sdk/ascode.go b/sdk/ascode.go index 524cea0a8d..9e48855752 100644 --- a/sdk/ascode.go +++ b/sdk/ascode.go @@ -9,6 +9,7 @@ import ( type AsCodeEvent struct { ID int64 `json:"id" db:"id"` + WorkflowID int64 `json:"workflow_id" db:"workflow_id"` PullRequestID int64 `json:"pullrequest_id" db:"pullrequest_id"` PullRequestURL string `json:"pullrequest_url" db:"pullrequest_url"` Username string `json:"username" db:"username"` diff --git a/sdk/messages.go b/sdk/messages.go index 059174c4c0..04b4028e13 100644 --- a/sdk/messages.go +++ b/sdk/messages.go @@ -20,146 +20,148 @@ 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, 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} + 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} + MsgWorkflowGeneratedFromTemplateVersion = &Message{"MsgWorkflowGeneratedFromTemplateVersion", trad{FR: "Le workflow a été généré à partir du modèle de workflow: %s.", EN: "The workflow was generated from the template: %s"}, nil, RunInfoTypInfo} ) // Messages contains all sdk Messages var Messages = map[string]*Message{ - MsgAppCreated.ID: MsgAppCreated, - MsgAppUpdated.ID: MsgAppUpdated, - MsgPipelineCreated.ID: MsgPipelineCreated, - MsgPipelineCreationAborted.ID: MsgPipelineCreationAborted, - MsgPipelineExists.ID: MsgPipelineExists, - MsgAppVariablesCreated.ID: MsgAppVariablesCreated, - MsgAppKeyCreated.ID: MsgAppKeyCreated, - MsgEnvironmentExists.ID: MsgEnvironmentExists, - MsgEnvironmentCreated.ID: MsgEnvironmentCreated, - MsgEnvironmentVariableUpdated.ID: MsgEnvironmentVariableUpdated, - MsgEnvironmentVariableCannotBeUpdated.ID: MsgEnvironmentVariableCannotBeUpdated, - MsgEnvironmentVariableCreated.ID: MsgEnvironmentVariableCreated, - MsgEnvironmentVariableCannotBeCreated.ID: MsgEnvironmentVariableCannotBeCreated, - MsgEnvironmentGroupUpdated.ID: MsgEnvironmentGroupUpdated, - MsgEnvironmentGroupCannotBeUpdated.ID: MsgEnvironmentGroupCannotBeUpdated, - MsgEnvironmentGroupCreated.ID: MsgEnvironmentGroupCreated, - MsgEnvironmentGroupCannotBeCreated.ID: MsgEnvironmentGroupCannotBeCreated, - MsgEnvironmentGroupDeleted.ID: MsgEnvironmentGroupDeleted, - MsgEnvironmentGroupCannotBeDeleted.ID: MsgEnvironmentGroupCannotBeDeleted, - MsgEnvironmentKeyCreated.ID: MsgEnvironmentKeyCreated, - MsgJobNotValidActionNotFound.ID: MsgJobNotValidActionNotFound, - MsgJobNotValidInvalidActionParameter.ID: MsgJobNotValidInvalidActionParameter, - MsgPipelineGroupUpdated.ID: MsgPipelineGroupUpdated, - MsgPipelineGroupAdded.ID: MsgPipelineGroupAdded, - MsgPipelineGroupDeleted.ID: MsgPipelineGroupDeleted, - MsgPipelineStageUpdated.ID: MsgPipelineStageUpdated, - MsgPipelineStageUpdating.ID: MsgPipelineStageUpdating, - MsgPipelineStageDeletingOldJobs.ID: MsgPipelineStageDeletingOldJobs, - MsgPipelineStageInsertingNewJobs.ID: MsgPipelineStageInsertingNewJobs, - MsgPipelineStageAdded.ID: MsgPipelineStageAdded, - MsgPipelineStageDeleted.ID: MsgPipelineStageDeleted, - MsgPipelineJobUpdated.ID: MsgPipelineJobUpdated, - MsgPipelineJobAdded.ID: MsgPipelineJobAdded, - MsgPipelineJobDeleted.ID: MsgPipelineJobDeleted, - MsgSpawnInfoHatcheryStarts.ID: MsgSpawnInfoHatcheryStarts, - MsgSpawnInfoHatcheryErrorSpawn.ID: MsgSpawnInfoHatcheryErrorSpawn, - MsgSpawnInfoHatcheryStartsSuccessfully.ID: MsgSpawnInfoHatcheryStartsSuccessfully, - MsgSpawnInfoHatcheryStartDockerPull.ID: MsgSpawnInfoHatcheryStartDockerPull, - MsgSpawnInfoHatcheryEndDockerPull.ID: MsgSpawnInfoHatcheryEndDockerPull, - MsgSpawnInfoHatcheryEndDockerPullErr.ID: MsgSpawnInfoHatcheryEndDockerPullErr, - MsgSpawnInfoDeprecatedModel.ID: MsgSpawnInfoDeprecatedModel, - MsgSpawnInfoWorkerEnd.ID: MsgSpawnInfoWorkerEnd, - MsgSpawnInfoJobInQueue.ID: MsgSpawnInfoJobInQueue, - MsgSpawnInfoJobTaken.ID: MsgSpawnInfoJobTaken, - MsgSpawnInfoJobTakenWorkerVersion.ID: MsgSpawnInfoJobTakenWorkerVersion, - MsgSpawnInfoWorkerForJob.ID: MsgSpawnInfoWorkerForJob, - MsgSpawnInfoWorkerForJobError.ID: MsgSpawnInfoWorkerForJobError, - MsgSpawnInfoJobError.ID: MsgSpawnInfoJobError, - MsgWorkflowStarting.ID: MsgWorkflowStarting, - MsgWorkflowError.ID: MsgWorkflowError, - MsgWorkflowConditionError.ID: MsgWorkflowConditionError, - MsgWorkflowNodeStop.ID: MsgWorkflowNodeStop, - MsgWorkflowNodeMutex.ID: MsgWorkflowNodeMutex, - MsgWorkflowNodeMutexRelease.ID: MsgWorkflowNodeMutexRelease, - MsgWorkflowImportedUpdated.ID: MsgWorkflowImportedUpdated, - MsgWorkflowImportedInserted.ID: MsgWorkflowImportedInserted, - MsgSpawnInfoHatcheryCannotStartJob.ID: MsgSpawnInfoHatcheryCannotStartJob, - MsgWorkflowRunBranchDeleted.ID: MsgWorkflowRunBranchDeleted, - MsgWorkflowTemplateImportedInserted.ID: MsgWorkflowTemplateImportedInserted, - MsgWorkflowTemplateImportedUpdated.ID: MsgWorkflowTemplateImportedUpdated, - MsgWorkflowErrorBadPipelineName.ID: MsgWorkflowErrorBadPipelineName, - MsgWorkflowErrorBadApplicationName.ID: MsgWorkflowErrorBadApplicationName, - MsgWorkflowErrorBadEnvironmentName.ID: MsgWorkflowErrorBadEnvironmentName, - MsgWorkflowErrorBadIntegrationName.ID: MsgWorkflowErrorBadIntegrationName, - MsgWorkflowErrorBadCdsDir.ID: MsgWorkflowErrorBadCdsDir, - MsgWorkflowErrorUnknownKey.ID: MsgWorkflowErrorUnknownKey, - MsgWorkflowErrorBadVCSStrategy.ID: MsgWorkflowErrorBadVCSStrategy, - MsgWorkflowDeprecatedVersion.ID: MsgWorkflowDeprecatedVersion, + MsgAppCreated.ID: MsgAppCreated, + MsgAppUpdated.ID: MsgAppUpdated, + MsgPipelineCreated.ID: MsgPipelineCreated, + MsgPipelineCreationAborted.ID: MsgPipelineCreationAborted, + MsgPipelineExists.ID: MsgPipelineExists, + MsgAppVariablesCreated.ID: MsgAppVariablesCreated, + MsgAppKeyCreated.ID: MsgAppKeyCreated, + MsgEnvironmentExists.ID: MsgEnvironmentExists, + MsgEnvironmentCreated.ID: MsgEnvironmentCreated, + MsgEnvironmentVariableUpdated.ID: MsgEnvironmentVariableUpdated, + MsgEnvironmentVariableCannotBeUpdated.ID: MsgEnvironmentVariableCannotBeUpdated, + MsgEnvironmentVariableCreated.ID: MsgEnvironmentVariableCreated, + MsgEnvironmentVariableCannotBeCreated.ID: MsgEnvironmentVariableCannotBeCreated, + MsgEnvironmentGroupUpdated.ID: MsgEnvironmentGroupUpdated, + MsgEnvironmentGroupCannotBeUpdated.ID: MsgEnvironmentGroupCannotBeUpdated, + MsgEnvironmentGroupCreated.ID: MsgEnvironmentGroupCreated, + MsgEnvironmentGroupCannotBeCreated.ID: MsgEnvironmentGroupCannotBeCreated, + MsgEnvironmentGroupDeleted.ID: MsgEnvironmentGroupDeleted, + MsgEnvironmentGroupCannotBeDeleted.ID: MsgEnvironmentGroupCannotBeDeleted, + MsgEnvironmentKeyCreated.ID: MsgEnvironmentKeyCreated, + MsgJobNotValidActionNotFound.ID: MsgJobNotValidActionNotFound, + MsgJobNotValidInvalidActionParameter.ID: MsgJobNotValidInvalidActionParameter, + MsgPipelineGroupUpdated.ID: MsgPipelineGroupUpdated, + MsgPipelineGroupAdded.ID: MsgPipelineGroupAdded, + MsgPipelineGroupDeleted.ID: MsgPipelineGroupDeleted, + MsgPipelineStageUpdated.ID: MsgPipelineStageUpdated, + MsgPipelineStageUpdating.ID: MsgPipelineStageUpdating, + MsgPipelineStageDeletingOldJobs.ID: MsgPipelineStageDeletingOldJobs, + MsgPipelineStageInsertingNewJobs.ID: MsgPipelineStageInsertingNewJobs, + MsgPipelineStageAdded.ID: MsgPipelineStageAdded, + MsgPipelineStageDeleted.ID: MsgPipelineStageDeleted, + MsgPipelineJobUpdated.ID: MsgPipelineJobUpdated, + MsgPipelineJobAdded.ID: MsgPipelineJobAdded, + MsgPipelineJobDeleted.ID: MsgPipelineJobDeleted, + MsgSpawnInfoHatcheryStarts.ID: MsgSpawnInfoHatcheryStarts, + MsgSpawnInfoHatcheryErrorSpawn.ID: MsgSpawnInfoHatcheryErrorSpawn, + MsgSpawnInfoHatcheryStartsSuccessfully.ID: MsgSpawnInfoHatcheryStartsSuccessfully, + MsgSpawnInfoHatcheryStartDockerPull.ID: MsgSpawnInfoHatcheryStartDockerPull, + MsgSpawnInfoHatcheryEndDockerPull.ID: MsgSpawnInfoHatcheryEndDockerPull, + MsgSpawnInfoHatcheryEndDockerPullErr.ID: MsgSpawnInfoHatcheryEndDockerPullErr, + MsgSpawnInfoDeprecatedModel.ID: MsgSpawnInfoDeprecatedModel, + MsgSpawnInfoWorkerEnd.ID: MsgSpawnInfoWorkerEnd, + MsgSpawnInfoJobInQueue.ID: MsgSpawnInfoJobInQueue, + MsgSpawnInfoJobTaken.ID: MsgSpawnInfoJobTaken, + MsgSpawnInfoJobTakenWorkerVersion.ID: MsgSpawnInfoJobTakenWorkerVersion, + MsgSpawnInfoWorkerForJob.ID: MsgSpawnInfoWorkerForJob, + MsgSpawnInfoWorkerForJobError.ID: MsgSpawnInfoWorkerForJobError, + MsgSpawnInfoJobError.ID: MsgSpawnInfoJobError, + MsgWorkflowStarting.ID: MsgWorkflowStarting, + MsgWorkflowError.ID: MsgWorkflowError, + MsgWorkflowConditionError.ID: MsgWorkflowConditionError, + MsgWorkflowNodeStop.ID: MsgWorkflowNodeStop, + MsgWorkflowNodeMutex.ID: MsgWorkflowNodeMutex, + MsgWorkflowNodeMutexRelease.ID: MsgWorkflowNodeMutexRelease, + MsgWorkflowImportedUpdated.ID: MsgWorkflowImportedUpdated, + MsgWorkflowImportedInserted.ID: MsgWorkflowImportedInserted, + MsgSpawnInfoHatcheryCannotStartJob.ID: MsgSpawnInfoHatcheryCannotStartJob, + MsgWorkflowRunBranchDeleted.ID: MsgWorkflowRunBranchDeleted, + MsgWorkflowTemplateImportedInserted.ID: MsgWorkflowTemplateImportedInserted, + MsgWorkflowTemplateImportedUpdated.ID: MsgWorkflowTemplateImportedUpdated, + MsgWorkflowErrorBadPipelineName.ID: MsgWorkflowErrorBadPipelineName, + MsgWorkflowErrorBadApplicationName.ID: MsgWorkflowErrorBadApplicationName, + MsgWorkflowErrorBadEnvironmentName.ID: MsgWorkflowErrorBadEnvironmentName, + MsgWorkflowErrorBadIntegrationName.ID: MsgWorkflowErrorBadIntegrationName, + MsgWorkflowErrorBadCdsDir.ID: MsgWorkflowErrorBadCdsDir, + MsgWorkflowErrorUnknownKey.ID: MsgWorkflowErrorUnknownKey, + MsgWorkflowErrorBadVCSStrategy.ID: MsgWorkflowErrorBadVCSStrategy, + MsgWorkflowDeprecatedVersion.ID: MsgWorkflowDeprecatedVersion, + MsgWorkflowGeneratedFromTemplateVersion.ID: MsgWorkflowGeneratedFromTemplateVersion, } //Message represent a struc format translated messages diff --git a/sdk/repositories_manager.go b/sdk/repositories_manager.go index 5fac954899..a6b52fbb76 100644 --- a/sdk/repositories_manager.go +++ b/sdk/repositories_manager.go @@ -97,6 +97,27 @@ type VCSPullRequest struct { Revision string `json:"revision"` } +type VCSPullRequestOptions struct { + State VCSPullRequestState +} + +const ( + VCSPullRequestStateAll VCSPullRequestState = "all" + VCSPullRequestStateOpen VCSPullRequestState = "open" + VCSPullRequestStateClosed VCSPullRequestState = "closed" + VCSPullRequestStateMerged VCSPullRequestState = "merged" +) + +type VCSPullRequestState string + +func (s VCSPullRequestState) IsValid() bool { + switch s { + case VCSPullRequestStateAll, VCSPullRequestStateOpen, VCSPullRequestStateClosed, VCSPullRequestStateMerged: + return true + } + return false +} + type VCSPullRequestCommentRequest struct { VCSPullRequest Message string `json:"message"` diff --git a/sdk/repositories_operation.go b/sdk/repositories_operation.go index 57b389483d..1fe070ba7f 100644 --- a/sdk/repositories_operation.go +++ b/sdk/repositories_operation.go @@ -19,9 +19,9 @@ type Operation struct { RepositoryInfo *OperationRepositoryInfo `json:"repository_info,omitempty"` Date *time.Time `json:"date,omitempty"` User struct { - Username string `json:"username" db:"-" cli:"-"` - Fullname string `json:"fullname" db:"-" cli:"-"` - Email string `json:"email" db:"-" cli:"-"` + Username string `json:"username,omitempty" db:"-" cli:"-"` + Fullname string `json:"fullname,omitempty" db:"-" cli:"-"` + Email string `json:"email,omitempty" db:"-" cli:"-"` } `json:"user,omitempty"` } diff --git a/sdk/vcs.go b/sdk/vcs.go index c8d6049cd8..00d310d0be 100644 --- a/sdk/vcs.go +++ b/sdk/vcs.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io" + "net/http" "time" ) @@ -33,15 +34,24 @@ type VCSConfiguration struct { SSHPort int `json:"sshport"` } -// VCSServer is an interce for a OAuth VCS Server. The goal of this interface is to return a VCSAuthorizedClient -type VCSServer interface { +type VCSServerCommon interface { AuthorizeRedirect(context.Context) (string, string, error) AuthorizeToken(context.Context, string, string) (string, string, error) +} + +// VCSServer is an interface for a OAuth VCS Server. The goal of this interface is to return a VCSAuthorizedClient. +type VCSServer interface { + VCSServerCommon GetAuthorizedClient(context.Context, string, string, int64) (VCSAuthorizedClient, error) } -// VCSAuthorizedClient is an interface for a connected client on a VCS Server. -type VCSAuthorizedClient interface { +type VCSServerService interface { + VCSServerCommon + GetAuthorizedClient(context.Context, string, string, int64) (VCSAuthorizedClientService, error) +} + +// VCSAuthorizedClientCommon is an interface for a connected client on a VCS Server. +type VCSAuthorizedClientCommon interface { //Repos Repos(context.Context) ([]VCSRepo, error) RepoByFullname(ctx context.Context, fullname string) (VCSRepo, error) @@ -59,10 +69,9 @@ type VCSAuthorizedClient interface { CommitsBetweenRefs(ctx context.Context, repo, base, head string) ([]VCSCommit, error) // PullRequests - PullRequest(context.Context, string, int) (VCSPullRequest, error) - PullRequests(context.Context, string) ([]VCSPullRequest, error) - PullRequestComment(context.Context, string, VCSPullRequestCommentRequest) error - PullRequestCreate(context.Context, string, VCSPullRequest) (VCSPullRequest, error) + PullRequest(ctx context.Context, repo string, id int) (VCSPullRequest, error) + PullRequestComment(ctx context.Context, repo string, c VCSPullRequestCommentRequest) error + PullRequestCreate(ctx context.Context, repo string, pr VCSPullRequest) (VCSPullRequest, error) //Hooks CreateHook(ctx context.Context, repo string, hook *VCSHook) error @@ -95,6 +104,26 @@ type VCSAuthorizedClient interface { GetAccessToken(ctx context.Context) string } +type VCSAuthorizedClient interface { + VCSAuthorizedClientCommon + PullRequests(ctx context.Context, repo string, opts VCSPullRequestOptions) ([]VCSPullRequest, error) +} + +type VCSAuthorizedClientService interface { + VCSAuthorizedClientCommon + PullRequests(ctx context.Context, repo string, mods ...VCSRequestModifier) ([]VCSPullRequest, error) +} + +type VCSRequestModifier func(r *http.Request) + +func VCSRequestModifierWithState(state VCSPullRequestState) VCSRequestModifier { + return func(r *http.Request) { + q := r.URL.Query() + q.Set("state", string(state)) + r.URL.RawQuery = q.Encode() + } +} + // GetDefaultBranch return the default branch func GetDefaultBranch(branches []VCSBranch) VCSBranch { for _, branch := range branches { diff --git a/ui/src/app/app.service.ts b/ui/src/app/app.service.ts index f4d3e8a4c3..db629fcad1 100644 --- a/ui/src/app/app.service.ts +++ b/ui/src/app/app.service.ts @@ -71,8 +71,7 @@ export class AppService { } if (event.type_event.indexOf(EventType.ASCODE) === 0) { if (event.username === this._store.selectSnapshot(AuthenticationState.user).username) { - let e: AsCodeEvent = event.payload['as_code_event']; - this._store.dispatch(e); + this._store.dispatch(new AsCodeEvent(event.payload['as_code_event'])); } return; } @@ -203,7 +202,7 @@ export class AppService { this.routeParams['appName'] && this.routeParams['appName'] === event.application_name && event.username !== this._store.selectSnapshot(AuthenticationState.user).username) { this._toast.info('', this._translate.instant('application_deleted_by', - {appName: this.routeParams['appName'], username: event.username})); + { appName: this.routeParams['appName'], username: event.username })); this._router.navigate(['/project'], this.routeParams['key']); } this._store.dispatch(new ClearCacheApplication()); diff --git a/ui/src/app/service/ascode/ascode.service.ts b/ui/src/app/service/ascode/ascode.service.ts index e9bf43c280..22f67d5864 100644 --- a/ui/src/app/service/ascode/ascode.service.ts +++ b/ui/src/app/service/ascode/ascode.service.ts @@ -1,4 +1,4 @@ -import { HttpClient, HttpParams } from '@angular/common/http'; +import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Store } from '@ngxs/store'; import { ResyncEvents } from 'app/store/ascode.action'; @@ -11,16 +11,8 @@ export class AscodeService { private _store: Store ) { } - resyncPRAsCode(projectKey: string, appName: string, repo?: string): Observable { - let params = new HttpParams(); - if (repo) { - params = params.append('repo', repo); - } - if (appName) { - params = params.append('appName', appName); - } - - return this._http.post(`/project/${projectKey}/ascode/events/resync`, null, { params }) + resyncPRAsCode(projectKey: string, workflowName: string): Observable { + return this._http.post(`/project/${projectKey}/workflows/${workflowName}/ascode/events/resync`, null) .map(() => { this._store.dispatch(new ResyncEvents()); return true; diff --git a/ui/src/app/service/pipeline/pipeline.service.ts b/ui/src/app/service/pipeline/pipeline.service.ts index d1934a05e6..d747c048ad 100644 --- a/ui/src/app/service/pipeline/pipeline.service.ts +++ b/ui/src/app/service/pipeline/pipeline.service.ts @@ -45,7 +45,6 @@ export class PipelineService { updateAsCode(key: string, pipeline: Pipeline, branch, message: string): Observable { let params = new HttpParams(); params = params.append('branch', branch); - params = params.append('repo', pipeline.from_repository); params = params.append('message', message) return this._http.put(`/project/${key}/pipeline/${pipeline.name}/ascode`, pipeline, { params }); } diff --git a/ui/src/app/service/workflow-template/workflow-template.service.ts b/ui/src/app/service/workflow-template/workflow-template.service.ts index 18537b35ec..f2ae883155 100644 --- a/ui/src/app/service/workflow-template/workflow-template.service.ts +++ b/ui/src/app/service/workflow-template/workflow-template.service.ts @@ -2,6 +2,7 @@ import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { AuditWorkflowTemplate } from 'app/model/audit.model'; +import { Operation } from 'app/model/operation.model'; import { WorkflowTemplate, WorkflowTemplateApplyResult, @@ -47,6 +48,16 @@ export class WorkflowTemplateService { }); } + applyAsCode(groupName: string, templateSlug: string, req: WorkflowTemplateRequest, + branch: string, message: string): Observable { + let params = new HttpParams(); + params = params.append('import', 'true'); + params = params.append('branch', branch); + params = params.append('message', message) + return this._http.post(`/template/${groupName}/${templateSlug}/apply`, + req, { params }); + } + deleteInstance(wt: WorkflowTemplate, wti: WorkflowTemplateInstance): Observable { return this._http.delete(`/template/${wt.group.name}/${wt.slug}/instance/${wti.id}`); } @@ -72,6 +83,15 @@ export class WorkflowTemplateService { return this._http.post(`/template/${groupName}/${templateSlug}/bulk`, req); } + bulkAsCode(groupName: string, templateSlug: string, req: WorkflowTemplateBulk, + branch: string, message: string): Observable { + let params = new HttpParams(); + params = params.append('branch', branch); + params = params.append('message', message) + return this._http.post(`/template/${groupName}/${templateSlug}/bulk`, + req, { params }); + } + getBulk(groupName: string, templateSlug: string, id: number): Observable { return this._http.get(`/template/${groupName}/${templateSlug}/bulk/${id}`); } diff --git a/ui/src/app/shared/ascode/events/ascode.event.component.ts b/ui/src/app/shared/ascode/events/ascode.event.component.ts index 0f9a34c9fb..1573a1cfac 100644 --- a/ui/src/app/shared/ascode/events/ascode.event.component.ts +++ b/ui/src/app/shared/ascode/events/ascode.event.component.ts @@ -1,6 +1,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input } from '@angular/core'; import { AsCodeEvents } from 'app/model/ascode.model'; import { Project } from 'app/model/project.model'; +import { Workflow } from 'app/model/workflow.model'; import { AscodeService } from 'app/service/ascode/ascode.service'; import { finalize } from 'rxjs/operators'; @@ -12,9 +13,8 @@ import { finalize } from 'rxjs/operators'; }) export class AsCodeEventComponent { @Input() events: Array; - @Input() repo: string; - @Input() appName: string; @Input() project: Project; + @Input() workflow: Workflow; loadingPopupButton = false; @@ -25,7 +25,7 @@ export class AsCodeEventComponent { resyncEvents(): void { this.loadingPopupButton = true; - this._ascodeService.resyncPRAsCode(this.project.key, this.appName, this.repo) + this._ascodeService.resyncPRAsCode(this.project.key, this.workflow.name) .pipe(finalize(() => { this.loadingPopupButton = false; this._cd.markForCheck(); diff --git a/ui/src/app/shared/ascode/events/ascode.event.html b/ui/src/app/shared/ascode/events/ascode.event.html index 118ff54f66..feae544f17 100644 --- a/ui/src/app/shared/ascode/events/ascode.event.html +++ b/ui/src/app/shared/ascode/events/ascode.event.html @@ -1,7 +1,7 @@

- {{ 'pipeline_from_repository' | translate: {repo: repo} }} - {{ 'workflow_from_repository_pending' | translate }} + {{ 'pipeline_from_repository' | translate: {repo: repo} }} + {{ 'workflow_from_repository_pending' | translate }}

  • diff --git a/ui/src/app/shared/ascode/save-form/ascode.save-form.component.ts b/ui/src/app/shared/ascode/save-form/ascode.save-form.component.ts new file mode 100644 index 0000000000..a09ce50786 --- /dev/null +++ b/ui/src/app/shared/ascode/save-form/ascode.save-form.component.ts @@ -0,0 +1,81 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Operation } from 'app/model/operation.model'; +import { Project } from 'app/model/project.model'; +import { Workflow } from 'app/model/workflow.model'; +import { ApplicationWorkflowService } from 'app/service/application/application.workflow.service'; +import { AutoUnsubscribe } from 'app/shared/decorator/autoUnsubscribe'; +import { finalize, first } from 'rxjs/operators'; + +export class ParamData { + branch_name: string; + commit_message: string; +} + +@Component({ + selector: 'app-ascode-save-form', + templateUrl: './ascode.save-form.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +@AutoUnsubscribe() +export class AsCodeSaveFormComponent implements OnInit { + @Input() project: Project; + @Input() workflow: Workflow; + @Input() operation: Operation; + @Output() paramChange = new EventEmitter(); + + loading: boolean; + selectedBranch: string; + commitMessage: string; + branches: Array; + + constructor( + private _cd: ChangeDetectorRef, + private _awService: ApplicationWorkflowService + ) { } + + ngOnInit() { + this.paramChange.emit(new ParamData()); + + if (!this.workflow) { + return; + } + + let rootAppId = this.workflow.workflow_data.node.context.application_id; + let rootApp = this.workflow.applications[rootAppId]; + + this.loading = true; + this._cd.markForCheck(); + this._awService.getVCSInfos(this.project.key, rootApp.name, '') + .pipe(first()) + .pipe(finalize(() => { + this.loading = false; + this._cd.markForCheck(); + })) + .subscribe(vcsinfos => { + if (vcsinfos && vcsinfos.branches) { + this.branches = vcsinfos.branches.map(b => b.display_id); + } + }); + } + + optionsFilter = (opts: Array, query: string): Array => { + this.selectedBranch = query; + let result = Array(); + opts.forEach(o => { + if (o.indexOf(query) > -1) { + result.push(o); + } + }); + if (result.indexOf(query) === -1) { + result.push(query); + } + return result; + }; + + changeParam(): void { + this.paramChange.emit({ + branch_name: this.selectedBranch, + commit_message: this.commitMessage + }) + } +} diff --git a/ui/src/app/shared/ascode/save-form/ascode.save-form.html b/ui/src/app/shared/ascode/save-form/ascode.save-form.html new file mode 100644 index 0000000000..e2531398d4 --- /dev/null +++ b/ui/src/app/shared/ascode/save-form/ascode.save-form.html @@ -0,0 +1,34 @@ +
    +
    +
    +
    + + + + + +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
    + {{operation.error}} +
    +
    + {{ 'workflow_as_code_pr_success' | translate }} + {{ operation.setup.push.pr_link }} +
    +
    +
    +
    +
    diff --git a/ui/src/app/shared/modal/save-as-code/update.as.code.component.ts b/ui/src/app/shared/ascode/save-modal/ascode.save-modal.component.ts similarity index 67% rename from ui/src/app/shared/modal/save-as-code/update.as.code.component.ts rename to ui/src/app/shared/ascode/save-modal/ascode.save-modal.component.ts index 16112b8acd..2990ef73f4 100644 --- a/ui/src/app/shared/modal/save-as-code/update.as.code.component.ts +++ b/ui/src/app/shared/ascode/save-modal/ascode.save-modal.component.ts @@ -6,42 +6,40 @@ import { Pipeline } from 'app/model/pipeline.model'; import { Project } from 'app/model/project.model'; import { Workflow } from 'app/model/workflow.model'; import { PipelineService } from 'app/service/pipeline/pipeline.service'; -import { ApplicationWorkflowService } from 'app/service/services.module'; import { WorkflowService } from 'app/service/workflow/workflow.service'; import { AutoUnsubscribe } from 'app/shared/decorator/autoUnsubscribe'; import { ToastService } from 'app/shared/toast/ToastService'; import { Observable, Subscription } from 'rxjs'; -import { finalize, first } from 'rxjs/operators'; +import { finalize } from 'rxjs/operators'; +import { ParamData } from '../save-form/ascode.save-form.component'; @Component({ - selector: 'app-update-ascode', - templateUrl: './update-ascode.html', + selector: 'app-ascode-save-modal', + templateUrl: './ascode.save-modal.html', changeDetection: ChangeDetectionStrategy.OnPush }) @AutoUnsubscribe() -export class UpdateAsCodeComponent { +export class AsCodeSaveModalComponent { + @ViewChild('updateAsCodeModal') + public myModalTemplate: ModalTemplate; + modal: SuiActiveModal; + modalConfig: TemplateModalConfig; + @Input() project: Project; - @Input() appName: string; + @Input() workflow: Workflow; @Input() name: string; dataToSave: any; dataType: string; - - @ViewChild('updateAsCodeModal') - public myModalTemplate: ModalTemplate; - modal: SuiActiveModal; - modalConfig: TemplateModalConfig; - branches: Array; - selectedBranch: string; - commitMessage: string; loading: boolean; webworkerSub: Subscription; - ope: Operation; + asCodeOperation: Operation; pollingOperationSub: Subscription; + parameters: ParamData; + repositoryFullname: string; constructor( private _modalService: SuiModalService, - private _awService: ApplicationWorkflowService, private _cd: ChangeDetectorRef, private _toast: ToastService, private _translate: TranslateService, @@ -53,50 +51,42 @@ export class UpdateAsCodeComponent { this.loading = false; this.dataToSave = data; this.dataType = type; + + if (this.workflow && this.workflow.workflow_data.node.context) { + let rootAppID = this.workflow.workflow_data.node.context.application_id; + if (rootAppID) { + let rootApp = this.workflow.applications[rootAppID]; + if (rootApp.repository_fullname) { + this.repositoryFullname = rootApp.repository_fullname; + } + } + } + this.modalConfig = new TemplateModalConfig(this.myModalTemplate); this.modal = this._modalService.open(this.modalConfig); - this._awService.getVCSInfos(this.project.key, this.appName, '').pipe(first()) - .subscribe(vcsinfos => { - if (vcsinfos && vcsinfos.branches) { - this.branches = vcsinfos.branches.map(b => b.display_id); - } - this._cd.markForCheck(); - }); } close() { this.modal.approve(true); } - optionsFilter = (opts: Array, query: string): Array => { - this.selectedBranch = query; - let result = Array(); - opts.forEach(o => { - if (o.indexOf(query) > -1) { - result.push(o); - } - }); - if (result.indexOf(query) === -1) { - result.push(query); - } - return result; - }; - save(): void { switch (this.dataType) { case 'workflow': this.loading = true; - this._workflowService.updateAsCode(this.project.key, this.name, this.selectedBranch, - this.commitMessage, this.dataToSave).subscribe(o => { - this.ope = o; - this.startPollingOperation((this.dataToSave).name); + this._cd.markForCheck(); + this._workflowService.updateAsCode(this.project.key, this.name, this.parameters.branch_name, + this.parameters.commit_message, this.dataToSave).subscribe(o => { + this.asCodeOperation = o; + this.startPollingOperation(this.name); }); break; case 'pipeline': this.loading = true; + this._cd.markForCheck(); this._pipService.updateAsCode(this.project.key, this.dataToSave, - this.selectedBranch, this.commitMessage).subscribe(o => { - this.ope = o; + this.parameters.branch_name, this.parameters.commit_message).subscribe(o => { + this.asCodeOperation = o; this.startPollingOperation((this.dataToSave).workflow_ascode_holder.name); }); break; @@ -107,14 +97,18 @@ export class UpdateAsCodeComponent { startPollingOperation(workflowName: string) { this.pollingOperationSub = Observable.interval(1000) - .mergeMap(_ => this._workflowService.getAsCodeOperation(this.project.key, workflowName, this.ope.uuid)) + .mergeMap(_ => this._workflowService.getAsCodeOperation(this.project.key, workflowName, this.asCodeOperation.uuid)) .first(o => o.status > 1) .pipe(finalize(() => { this.loading = false; this._cd.markForCheck(); })) .subscribe(o => { - this.ope = o; + this.asCodeOperation = o; }); } + + onParamChange(param: ParamData): void { + this.parameters = param; + } } diff --git a/ui/src/app/shared/ascode/save-modal/ascode.save-modal.html b/ui/src/app/shared/ascode/save-modal/ascode.save-modal.html new file mode 100644 index 0000000000..ab64a31f3a --- /dev/null +++ b/ui/src/app/shared/ascode/save-modal/ascode.save-modal.html @@ -0,0 +1,18 @@ + +
    {{ 'ascode_modal_title' | translate }}
    +
    +
    + {{'ascode_save_modal_info_line_1' | translate: {repo: repositoryFullname} }} +
    + + +
    +
    +
    + {{'common_cancel' | translate}} +
    +
    + {{'btn_save' | translate}}
    +
    +
    diff --git a/ui/src/app/shared/modal/save-as-code/update-ascode.html b/ui/src/app/shared/modal/save-as-code/update-ascode.html deleted file mode 100644 index 7a5d67e48f..0000000000 --- a/ui/src/app/shared/modal/save-as-code/update-ascode.html +++ /dev/null @@ -1,35 +0,0 @@ - -
    {{ 'ascode_modal_title' | translate }}
    -
    -
    -
    -
    - - - - - -
    -
    - - -
    -
    -
    -
    {{ 'common_loading' | translate }}
    -
    - {{ 'workflow_as_code_pr_success' | translate }} - {{ ope.setup.push.pr_link }} -
    -
    - {{ope.error}} -
    -
    -
    -
    {{'common_cancel' | translate}} -
    -
    - {{'btn_save' | translate}}
    -
    -
    diff --git a/ui/src/app/shared/shared.module.ts b/ui/src/app/shared/shared.module.ts index 76212d7fd1..4ca1111d9f 100644 --- a/ui/src/app/shared/shared.module.ts +++ b/ui/src/app/shared/shared.module.ts @@ -9,10 +9,11 @@ import { NgxChartsModule } from '@swimlane/ngx-charts'; import { AuthenticationGuard } from 'app/guard/authentication.guard'; import { NoAuthenticationGuard } from 'app/guard/no-authentication.guard'; import { AsCodeEventComponent } from 'app/shared/ascode/events/ascode.event.component'; +import { AsCodeSaveFormComponent } from 'app/shared/ascode/save-form/ascode.save-form.component'; +import { AsCodeSaveModalComponent } from 'app/shared/ascode/save-modal/ascode.save-modal.component'; import { ConditionsComponent } from 'app/shared/conditions/conditions.component'; import { GroupFormComponent } from 'app/shared/group/form/group.form.component'; import { AutoFocusInputComponent } from 'app/shared/input/autofocus/autofocus.input.component'; -import { UpdateAsCodeComponent } from 'app/shared/modal/save-as-code/update.as.code.component'; import { SelectFilterComponent } from 'app/shared/select/select.component'; import { WorkflowHookMenuEditComponent } from 'app/shared/workflow/menu/edit-hook/menu.edit.hook.component'; import { WorkflowWizardNodeConditionComponent } from 'app/shared/workflow/wizard/conditions/wizard.conditions.component'; @@ -181,7 +182,8 @@ import { ZoneComponent } from './zone/zone.component'; StatusIconComponent, TabsComponent, TruncatePipe, - UpdateAsCodeComponent, + AsCodeSaveModalComponent, + AsCodeSaveFormComponent, UploadButtonComponent, UsageApplicationsComponent, UsageComponent, @@ -287,7 +289,8 @@ import { ZoneComponent } from './zone/zone.component'; TranslateModule, TruncatePipe, SafeHtmlPipe, - UpdateAsCodeComponent, + AsCodeSaveModalComponent, + AsCodeSaveFormComponent, VariableComponent, VariableFormComponent, VariableValueComponent, diff --git a/ui/src/app/shared/workflow-template/apply-form/workflow-template.apply-form.component.ts b/ui/src/app/shared/workflow-template/apply-form/workflow-template.apply-form.component.ts index b640691ee6..6c886c39e4 100644 --- a/ui/src/app/shared/workflow-template/apply-form/workflow-template.apply-form.component.ts +++ b/ui/src/app/shared/workflow-template/apply-form/workflow-template.apply-form.component.ts @@ -8,6 +8,7 @@ import { Output } from '@angular/core'; import { Router } from '@angular/router'; +import { Operation } from 'app/model/operation.model'; import { Project } from 'app/model/project.model'; import { ParamData, @@ -18,6 +19,10 @@ import { } from 'app/model/workflow-template.model'; import { Workflow } from 'app/model/workflow.model'; import { WorkflowTemplateService } from 'app/service/workflow-template/workflow-template.service'; +import { WorkflowService } from 'app/service/workflow/workflow.service'; +import { ParamData as AsCodeParamData } from 'app/shared/ascode/save-form/ascode.save-form.component'; +import { AutoUnsubscribe } from 'app/shared/decorator/autoUnsubscribe'; +import { Observable, Subscription } from 'rxjs'; import { finalize, first } from 'rxjs/operators'; @Component({ @@ -26,6 +31,7 @@ import { finalize, first } from 'rxjs/operators'; styleUrls: ['./workflow-template.apply-form.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) +@AutoUnsubscribe() export class WorkflowTemplateApplyFormComponent implements OnChanges { @Input() project: Project; @Input() workflow: Workflow; @@ -40,14 +46,22 @@ export class WorkflowTemplateApplyFormComponent implements OnChanges { parameterName: string; parameterValues: ParamData; detached: boolean; + asCodeOperation: Operation; + pollingOperationSub: Subscription; + asCodeApply: boolean; + asCodeParameters: AsCodeParamData; + validFields: boolean; constructor( private _workflowTemplateService: WorkflowTemplateService, - private _router: Router, private _cd: ChangeDetectorRef + private _router: Router, + private _cd: ChangeDetectorRef, + private _workflowService: WorkflowService ) { } ngOnChanges() { this.parameterName = this.workflowTemplateInstance ? this.workflowTemplateInstance.request.workflow_name : ''; + this.asCodeApply = this.workflow && !!this.workflow.from_repository; } applyTemplate() { @@ -60,6 +74,18 @@ export class WorkflowTemplateApplyFormComponent implements OnChanges { this.result = null; this.loading = true; + this._cd.markForCheck(); + + if (this.asCodeApply) { + this._workflowTemplateService.applyAsCode(this.workflowTemplate.group.name, this.workflowTemplate.slug, req, + this.asCodeParameters.branch_name, this.asCodeParameters.commit_message) + .subscribe(o => { + this.asCodeOperation = o; + this.startPollingOperation(this.workflow.name); + }); + return; + } + this._workflowTemplateService.apply(this.workflowTemplate.group.name, this.workflowTemplate.slug, req) .pipe(first(), finalize(() => { this.loading = false; @@ -99,16 +125,40 @@ export class WorkflowTemplateApplyFormComponent implements OnChanges { changeParam(values: { [key: string]: string; }) { this.parameterValues = values; + this.validateParam(); } clickDetach() { this._workflowTemplateService.deleteInstance(this.workflowTemplate, this.workflowTemplateInstance) .subscribe(() => { - this.clickClose(); - }); + this.clickClose(); + }); } onSelectDetachChange(e: any) { this.detached = !this.detached; } + + startPollingOperation(workflowName: string) { + this.pollingOperationSub = Observable.interval(1000) + .mergeMap(_ => this._workflowService.getAsCodeOperation(this.project.key, workflowName, this.asCodeOperation.uuid)) + .first(o => o.status > 1) + .pipe(finalize(() => { + this.loading = false; + this._cd.markForCheck(); + })) + .subscribe(o => { + this.asCodeOperation = o; + }); + } + + onAsCodeParamChange(param: AsCodeParamData): void { + this.asCodeParameters = param; + this.validateParam(); + } + + validateParam() { + this.validFields = !this.asCodeApply || (this.asCodeParameters && + !!this.asCodeParameters.branch_name && !!this.asCodeParameters.commit_message); + } } diff --git a/ui/src/app/shared/workflow-template/apply-form/workflow-template.apply-form.html b/ui/src/app/shared/workflow-template/apply-form/workflow-template.apply-form.html index 5ec9108e92..c9b71359a6 100644 --- a/ui/src/app/shared/workflow-template/apply-form/workflow-template.apply-form.html +++ b/ui/src/app/shared/workflow-template/apply-form/workflow-template.apply-form.html @@ -9,6 +9,11 @@ +
    + + +
      @@ -18,7 +23,7 @@
    -
    @@ -27,7 +32,7 @@
    + [class.loading]="loading" [disabled]="!validFields">{{ 'btn_apply' | translate }} diff --git a/ui/src/app/shared/workflow-template/apply-modal/workflow-template.apply-modal.component.ts b/ui/src/app/shared/workflow-template/apply-modal/workflow-template.apply-modal.component.ts index ff14e3fe64..05a55a1ddc 100644 --- a/ui/src/app/shared/workflow-template/apply-modal/workflow-template.apply-modal.component.ts +++ b/ui/src/app/shared/workflow-template/apply-modal/workflow-template.apply-modal.component.ts @@ -18,6 +18,7 @@ import { WorkflowService } from 'app/service/workflow/workflow.service'; import { calculateWorkflowTemplateDiff } from 'app/shared/diff/diff'; import { Item } from 'app/shared/diff/list/diff.list.component'; import { finalize } from 'rxjs/operators'; +import { forkJoin } from 'rxjs/internal/observable/forkJoin'; @Component({ selector: 'app-workflow-template-apply-modal', @@ -32,16 +33,25 @@ export class WorkflowTemplateApplyModalComponent implements OnChanges { modal: SuiActiveModal; open: boolean; - @Input() project: Project; - @Input() workflow: Workflow; - @Input() workflowTemplate: WorkflowTemplate; - @Input() workflowTemplateInstance: WorkflowTemplateInstance; + // tslint:disable-next-line: no-input-rename + @Input('project') projectIn: Project; + // tslint:disable-next-line: no-input-rename + @Input('workflow') workflowIn: Workflow; + // tslint:disable-next-line: no-input-rename + @Input('workflowTemplate') workflowTemplateIn: WorkflowTemplate; + // tslint:disable-next-line: no-input-rename + @Input('workflowTemplateInstance') workflowTemplateInstanceIn: WorkflowTemplateInstance; @Output() close = new EventEmitter(); diffVisible: boolean; diffItems: Array; workflowTemplateAuditMessages: Array; + project: Project; + workflow: Workflow; + workflowTemplate: WorkflowTemplate; + workflowTemplateInstance: WorkflowTemplateInstance; + constructor( private _modalService: SuiModalService, private _projectService: ProjectService, @@ -81,25 +91,33 @@ export class WorkflowTemplateApplyModalComponent implements OnChanges { } load() { - if (this.workflowTemplate && this.workflowTemplateInstance) { - this._projectService.getProject(this.workflowTemplateInstance.project.key, [new LoadOpts('withKeys', 'keys')]) + if (this.workflowTemplateIn && this.workflowTemplateInstanceIn) { + this.workflowTemplate = this.workflowTemplateIn; + this.workflowTemplateInstance = this.workflowTemplateInstanceIn; + forkJoin([ + this._projectService.getProject(this.workflowTemplateInstanceIn.project.key, [new LoadOpts('withKeys', 'keys')]), + this._workflowService.getWorkflow(this.workflowTemplateInstance.project.key, this.workflowTemplateInstance.workflow_name) + ]) .pipe(finalize(() => this._cd.markForCheck())) - .subscribe(p => { - this.project = p; + .subscribe(results => { + this.project = results[0]; + this.workflow = results[1]; this.loadAudits(); }); - return - } else if (this.workflow) { + return; + } else if (this.projectIn && this.workflowIn) { // retrieve workflow template and instance from given workflow - let s = this.workflow.from_template.split('@'); + let s = this.workflowIn.from_template.split('@'); s = s[0].split('/'); - this.workflowTemplateInstance = this.workflow.template_instance; - - this._templateService.get(s[0], s.splice(1, s.length - 1).join('/')).subscribe(wt => { - this.workflowTemplate = wt; - this.loadAudits(); - this._cd.markForCheck(); - }); + this.project = this.projectIn; + this.workflow = this.workflowIn; + this.workflowTemplateInstance = this.workflowIn.template_instance; + this._templateService.get(s[0], s.splice(1, s.length - 1).join('/')) + .pipe(finalize(() => this._cd.markForCheck())) + .subscribe(wt => { + this.workflowTemplate = wt; + this.loadAudits(); + }); } } diff --git a/ui/src/app/shared/workflow-template/apply-modal/workflow-template.apply-modal.html b/ui/src/app/shared/workflow-template/apply-modal/workflow-template.apply-modal.html index 1ca3fa8fed..7027af26bb 100644 --- a/ui/src/app/shared/workflow-template/apply-modal/workflow-template.apply-modal.html +++ b/ui/src/app/shared/workflow-template/apply-modal/workflow-template.apply-modal.html @@ -6,12 +6,15 @@
    -
    +
    {{'workflow_template_update_info_line_1' | translate}}
    +
    + {{'workflow_template_update_info_line_2' | translate}} +
    -
    +
    {{ 'common_loading' | translate }}
    diff --git a/ui/src/app/shared/workflow-template/bulk-modal/workflow-template.bulk-modal.component.ts b/ui/src/app/shared/workflow-template/bulk-modal/workflow-template.bulk-modal.component.ts index 3a3e5fb90e..813790e531 100644 --- a/ui/src/app/shared/workflow-template/bulk-modal/workflow-template.bulk-modal.component.ts +++ b/ui/src/app/shared/workflow-template/bulk-modal/workflow-template.bulk-modal.component.ts @@ -20,6 +20,7 @@ import { WorkflowTemplateInstance } from 'app/model/workflow-template.model'; import { WorkflowTemplateService } from 'app/service/workflow-template/workflow-template.service'; +import { ParamData as AsCodeParamData } from 'app/shared/ascode/save-form/ascode.save-form.component'; import { AutoUnsubscribe } from 'app/shared/decorator/autoUnsubscribe'; import { Column, ColumnType, Select } from 'app/shared/table/data-table.component'; import { Observable, Subscription } from 'rxjs'; @@ -39,6 +40,7 @@ export class WorkflowTemplateBulkModalComponent { @Input() workflowTemplate: WorkflowTemplate; @Output() close = new EventEmitter(); + columnsInstances: Array>; columnsOperations: Array>; instances: Array; @@ -50,6 +52,9 @@ export class WorkflowTemplateBulkModalComponent { parameters: { [s: number]: ParamData }; response: WorkflowTemplateBulk; pollingStatusSub: Subscription; + asCodeParameters: AsCodeParamData; + withAsCodeWorkflow: boolean; + validFields: boolean; constructor( private _modalService: SuiModalService, @@ -154,8 +159,7 @@ export class WorkflowTemplateBulkModalComponent { this.loadingInstances = false; this._cd.markForCheck(); })) - .subscribe(is => this.instances = is.filter(i => !i.workflow || !i.workflow.from_repository) - .sort((a, b) => a.key() < b.key() ? -1 : 1)); + .subscribe(is => this.instances = is.sort((a, b) => a.key() < b.key() ? -1 : 1)); this.selectedInstanceKeys = []; @@ -164,6 +168,13 @@ export class WorkflowTemplateBulkModalComponent { clickGoToParam() { this.selectedInstances = this.instances.filter(i => !!this.selectedInstanceKeys.find(k => k === i.key())); + this.withAsCodeWorkflow = false; + for (let i = 0; i < this.selectedInstances.length; i++) { + if (this.selectedInstances[i].workflow && this.selectedInstances[i].workflow.from_repository) { + this.withAsCodeWorkflow = true; + break; + } + } this.moveToStep(1); } @@ -180,12 +191,17 @@ export class WorkflowTemplateBulkModalComponent { }); this.response = null; - this._workflowTemplateService.bulk(this.workflowTemplate.group.name, this.workflowTemplate.slug, req) - .pipe(finalize(() => this._cd.markForCheck())) - .subscribe(b => { - this.response = b; - this.startPollingStatus(); - }); + let request: Observable; + if (this.withAsCodeWorkflow) { + request = this._workflowTemplateService.bulkAsCode(this.workflowTemplate.group.name, this.workflowTemplate.slug, req, + this.asCodeParameters.branch_name, this.asCodeParameters.commit_message) + } else { + request = this._workflowTemplateService.bulk(this.workflowTemplate.group.name, this.workflowTemplate.slug, req) + } + request.pipe(finalize(() => this._cd.markForCheck())).subscribe(b => { + this.response = b; + this.startPollingStatus(); + }); this.moveToStep(2); } @@ -225,4 +241,14 @@ export class WorkflowTemplateBulkModalComponent { }) }); } + + onAsCodeParamChange(param: AsCodeParamData): void { + this.asCodeParameters = param; + this.validateParam(); + } + + validateParam() { + this.validFields = !this.withAsCodeWorkflow || (this.asCodeParameters && + !!this.asCodeParameters.branch_name && !!this.asCodeParameters.commit_message); + } } diff --git a/ui/src/app/shared/workflow-template/bulk-modal/workflow-template.bulk-modal.html b/ui/src/app/shared/workflow-template/bulk-modal/workflow-template.bulk-modal.html index e040430c28..c0926a41b6 100644 --- a/ui/src/app/shared/workflow-template/bulk-modal/workflow-template.bulk-modal.html +++ b/ui/src/app/shared/workflow-template/bulk-modal/workflow-template.bulk-modal.html @@ -40,6 +40,13 @@
    +
    +
    + {{'workflow_template_bulk_info_line_ascode' | translate}} +
    + + +
    @@ -51,6 +58,7 @@
    @@ -63,7 +71,7 @@
    -
    diff --git a/ui/src/app/shared/workflow-template/param-form/workflow-template.param-form.component.ts b/ui/src/app/shared/workflow-template/param-form/workflow-template.param-form.component.ts index 3db441d3ed..c9a9f023b4 100644 --- a/ui/src/app/shared/workflow-template/param-form/workflow-template.param-form.component.ts +++ b/ui/src/app/shared/workflow-template/param-form/workflow-template.param-form.component.ts @@ -42,6 +42,7 @@ export class WorkflowTemplateParamFormComponent implements OnInit { } @Input() workflowTemplate: WorkflowTemplate; @Input() workflowTemplateInstance: WorkflowTemplateInstance; + @Input() parameters: ParamData; @Output() paramChange = new EventEmitter(); vcsNames: Array; @@ -79,6 +80,7 @@ export class WorkflowTemplateParamFormComponent implements OnInit { ngOnInit(): void { this.initProject(); + this.changeParam(); } initProject() { @@ -142,7 +144,8 @@ export class WorkflowTemplateParamFormComponent implements OnInit { fillFormWithInstanceData(): void { if (this.workflowTemplate && this.workflowTemplateInstance) { this.workflowTemplate.parameters.forEach(parameter => { - let v = this.workflowTemplateInstance.request.parameters[parameter.key]; + let v = (this.parameters && this.parameters[parameter.key]) ? + this.parameters[parameter.key] : this.workflowTemplateInstance.request.parameters[parameter.key]; if (v) { switch (parameter.type) { case 'boolean': diff --git a/ui/src/app/views/pipeline/show/pipeline.show.component.ts b/ui/src/app/views/pipeline/show/pipeline.show.component.ts index 4444a24ca5..e3ffcf4df3 100644 --- a/ui/src/app/views/pipeline/show/pipeline.show.component.ts +++ b/ui/src/app/views/pipeline/show/pipeline.show.component.ts @@ -11,8 +11,8 @@ import { AuthentifiedUser } from 'app/model/user.model'; import { Workflow } from 'app/model/workflow.model'; import { KeyService } from 'app/service/keys/keys.service'; import { PipelineCoreService } from 'app/service/pipeline/pipeline.core.service'; +import { AsCodeSaveModalComponent } from 'app/shared/ascode/save-modal/ascode.save-modal.component'; import { AutoUnsubscribe } from 'app/shared/decorator/autoUnsubscribe'; -import { UpdateAsCodeComponent } from 'app/shared/modal/save-as-code/update.as.code.component'; import { WarningModalComponent } from 'app/shared/modal/warning/warning.component'; import { ParameterEvent } from 'app/shared/parameter/parameter.event.model'; import { ToastService } from 'app/shared/toast/ToastService'; @@ -70,7 +70,7 @@ export class PipelineShowComponent implements OnInit { @ViewChild('paramWarning') parameterModalWarning: WarningModalComponent; @ViewChild('updateEditMode') - asCodeSaveModal: UpdateAsCodeComponent; + asCodeSaveModal: AsCodeSaveModalComponent; keys: AllKeys; asCodeEditorOpen: boolean; diff --git a/ui/src/app/views/pipeline/show/pipeline.show.html b/ui/src/app/views/pipeline/show/pipeline.show.html index 1885774937..13c208ca6a 100644 --- a/ui/src/app/views/pipeline/show/pipeline.show.html +++ b/ui/src/app/views/pipeline/show/pipeline.show.html @@ -14,9 +14,9 @@
    - - + +

    {{'pipeline_repository_help_line_1' | translate}}

    @@ -111,7 +111,8 @@

    {{ 'pipeline_parameters_form_title' | translate }}

    Loading pipeline...
    - - - + + + diff --git a/ui/src/app/views/settings/workflow-template/edit/workflow-template.edit.component.ts b/ui/src/app/views/settings/workflow-template/edit/workflow-template.edit.component.ts index af5978ec10..71650fd32b 100644 --- a/ui/src/app/views/settings/workflow-template/edit/workflow-template.edit.component.ts +++ b/ui/src/app/views/settings/workflow-template/edit/workflow-template.edit.component.ts @@ -171,7 +171,6 @@ export class WorkflowTemplateEditComponent implements OnInit { } }, >{ type: ColumnType.BUTTON, - hidden: (i: WorkflowTemplateInstance) => i.workflow && !!i.workflow.from_repository, name: 'common_action', class: 'two right aligned', selector: (i: WorkflowTemplateInstance) => { @@ -179,7 +178,7 @@ export class WorkflowTemplateEditComponent implements OnInit { title: 'common_update', class: 'primary small', click: () => { - this.clickUpdate(i) + this.clickUpdate(i); } }; } diff --git a/ui/src/app/views/workflow/show/workflow.component.ts b/ui/src/app/views/workflow/show/workflow.component.ts index c31a274f7f..c48955c4e2 100644 --- a/ui/src/app/views/workflow/show/workflow.component.ts +++ b/ui/src/app/views/workflow/show/workflow.component.ts @@ -7,8 +7,8 @@ import { Project } from 'app/model/project.model'; import { Workflow } from 'app/model/workflow.model'; import { WorkflowCoreService } from 'app/service/workflow/workflow.core.service'; import { WorkflowStore } from 'app/service/workflow/workflow.store'; +import { AsCodeSaveModalComponent } from 'app/shared/ascode/save-modal/ascode.save-modal.component'; import { AutoUnsubscribe } from 'app/shared/decorator/autoUnsubscribe'; -import { UpdateAsCodeComponent } from 'app/shared/modal/save-as-code/update.as.code.component'; import { WarningModalComponent } from 'app/shared/modal/warning/warning.component'; import { PermissionEvent } from 'app/shared/permission/permission.event.model'; import { ToastService } from 'app/shared/toast/ToastService'; @@ -51,7 +51,7 @@ export class WorkflowShowComponent implements OnInit { @ViewChild('permWarning') permWarningModal: WarningModalComponent; @ViewChild('updateAsCode') - updateAsCodeModal: UpdateAsCodeComponent; + updateAsCodeModal: AsCodeSaveModalComponent; selectedHookRef: string; @@ -72,8 +72,7 @@ export class WorkflowShowComponent implements OnInit { private _toast: ToastService, private _workflowCoreService: WorkflowCoreService, private _cd: ChangeDetectorRef - ) { - } + ) { } ngOnInit(): void { // Update data if route change diff --git a/ui/src/app/views/workflow/show/workflow.html b/ui/src/app/views/workflow/show/workflow.html index c2d74ab763..345b27f39a 100644 --- a/ui/src/app/views/workflow/show/workflow.html +++ b/ui/src/app/views/workflow/show/workflow.html @@ -3,33 +3,37 @@
    @@ -51,10 +56,12 @@
    - -
    @@ -69,7 +76,8 @@ popupPlacement="top center">
    -
    {{'btn_save' | @@ -77,14 +85,16 @@
    -
    -
    -
    - {{'btn_run_workflow' | translate }} -
    +
    +
    +
    + {{'btn_run_workflow' | translate }}
    +
    @@ -101,32 +111,41 @@
    - +

    {{ 'workflow_permission_list_title' | translate }}

    - - + + +

    {{ 'workflow_permission_form_title' | translate }}

    - +
    - +
    - + +
    -
    - +
    - +
    @@ -136,9 +155,6 @@

    {{ 'workflow_permission_form_title' | translate }}

    - + diff --git a/ui/src/app/views/workflow/workflow.component.ts b/ui/src/app/views/workflow/workflow.component.ts index 405c4e458f..d60868fd74 100644 --- a/ui/src/app/views/workflow/workflow.component.ts +++ b/ui/src/app/views/workflow/workflow.component.ts @@ -14,8 +14,8 @@ import { Project } from 'app/model/project.model'; import { Workflow } from 'app/model/workflow.model'; import { WorkflowCoreService } from 'app/service/workflow/workflow.core.service'; import { WorkflowSidebarMode } from 'app/service/workflow/workflow.sidebar.store'; +import { AsCodeSaveModalComponent } from 'app/shared/ascode/save-modal/ascode.save-modal.component'; import { AutoUnsubscribe } from 'app/shared/decorator/autoUnsubscribe'; -import { UpdateAsCodeComponent } from 'app/shared/modal/save-as-code/update.as.code.component'; import { ToastService } from 'app/shared/toast/ToastService'; import { WorkflowTemplateApplyModalComponent } from 'app/shared/workflow-template/apply-modal/workflow-template.apply-modal.component'; import { ProjectState, ProjectStateModel } from 'app/store/project.state'; @@ -69,11 +69,12 @@ export class WorkflowComponent implements OnInit { asCodeEditorOpen = false; @ViewChild('updateAsCode') - saveAsCode: UpdateAsCodeComponent; - @ViewChild('popup') - popupFromlRepository: SuiPopup; - @ViewChildren(SuiPopupController) popups: QueryList; - @ViewChildren(SuiPopupTemplateController) popups2: QueryList>; + saveAsCode: AsCodeSaveModalComponent; + + @ViewChild('popupFromRepo') + popupFromRepository: SuiPopup; + @ViewChild('popupFromTemp') + popupFromTemplate: SuiPopup; selectedNodeID: number; selectedNodeRef: string; @@ -90,9 +91,7 @@ export class WorkflowComponent implements OnInit { private _translate: TranslateService, private _store: Store, private _cd: ChangeDetectorRef - ) { - - } + ) { } ngOnInit(): void { this.projectSubscription = this._store.select(ProjectState) @@ -104,7 +103,7 @@ export class WorkflowComponent implements OnInit { this._cd.detectChanges(); }); - this.sidebarSubs = this.sibebar$.subscribe( m => { + this.sidebarSubs = this.sibebar$.subscribe(m => { if (m === this.sidebarMode) { return; } @@ -149,7 +148,7 @@ export class WorkflowComponent implements OnInit { if (this.selectecHookRef) { let h = Workflow.getHookByRef(this.selectecHookRef, this.workflow); if (h) { - this._store.dispatch(new SelectHook({hook: h, node: this.workflow.workflow_data.node})); + this._store.dispatch(new SelectHook({ hook: h, node: this.workflow.workflow_data.node })); } } this._cd.markForCheck(); @@ -180,7 +179,7 @@ export class WorkflowComponent implements OnInit { initRuns(key: string, workflowName: string, filters?: {}): void { this._store.dispatch( - new GetWorkflowRuns({projectKey: key, workflowName: workflowName, limit: '50', offset: '0', filters}) + new GetWorkflowRuns({ projectKey: key, workflowName: workflowName, limit: '50', offset: '0', filters }) ); } @@ -232,6 +231,7 @@ export class WorkflowComponent implements OnInit { return; } + if (this.saveAsCode) { this.saveAsCode.show(null, 'workflow'); } diff --git a/ui/src/app/views/workflow/workflow.html b/ui/src/app/views/workflow/workflow.html index d9f3d54715..08ea23af6a 100644 --- a/ui/src/app/views/workflow/workflow.html +++ b/ui/src/app/views/workflow/workflow.html @@ -2,41 +2,40 @@
    + [ngClass]="{'animated infinite pulse' : loadingFav}" [class.favorite]="workflow?.favorite" + [class.unfavorite]="!workflow?.favorite" (click)="updateFav()">
    + [class.green]="workflow && workflow.from_repository && (!workflow.as_code_events || workflow.as_code_events.length === 0)" + [class.orange]="workflow && workflow.as_code_events && workflow.as_code_events.length > 0" + [class.grey]="workflow && !workflow.from_repository && (!workflow.as_code_events || workflow.as_code_events.length === 0)" + suiPopup [popupTemplate]="popupFromRepository" popupPlacement="bottom right" popupTrigger="outsideClick" + #popupFromRepo="suiPopup"> as code
    - + *ngIf="workflow && (workflow.from_repository || (workflow.as_code_events && workflow.as_code_events.length > 0))"> + + + *ngIf="workflow && !workflow.from_repository && (!workflow.as_code_events || workflow.as_code_events.length === 0)">

    {{'workflow_repository_help_line_1' | translate}}

    {{'workflow_repository_help_line_2' | translate}}

    @@ -46,9 +45,9 @@
    + [ngClass]="workflow && workflow.from_template ? (workflow.template_up_to_date ? 'green' : 'orange') : 'grey'" + suiPopup [popupTemplate]="popupFromTemplate" popupPlacement="bottom right" popupTrigger="outsideClick" + #popupFromTemp="suiPopup"> template @@ -56,12 +55,12 @@

    {{ 'workflow_from_template' | translate}} - {{workflow.from_template}} - {{'common_not_up_to_date' + {{workflow.from_template}} + {{'common_not_up_to_date' | translate }}

    - -