diff --git a/engine/api/api_routes.go b/engine/api/api_routes.go index 8fa8af5f84..3c9cd8915b 100644 --- a/engine/api/api_routes.go +++ b/engine/api/api_routes.go @@ -431,6 +431,7 @@ func (api *API) InitRouter() { r.Handle("/template/{groupName}/{templateSlug}/instance/{instanceID}", Scope(sdk.AuthConsumerScopeTemplate), r.DELETE(api.deleteTemplateInstanceHandler)) r.Handle("/template/{groupName}/{templateSlug}/usage", Scope(sdk.AuthConsumerScopeTemplate), r.GET(api.getTemplateUsageHandler)) + r.Handle("/v2/project/repositories", Scope(sdk.AuthConsumerScopeHooks), r.GETv2(api.getAllRepositoriesHandler)) r.Handle("/v2/project/{projectKey}/vcs", nil, r.POSTv2(api.postVCSProjectHandler), r.GETv2(api.getVCSProjectAllHandler)) r.Handle("/v2/project/{projectKey}/vcs/{vcsIdentifier}", nil, r.PUTv2(api.putVCSProjectHandler), r.DELETEv2(api.deleteVCSProjectHandler), r.GETv2(api.getVCSProjectHandler)) r.Handle("/v2/project/{projectKey}/vcs/{vcsIdentifier}/repository", nil, r.POSTv2(api.postProjectRepositoryHandler), r.GETv2(api.getVCSProjectRepositoryAllHandler)) diff --git a/engine/api/rbac/rule_hook.go b/engine/api/rbac/rule_hook.go new file mode 100644 index 0000000000..d017134ff0 --- /dev/null +++ b/engine/api/rbac/rule_hook.go @@ -0,0 +1,16 @@ +package rbac + +import ( + "context" + + "github.com/go-gorp/gorp" + "github.com/ovh/cds/engine/cache" + "github.com/ovh/cds/sdk" +) + +func IsHookService(_ context.Context, auth *sdk.AuthConsumer, _ cache.Store, _ gorp.SqlExecutor, _ map[string]string) error { + if auth.Service != nil && auth.Service.Type == sdk.TypeHooks { + return nil + } + return sdk.WithStack(sdk.ErrForbidden) +} diff --git a/engine/api/repository/dao_vcs_project_repository.go b/engine/api/repository/dao_vcs_project_repository.go index 0bad158773..c2d478b1d0 100644 --- a/engine/api/repository/dao_vcs_project_repository.go +++ b/engine/api/repository/dao_vcs_project_repository.go @@ -22,6 +22,15 @@ func Insert(ctx context.Context, db gorpmapper.SqlExecutorWithTx, repo *sdk.Proj return nil } +func Update(ctx context.Context, db gorpmapper.SqlExecutorWithTx, repo *sdk.ProjectRepository) error { + dbData := &dbProjectRepository{ProjectRepository: *repo} + if err := gorpmapping.UpdateAndSign(ctx, db, dbData); err != nil { + return err + } + *repo = dbData.ProjectRepository + return nil +} + func Delete(db gorpmapper.SqlExecutorWithTx, vcsProjectID string, name string) error { _, err := db.Exec("DELETE FROM project_repository WHERE vcs_project_id = $1 AND name = $2", vcsProjectID, name) return sdk.WrapError(err, "cannot delete project_repository %s / %s", vcsProjectID, name) @@ -84,3 +93,25 @@ func LoadAllRepositoriesByVCSProjectID(ctx context.Context, db gorp.SqlExecutor, } return repositories, nil } + +func LoadAllRepositories(ctx context.Context, db gorp.SqlExecutor) ([]sdk.ProjectRepository, error) { + query := gorpmapping.NewQuery(`SELECT project_repository.* FROM project_repository`) + var res []dbProjectRepository + if err := gorpmapping.GetAll(ctx, db, query, &res); err != nil { + return nil, err + } + + repositories := make([]sdk.ProjectRepository, 0, len(res)) + for _, r := range res { + isValid, err := gorpmapping.CheckSignature(r, r.Signature) + if err != nil { + return nil, err + } + if !isValid { + log.Error(ctx, "project_repository %d / %s data corrupted", r.ID, r.Name) + continue + } + repositories = append(repositories, r.ProjectRepository) + } + return repositories, nil +} diff --git a/engine/api/v2_project.go b/engine/api/v2_project.go index 1356cc6fb7..9ff3cd2923 100644 --- a/engine/api/v2_project.go +++ b/engine/api/v2_project.go @@ -3,192 +3,20 @@ package api import ( "context" "net/http" - "net/url" - "github.com/gorilla/mux" - - "github.com/ovh/cds/engine/api/project" "github.com/ovh/cds/engine/api/rbac" - "github.com/ovh/cds/engine/api/repositoriesmanager" - "github.com/ovh/cds/engine/api/vcs" + "github.com/ovh/cds/engine/api/repository" "github.com/ovh/cds/engine/service" - "github.com/ovh/cds/sdk" ) -func (api *API) getVCSByIdentifier(ctx context.Context, projectKey string, vcsIdentifier string) (*sdk.VCSProject, error) { - var vcsProject *sdk.VCSProject - var err error - if sdk.IsValidUUID(vcsIdentifier) { - vcsProject, err = vcs.LoadVCSByID(ctx, api.mustDB(), projectKey, vcsIdentifier) - } else { - vcsProject, err = vcs.LoadVCSByProject(ctx, api.mustDB(), projectKey, vcsIdentifier) - } - if err != nil { - return nil, err - } - return vcsProject, nil -} - -func (api *API) postVCSProjectHandler() ([]service.RbacChecker, service.Handler) { - return service.RBAC(rbac.ProjectManage), - func(ctx context.Context, w http.ResponseWriter, req *http.Request) error { - vars := mux.Vars(req) - pKey := vars["projectKey"] - - tx, err := api.mustDB().Begin() - if err != nil { - return sdk.WithStack(err) - } - defer tx.Rollback() // nolint - - project, err := project.Load(ctx, tx, pKey) - if err != nil { - return sdk.WithStack(err) - } - - var vcsProject sdk.VCSProject - if err := service.UnmarshalRequest(ctx, req, &vcsProject); err != nil { - return err - } - - vcsProject.ProjectID = project.ID - vcsProject.CreatedBy = getAPIConsumer(ctx).GetUsername() - - if err := vcs.Insert(ctx, tx, &vcsProject); err != nil { - return err - } - - vcsClient, err := repositoriesmanager.AuthorizedClient(ctx, tx, api.Cache, pKey, vcsProject.Name) - if err != nil { - return err - } - - if _, err := vcsClient.Repos(ctx); err != nil { - return err - } - - if err := tx.Commit(); err != nil { - return sdk.WithStack(err) - } - - return service.WriteMarshal(w, req, vcsProject, http.StatusCreated) - } -} - -func (api *API) putVCSProjectHandler() ([]service.RbacChecker, service.Handler) { - return service.RBAC(rbac.ProjectManage), +// getAllRepositoriesHandler Get all repositories +func (api *API) getAllRepositoriesHandler() ([]service.RbacChecker, service.Handler) { + return service.RBAC(rbac.IsHookService), func(ctx context.Context, w http.ResponseWriter, req *http.Request) error { - vars := mux.Vars(req) - pKey := vars["projectKey"] - - vcsIdentifier, err := url.PathUnescape(vars["vcsIdentifier"]) - if err != nil { - return sdk.NewError(sdk.ErrWrongRequest, err) - } - - vcsOld, err := api.getVCSByIdentifier(ctx, pKey, vcsIdentifier) - if err != nil { - return err - } - - tx, err := api.mustDB().Begin() - if err != nil { - return sdk.WithStack(err) - } - defer tx.Rollback() // nolint - - var vcsProject sdk.VCSProject - if err := service.UnmarshalRequest(ctx, req, &vcsProject); err != nil { - return err - } - - vcsProject.ID = vcsOld.ID - vcsProject.Created = vcsOld.Created - vcsProject.CreatedBy = vcsOld.CreatedBy - vcsProject.ProjectID = vcsOld.ProjectID - - if err := vcs.Update(ctx, tx, &vcsProject); err != nil { - return err - } - - if err := tx.Commit(); err != nil { - return sdk.WithStack(err) - } - - return service.WriteMarshal(w, req, vcsProject, http.StatusCreated) - } -} - -func (api *API) deleteVCSProjectHandler() ([]service.RbacChecker, service.Handler) { - return service.RBAC(rbac.ProjectManage), - func(ctx context.Context, w http.ResponseWriter, req *http.Request) error { - vars := mux.Vars(req) - pKey := vars["projectKey"] - - vcsIdentifier, err := url.PathUnescape(vars["vcsIdentifier"]) - if err != nil { - return sdk.NewError(sdk.ErrWrongRequest, err) - } - - vcsProject, err := api.getVCSByIdentifier(ctx, pKey, vcsIdentifier) - if err != nil { - return err - } - - tx, err := api.mustDB().Begin() - if err != nil { - return sdk.WithStack(err) - } - defer tx.Rollback() // nolint - - project, err := project.Load(ctx, tx, pKey) - if err != nil { - return sdk.WithStack(err) - } - - if err := vcs.Delete(tx, project.ID, vcsProject.Name); err != nil { - return err - } - - if err := tx.Commit(); err != nil { - return sdk.WithStack(err) - } - - return nil - } -} - -// getVCSProjectAllHandler returns list of vcs of one project key -func (api *API) getVCSProjectAllHandler() ([]service.RbacChecker, service.Handler) { - return service.RBAC(rbac.ProjectRead), - func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { - vars := mux.Vars(r) - pKey := vars["projectKey"] - - vcsProjects, err := vcs.LoadAllVCSByProject(ctx, api.mustDB(), pKey) - if err != nil { - return sdk.WrapError(err, "unable to load vcs server on project %s", pKey) - } - - return service.WriteJSON(w, vcsProjects, http.StatusOK) - } -} - -func (api *API) getVCSProjectHandler() ([]service.RbacChecker, service.Handler) { - return service.RBAC(rbac.ProjectRead), - func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { - vars := mux.Vars(r) - pKey := vars["projectKey"] - - vcsIdentifier, err := url.PathUnescape(vars["vcsIdentifier"]) - if err != nil { - return sdk.NewError(sdk.ErrWrongRequest, err) - } - - vcsProject, err := api.getVCSByIdentifier(ctx, pKey, vcsIdentifier) + repos, err := repository.LoadAllRepositories(ctx, api.mustDB()) if err != nil { return err } - return service.WriteMarshal(w, r, vcsProject, http.StatusOK) + return service.WriteJSON(w, repos, http.StatusOK) } } diff --git a/engine/api/v2_project_repository.go b/engine/api/v2_project_repository.go index a491a835ff..c07dab9763 100644 --- a/engine/api/v2_project_repository.go +++ b/engine/api/v2_project_repository.go @@ -10,6 +10,7 @@ import ( "github.com/ovh/cds/engine/api/rbac" "github.com/ovh/cds/engine/api/repositoriesmanager" "github.com/ovh/cds/engine/api/repository" + "github.com/ovh/cds/engine/api/services" "github.com/ovh/cds/engine/service" "github.com/ovh/cds/sdk" ) @@ -59,6 +60,19 @@ func (api *API) deleteProjectRepositoryHandler() ([]service.RbacChecker, service } defer tx.Rollback() // nolint + // Remove hooks + srvs, err := services.LoadAllByType(ctx, tx, sdk.TypeHooks) + if err != nil { + return err + } + if len(srvs) < 1 { + return sdk.NewErrorFrom(sdk.ErrNotFound, "unable to find hook uservice") + } + _, code, errHooks := services.NewClient(tx, srvs).DoJSONRequest(ctx, http.MethodDelete, "/task/"+repo.ID, nil, nil) + if (errHooks != nil || code >= 400) && code != 404 { + return sdk.WrapError(errHooks, "unable to delete hook [HTTP: %d]", code) + } + if err := repository.Delete(tx, repo.VCSProjectID, repo.Name); err != nil { return err } @@ -99,6 +113,8 @@ func (api *API) postProjectRepositoryHandler() ([]service.RbacChecker, service.H repo.VCSProjectID = vcsProject.ID repo.CreatedBy = getAPIConsumer(ctx).GetUsername() + + // Insert Repository if err := repository.Insert(ctx, tx, &repo); err != nil { return err } @@ -111,6 +127,26 @@ func (api *API) postProjectRepositoryHandler() ([]service.RbacChecker, service.H if _, err := vcsClient.RepoByFullname(ctx, repo.Name); err != nil { return err } + + // Create hook + srvs, err := services.LoadAllByType(ctx, tx, sdk.TypeHooks) + if err != nil { + return err + } + if len(srvs) < 1 { + return sdk.NewErrorFrom(sdk.ErrNotFound, "unable to find hook uservice") + } + repositoryHookRegister := sdk.NewEntitiesHook(repo.ID, pKey, vcsProject.Type, vcsProject.Name, repo.Name) + _, code, errHooks := services.NewClient(tx, srvs).DoJSONRequest(ctx, http.MethodPost, "/v2/task", repositoryHookRegister, nil) + if errHooks != nil || code >= 400 { + return sdk.WrapError(errHooks, "unable to create hooks [HTTP: %d]", code) + } + + // Update repository with Hook configuration + repo.HookConfiguration = repositoryHookRegister.Configuration + if err := repository.Update(ctx, tx, &repo); err != nil { + return err + } if err := tx.Commit(); err != nil { return sdk.WithStack(err) diff --git a/engine/api/v2_project_repository_test.go b/engine/api/v2_project_repository_test.go index f616b1afe2..f19bad6dbf 100644 --- a/engine/api/v2_project_repository_test.go +++ b/engine/api/v2_project_repository_test.go @@ -34,6 +34,7 @@ func Test_crudRepositoryOnProjectLambdaUserOK(t *testing.T) { // Mock VCS s, _ := assets.InsertService(t, db, t.Name()+"_VCS", sdk.TypeVCS) + sHooks, _ := assets.InsertService(t, db, t.Name()+"_HOOK", sdk.TypeHooks) // Setup a mock for all services called by the API ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -43,6 +44,7 @@ func Test_crudRepositoryOnProjectLambdaUserOK(t *testing.T) { } defer func() { _ = services.Delete(db, s) + _ = services.Delete(db, sHooks) services.NewClient = services.NewDefaultClient }() @@ -58,6 +60,7 @@ func Test_crudRepositoryOnProjectLambdaUserOK(t *testing.T) { return nil, 200, nil }, ).MaxTimes(1) + servicesClients.EXPECT().DoJSONRequest(gomock.Any(), "POST", "/v2/task", gomock.Any(), gomock.Any(), gomock.Any()).Times(1) // Creation request repo := sdk.ProjectRepository{ @@ -93,6 +96,8 @@ func Test_crudRepositoryOnProjectLambdaUserOK(t *testing.T) { require.NoError(t, json.Unmarshal(w2.Body.Bytes(), &repositories)) require.Len(t, repositories, 1) + servicesClients.EXPECT().DoJSONRequest(gomock.Any(), "DELETE", "/task/"+repositories[0].ID, gomock.Any(), gomock.Any(), gomock.Any()).Times(1) + // Then Delete repository varsDelete := vars varsDelete["repositoryIdentifier"] = url.PathEscape("ovh/cds") diff --git a/engine/api/v2_project_test.go b/engine/api/v2_project_test.go index 7df6a50a5f..36e032792c 100644 --- a/engine/api/v2_project_test.go +++ b/engine/api/v2_project_test.go @@ -3,219 +3,49 @@ package api import ( "context" "encoding/json" - "io" - "net/http" "net/http/httptest" - "strings" "testing" + "time" - "github.com/go-gorp/gorp" - "github.com/golang/mock/gomock" - "github.com/ovh/cds/engine/api/services" - "github.com/ovh/cds/engine/api/services/mock_services" + "github.com/stretchr/testify/require" + + "github.com/ovh/cds/engine/api/repository" "github.com/ovh/cds/engine/api/test" "github.com/ovh/cds/engine/api/test/assets" "github.com/ovh/cds/sdk" - "github.com/stretchr/testify/require" ) -func Test_crudVCSOnProjectLambdaUserForbidden(t *testing.T) { +func Test_getAllRepositoriesHandler(t *testing.T) { api, db, _ := newTestAPI(t) - u, pass := assets.InsertLambdaUser(t, db) - proj := assets.InsertTestProject(t, db, api.Cache, sdk.RandomString(10), sdk.RandomString(10)) - - vars := map[string]string{ - "projectKey": proj.Key, - } - uriPost := api.Router.GetRouteV2("POST", api.postVCSProjectHandler, vars) - test.NotEmpty(t, uriPost) - - req := assets.NewAuthentifiedRequest(t, u, pass, "POST", uriPost, nil) - - w := httptest.NewRecorder() - api.Router.Mux.ServeHTTP(w, req) - require.Equal(t, 403, w.Code) - - uriGet := api.Router.GetRouteV2("GET", api.getVCSProjectAllHandler, vars) - test.NotEmpty(t, uriGet) - - reqGet := assets.NewAuthentifiedRequest(t, u, pass, "GET", uriGet, nil) - w2 := httptest.NewRecorder() - api.Router.Mux.ServeHTTP(w2, reqGet) - require.Equal(t, 403, w2.Code) -} - -func Test_crudVCSOnProjectLambdaUserOK(t *testing.T) { - api, db, _ := newTestAPI(t) - - proj := assets.InsertTestProject(t, db, api.Cache, sdk.RandomString(10), sdk.RandomString(10)) - user1, pass := assets.InsertLambdaUser(t, db) - - assets.InsertRBAcProject(t, db, "manage", proj.Key, *user1) - assets.InsertRBAcProject(t, db, "read", proj.Key, *user1) - - // Mock VCS - s, _ := assets.InsertService(t, db, t.Name()+"_VCS", sdk.TypeVCS) - // Setup a mock for all services called by the API - ctrl := gomock.NewController(t) - defer ctrl.Finish() - servicesClients := mock_services.NewMockClient(ctrl) - services.NewClient = func(_ gorp.SqlExecutor, _ []sdk.Service) services.Client { - return servicesClients - } - defer func() { - _ = services.Delete(db, s) - services.NewClient = services.NewDefaultClient - }() - - servicesClients.EXPECT(). - DoJSONRequest(gomock.Any(), "GET", "/vcs/my_vcs_server/repos", gomock.Any(), gomock.Any(), gomock.Any()). - DoAndReturn( - func(ctx context.Context, method, path string, in interface{}, out interface{}, _ interface{}) (http.Header, int, error) { - repos := []sdk.VCSRepo{} - out = repos - return nil, 200, nil - }, - ).MaxTimes(1) - - vars := map[string]string{ - "projectKey": proj.Key, - } - uri := api.Router.GetRouteV2("POST", api.postVCSProjectHandler, vars) - test.NotEmpty(t, uri) - req := assets.NewAuthentifiedRequest(t, user1, pass, "POST", uri, nil) - - body := `version: v1.0 -name: my_vcs_server -type: bitbucketserver -description: "it's the test vcs server on project" -url: "http://my-vcs-server.localhost" -auth: - user: the-username - password: the-password -` - - // Here, we insert the vcs server as a CDS user (not administrator) - req.Body = io.NopCloser(strings.NewReader(body)) - req.Header.Set("Content-Type", "application/x-yaml") - - w := httptest.NewRecorder() - api.Router.Mux.ServeHTTP(w, req) - require.Equal(t, 201, w.Code) - - // Then, get the vcs server - uriGet := api.Router.GetRouteV2("GET", api.getVCSProjectAllHandler, vars) - test.NotEmpty(t, uriGet) - - reqGet := assets.NewAuthentifiedRequest(t, user1, pass, "GET", uriGet, nil) - w2 := httptest.NewRecorder() - api.Router.Mux.ServeHTTP(w2, reqGet) - require.Equal(t, 200, w2.Code) - - vcsProjects := []sdk.VCSProject{} - require.NoError(t, json.Unmarshal(w2.Body.Bytes(), &vcsProjects)) - require.Len(t, vcsProjects, 1) -} - -func Test_crudVCSOnProjectAdminOk(t *testing.T) { - api, db, _ := newTestAPI(t) + // Clean db + _, err := db.Exec("delete from project_repository") + require.NoError(t, err) - u, pass := assets.InsertAdminUser(t, db) proj := assets.InsertTestProject(t, db, api.Cache, sdk.RandomString(10), sdk.RandomString(10)) + user1, pass := assets.InsertAdminUser(t, db) - // Mock VCS - s, _ := assets.InsertService(t, db, t.Name()+"_VCS", sdk.TypeVCS) - // Setup a mock for all services called by the API - ctrl := gomock.NewController(t) - defer ctrl.Finish() - servicesClients := mock_services.NewMockClient(ctrl) - services.NewClient = func(_ gorp.SqlExecutor, _ []sdk.Service) services.Client { - return servicesClients - } - defer func() { - _ = services.Delete(db, s) - services.NewClient = services.NewDefaultClient - }() + vcsProj := assets.InsertTestVCSProject(t, db, proj.ID, "vcs-github", "github") - servicesClients.EXPECT(). - DoJSONRequest(gomock.Any(), "GET", "/vcs/my_vcs_server/repos", gomock.Any(), gomock.Any(), gomock.Any()). - DoAndReturn( - func(ctx context.Context, method, path string, in interface{}, out interface{}, _ interface{}) (http.Header, int, error) { - repos := []sdk.VCSRepo{} - out = repos - return nil, 200, nil - }, - ).MaxTimes(1) + require.NoError(t, repository.Insert(context.TODO(), db, &sdk.ProjectRepository{ + Name: "my/repo", + ID: sdk.UUID(), + CreatedBy: "me", + Created: time.Now(), + VCSProjectID: vcsProj.ID, + })) - vars := map[string]string{ - "projectKey": proj.Key, - } - uri := api.Router.GetRouteV2("POST", api.postVCSProjectHandler, vars) + vars := map[string]string{} + uri := api.Router.GetRouteV2("GET", api.getAllRepositoriesHandler, vars) test.NotEmpty(t, uri) - req := assets.NewAuthentifiedRequest(t, u, pass, "POST", uri, nil) - - body := `version: v1.0 -name: my_vcs_server -type: bitbucketserver -description: "it's the test vcs server on project" -url: "http://my-vcs-server.localhost" -auth: - user: the-username - password: the-password -` - - // Here, we insert the vcs server as a CDS administrator - req.Body = io.NopCloser(strings.NewReader(body)) - req.Header.Set("Content-Type", "application/x-yaml") + req := assets.NewAuthentifiedRequest(t, user1, pass, "GET", uri, nil) w := httptest.NewRecorder() api.Router.Mux.ServeHTTP(w, req) - require.Equal(t, 201, w.Code) - - // Then, get the vcs server in the list of vcs - uriGetAll := api.Router.GetRouteV2("GET", api.getVCSProjectAllHandler, vars) - test.NotEmpty(t, uriGetAll) - - reqGetAll := assets.NewAuthentifiedRequest(t, u, pass, "GET", uriGetAll, nil) - w2 := httptest.NewRecorder() - api.Router.Mux.ServeHTTP(w2, reqGetAll) - require.Equal(t, 200, w2.Code) - - vcsProjects := []sdk.VCSProject{} - require.NoError(t, json.Unmarshal(w2.Body.Bytes(), &vcsProjects)) - require.Len(t, vcsProjects, 1) - - // Then, try to get the vcs server directly - vars["vcsIdentifier"] = "my_vcs_server" - uriGet := api.Router.GetRouteV2("GET", api.getVCSProjectHandler, vars) - test.NotEmpty(t, uriGet) - - reqGet := assets.NewAuthentifiedRequest(t, u, pass, "GET", uriGet, nil) - w3 := httptest.NewRecorder() - api.Router.Mux.ServeHTTP(w3, reqGet) - require.Equal(t, 200, w3.Code) - - vcsProject := sdk.VCSProject{} - require.NoError(t, json.Unmarshal(w3.Body.Bytes(), &vcsProject)) - require.Equal(t, "my_vcs_server", vcsProject.Name) - require.Empty(t, vcsProject.Auth) - - // delete the vcs project - uriDelete := api.Router.GetRouteV2("DELETE", api.deleteVCSProjectHandler, vars) - test.NotEmpty(t, uriDelete) - - reqDelete := assets.NewAuthentifiedRequest(t, u, pass, "DELETE", uriDelete, nil) - w4 := httptest.NewRecorder() - api.Router.Mux.ServeHTTP(w4, reqDelete) - require.Equal(t, 204, w4.Code) + require.Equal(t, 200, w.Code) - reqGetAll2 := assets.NewAuthentifiedRequest(t, u, pass, "GET", uriGetAll, nil) - w5 := httptest.NewRecorder() - api.Router.Mux.ServeHTTP(w5, reqGetAll2) - require.Equal(t, 200, w5.Code) + var repositories []sdk.ProjectRepository + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &repositories)) + require.Len(t, repositories, 1) - vcsProjects2 := []sdk.VCSProject{} - require.NoError(t, json.Unmarshal(w5.Body.Bytes(), &vcsProjects2)) - require.Len(t, vcsProjects2, 0) } diff --git a/engine/api/v2_project_vcs.go b/engine/api/v2_project_vcs.go new file mode 100644 index 0000000000..1356cc6fb7 --- /dev/null +++ b/engine/api/v2_project_vcs.go @@ -0,0 +1,194 @@ +package api + +import ( + "context" + "net/http" + "net/url" + + "github.com/gorilla/mux" + + "github.com/ovh/cds/engine/api/project" + "github.com/ovh/cds/engine/api/rbac" + "github.com/ovh/cds/engine/api/repositoriesmanager" + "github.com/ovh/cds/engine/api/vcs" + "github.com/ovh/cds/engine/service" + "github.com/ovh/cds/sdk" +) + +func (api *API) getVCSByIdentifier(ctx context.Context, projectKey string, vcsIdentifier string) (*sdk.VCSProject, error) { + var vcsProject *sdk.VCSProject + var err error + if sdk.IsValidUUID(vcsIdentifier) { + vcsProject, err = vcs.LoadVCSByID(ctx, api.mustDB(), projectKey, vcsIdentifier) + } else { + vcsProject, err = vcs.LoadVCSByProject(ctx, api.mustDB(), projectKey, vcsIdentifier) + } + if err != nil { + return nil, err + } + return vcsProject, nil +} + +func (api *API) postVCSProjectHandler() ([]service.RbacChecker, service.Handler) { + return service.RBAC(rbac.ProjectManage), + func(ctx context.Context, w http.ResponseWriter, req *http.Request) error { + vars := mux.Vars(req) + pKey := vars["projectKey"] + + tx, err := api.mustDB().Begin() + if err != nil { + return sdk.WithStack(err) + } + defer tx.Rollback() // nolint + + project, err := project.Load(ctx, tx, pKey) + if err != nil { + return sdk.WithStack(err) + } + + var vcsProject sdk.VCSProject + if err := service.UnmarshalRequest(ctx, req, &vcsProject); err != nil { + return err + } + + vcsProject.ProjectID = project.ID + vcsProject.CreatedBy = getAPIConsumer(ctx).GetUsername() + + if err := vcs.Insert(ctx, tx, &vcsProject); err != nil { + return err + } + + vcsClient, err := repositoriesmanager.AuthorizedClient(ctx, tx, api.Cache, pKey, vcsProject.Name) + if err != nil { + return err + } + + if _, err := vcsClient.Repos(ctx); err != nil { + return err + } + + if err := tx.Commit(); err != nil { + return sdk.WithStack(err) + } + + return service.WriteMarshal(w, req, vcsProject, http.StatusCreated) + } +} + +func (api *API) putVCSProjectHandler() ([]service.RbacChecker, service.Handler) { + return service.RBAC(rbac.ProjectManage), + func(ctx context.Context, w http.ResponseWriter, req *http.Request) error { + vars := mux.Vars(req) + pKey := vars["projectKey"] + + vcsIdentifier, err := url.PathUnescape(vars["vcsIdentifier"]) + if err != nil { + return sdk.NewError(sdk.ErrWrongRequest, err) + } + + vcsOld, err := api.getVCSByIdentifier(ctx, pKey, vcsIdentifier) + if err != nil { + return err + } + + tx, err := api.mustDB().Begin() + if err != nil { + return sdk.WithStack(err) + } + defer tx.Rollback() // nolint + + var vcsProject sdk.VCSProject + if err := service.UnmarshalRequest(ctx, req, &vcsProject); err != nil { + return err + } + + vcsProject.ID = vcsOld.ID + vcsProject.Created = vcsOld.Created + vcsProject.CreatedBy = vcsOld.CreatedBy + vcsProject.ProjectID = vcsOld.ProjectID + + if err := vcs.Update(ctx, tx, &vcsProject); err != nil { + return err + } + + if err := tx.Commit(); err != nil { + return sdk.WithStack(err) + } + + return service.WriteMarshal(w, req, vcsProject, http.StatusCreated) + } +} + +func (api *API) deleteVCSProjectHandler() ([]service.RbacChecker, service.Handler) { + return service.RBAC(rbac.ProjectManage), + func(ctx context.Context, w http.ResponseWriter, req *http.Request) error { + vars := mux.Vars(req) + pKey := vars["projectKey"] + + vcsIdentifier, err := url.PathUnescape(vars["vcsIdentifier"]) + if err != nil { + return sdk.NewError(sdk.ErrWrongRequest, err) + } + + vcsProject, err := api.getVCSByIdentifier(ctx, pKey, vcsIdentifier) + if err != nil { + return err + } + + tx, err := api.mustDB().Begin() + if err != nil { + return sdk.WithStack(err) + } + defer tx.Rollback() // nolint + + project, err := project.Load(ctx, tx, pKey) + if err != nil { + return sdk.WithStack(err) + } + + if err := vcs.Delete(tx, project.ID, vcsProject.Name); err != nil { + return err + } + + if err := tx.Commit(); err != nil { + return sdk.WithStack(err) + } + + return nil + } +} + +// getVCSProjectAllHandler returns list of vcs of one project key +func (api *API) getVCSProjectAllHandler() ([]service.RbacChecker, service.Handler) { + return service.RBAC(rbac.ProjectRead), + func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { + vars := mux.Vars(r) + pKey := vars["projectKey"] + + vcsProjects, err := vcs.LoadAllVCSByProject(ctx, api.mustDB(), pKey) + if err != nil { + return sdk.WrapError(err, "unable to load vcs server on project %s", pKey) + } + + return service.WriteJSON(w, vcsProjects, http.StatusOK) + } +} + +func (api *API) getVCSProjectHandler() ([]service.RbacChecker, service.Handler) { + return service.RBAC(rbac.ProjectRead), + func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { + vars := mux.Vars(r) + pKey := vars["projectKey"] + + vcsIdentifier, err := url.PathUnescape(vars["vcsIdentifier"]) + if err != nil { + return sdk.NewError(sdk.ErrWrongRequest, err) + } + + vcsProject, err := api.getVCSByIdentifier(ctx, pKey, vcsIdentifier) + if err != nil { + return err + } + return service.WriteMarshal(w, r, vcsProject, http.StatusOK) + } +} diff --git a/engine/api/v2_project_vcs_test.go b/engine/api/v2_project_vcs_test.go new file mode 100644 index 0000000000..7df6a50a5f --- /dev/null +++ b/engine/api/v2_project_vcs_test.go @@ -0,0 +1,221 @@ +package api + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/go-gorp/gorp" + "github.com/golang/mock/gomock" + "github.com/ovh/cds/engine/api/services" + "github.com/ovh/cds/engine/api/services/mock_services" + "github.com/ovh/cds/engine/api/test" + "github.com/ovh/cds/engine/api/test/assets" + "github.com/ovh/cds/sdk" + "github.com/stretchr/testify/require" +) + +func Test_crudVCSOnProjectLambdaUserForbidden(t *testing.T) { + api, db, _ := newTestAPI(t) + + u, pass := assets.InsertLambdaUser(t, db) + proj := assets.InsertTestProject(t, db, api.Cache, sdk.RandomString(10), sdk.RandomString(10)) + + vars := map[string]string{ + "projectKey": proj.Key, + } + uriPost := api.Router.GetRouteV2("POST", api.postVCSProjectHandler, vars) + test.NotEmpty(t, uriPost) + + req := assets.NewAuthentifiedRequest(t, u, pass, "POST", uriPost, nil) + + w := httptest.NewRecorder() + api.Router.Mux.ServeHTTP(w, req) + require.Equal(t, 403, w.Code) + + uriGet := api.Router.GetRouteV2("GET", api.getVCSProjectAllHandler, vars) + test.NotEmpty(t, uriGet) + + reqGet := assets.NewAuthentifiedRequest(t, u, pass, "GET", uriGet, nil) + w2 := httptest.NewRecorder() + api.Router.Mux.ServeHTTP(w2, reqGet) + require.Equal(t, 403, w2.Code) +} + +func Test_crudVCSOnProjectLambdaUserOK(t *testing.T) { + api, db, _ := newTestAPI(t) + + proj := assets.InsertTestProject(t, db, api.Cache, sdk.RandomString(10), sdk.RandomString(10)) + user1, pass := assets.InsertLambdaUser(t, db) + + assets.InsertRBAcProject(t, db, "manage", proj.Key, *user1) + assets.InsertRBAcProject(t, db, "read", proj.Key, *user1) + + // Mock VCS + s, _ := assets.InsertService(t, db, t.Name()+"_VCS", sdk.TypeVCS) + // Setup a mock for all services called by the API + ctrl := gomock.NewController(t) + defer ctrl.Finish() + servicesClients := mock_services.NewMockClient(ctrl) + services.NewClient = func(_ gorp.SqlExecutor, _ []sdk.Service) services.Client { + return servicesClients + } + defer func() { + _ = services.Delete(db, s) + services.NewClient = services.NewDefaultClient + }() + + servicesClients.EXPECT(). + DoJSONRequest(gomock.Any(), "GET", "/vcs/my_vcs_server/repos", gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn( + func(ctx context.Context, method, path string, in interface{}, out interface{}, _ interface{}) (http.Header, int, error) { + repos := []sdk.VCSRepo{} + out = repos + return nil, 200, nil + }, + ).MaxTimes(1) + + vars := map[string]string{ + "projectKey": proj.Key, + } + uri := api.Router.GetRouteV2("POST", api.postVCSProjectHandler, vars) + test.NotEmpty(t, uri) + req := assets.NewAuthentifiedRequest(t, user1, pass, "POST", uri, nil) + + body := `version: v1.0 +name: my_vcs_server +type: bitbucketserver +description: "it's the test vcs server on project" +url: "http://my-vcs-server.localhost" +auth: + user: the-username + password: the-password +` + + // Here, we insert the vcs server as a CDS user (not administrator) + req.Body = io.NopCloser(strings.NewReader(body)) + req.Header.Set("Content-Type", "application/x-yaml") + + w := httptest.NewRecorder() + api.Router.Mux.ServeHTTP(w, req) + require.Equal(t, 201, w.Code) + + // Then, get the vcs server + uriGet := api.Router.GetRouteV2("GET", api.getVCSProjectAllHandler, vars) + test.NotEmpty(t, uriGet) + + reqGet := assets.NewAuthentifiedRequest(t, user1, pass, "GET", uriGet, nil) + w2 := httptest.NewRecorder() + api.Router.Mux.ServeHTTP(w2, reqGet) + require.Equal(t, 200, w2.Code) + + vcsProjects := []sdk.VCSProject{} + require.NoError(t, json.Unmarshal(w2.Body.Bytes(), &vcsProjects)) + require.Len(t, vcsProjects, 1) +} + +func Test_crudVCSOnProjectAdminOk(t *testing.T) { + api, db, _ := newTestAPI(t) + + u, pass := assets.InsertAdminUser(t, db) + proj := assets.InsertTestProject(t, db, api.Cache, sdk.RandomString(10), sdk.RandomString(10)) + + // Mock VCS + s, _ := assets.InsertService(t, db, t.Name()+"_VCS", sdk.TypeVCS) + // Setup a mock for all services called by the API + ctrl := gomock.NewController(t) + defer ctrl.Finish() + servicesClients := mock_services.NewMockClient(ctrl) + services.NewClient = func(_ gorp.SqlExecutor, _ []sdk.Service) services.Client { + return servicesClients + } + defer func() { + _ = services.Delete(db, s) + services.NewClient = services.NewDefaultClient + }() + + servicesClients.EXPECT(). + DoJSONRequest(gomock.Any(), "GET", "/vcs/my_vcs_server/repos", gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn( + func(ctx context.Context, method, path string, in interface{}, out interface{}, _ interface{}) (http.Header, int, error) { + repos := []sdk.VCSRepo{} + out = repos + return nil, 200, nil + }, + ).MaxTimes(1) + + vars := map[string]string{ + "projectKey": proj.Key, + } + uri := api.Router.GetRouteV2("POST", api.postVCSProjectHandler, vars) + test.NotEmpty(t, uri) + req := assets.NewAuthentifiedRequest(t, u, pass, "POST", uri, nil) + + body := `version: v1.0 +name: my_vcs_server +type: bitbucketserver +description: "it's the test vcs server on project" +url: "http://my-vcs-server.localhost" +auth: + user: the-username + password: the-password +` + + // Here, we insert the vcs server as a CDS administrator + req.Body = io.NopCloser(strings.NewReader(body)) + req.Header.Set("Content-Type", "application/x-yaml") + + w := httptest.NewRecorder() + api.Router.Mux.ServeHTTP(w, req) + require.Equal(t, 201, w.Code) + + // Then, get the vcs server in the list of vcs + uriGetAll := api.Router.GetRouteV2("GET", api.getVCSProjectAllHandler, vars) + test.NotEmpty(t, uriGetAll) + + reqGetAll := assets.NewAuthentifiedRequest(t, u, pass, "GET", uriGetAll, nil) + w2 := httptest.NewRecorder() + api.Router.Mux.ServeHTTP(w2, reqGetAll) + require.Equal(t, 200, w2.Code) + + vcsProjects := []sdk.VCSProject{} + require.NoError(t, json.Unmarshal(w2.Body.Bytes(), &vcsProjects)) + require.Len(t, vcsProjects, 1) + + // Then, try to get the vcs server directly + vars["vcsIdentifier"] = "my_vcs_server" + uriGet := api.Router.GetRouteV2("GET", api.getVCSProjectHandler, vars) + test.NotEmpty(t, uriGet) + + reqGet := assets.NewAuthentifiedRequest(t, u, pass, "GET", uriGet, nil) + w3 := httptest.NewRecorder() + api.Router.Mux.ServeHTTP(w3, reqGet) + require.Equal(t, 200, w3.Code) + + vcsProject := sdk.VCSProject{} + require.NoError(t, json.Unmarshal(w3.Body.Bytes(), &vcsProject)) + require.Equal(t, "my_vcs_server", vcsProject.Name) + require.Empty(t, vcsProject.Auth) + + // delete the vcs project + uriDelete := api.Router.GetRouteV2("DELETE", api.deleteVCSProjectHandler, vars) + test.NotEmpty(t, uriDelete) + + reqDelete := assets.NewAuthentifiedRequest(t, u, pass, "DELETE", uriDelete, nil) + w4 := httptest.NewRecorder() + api.Router.Mux.ServeHTTP(w4, reqDelete) + require.Equal(t, 204, w4.Code) + + reqGetAll2 := assets.NewAuthentifiedRequest(t, u, pass, "GET", uriGetAll, nil) + w5 := httptest.NewRecorder() + api.Router.Mux.ServeHTTP(w5, reqGetAll2) + require.Equal(t, 200, w5.Code) + + vcsProjects2 := []sdk.VCSProject{} + require.NoError(t, json.Unmarshal(w5.Body.Bytes(), &vcsProjects2)) + require.Len(t, vcsProjects2, 0) +} diff --git a/engine/cdn/types.go b/engine/cdn/types.go index efbd4f8f7e..020af56177 100644 --- a/engine/cdn/types.go +++ b/engine/cdn/types.go @@ -27,7 +27,7 @@ type handledMessage struct { IsTerminated bool } -// Service is the stuct representing a hooks µService +// Service is the stuct representing a CDN µService type Service struct { service.Common Cfg Configuration diff --git a/engine/elasticsearch/types.go b/engine/elasticsearch/types.go index 97ed4df8c4..92d022c713 100644 --- a/engine/elasticsearch/types.go +++ b/engine/elasticsearch/types.go @@ -8,7 +8,7 @@ import ( const indexNotFoundException = "index_not_found_exception" -// Service is the repostories service +// Service is the elasticsearch service type Service struct { service.Common Cfg Configuration diff --git a/engine/hooks/dao.go b/engine/hooks/dao.go index 05c5c4c09f..6f946c6ea9 100644 --- a/engine/hooks/dao.go +++ b/engine/hooks/dao.go @@ -37,7 +37,7 @@ func (d *dao) TaskExecutionsBalance() (int64, int64) { func (d *dao) FindAllTasks(ctx context.Context) ([]sdk.Task, error) { nbTasks, err := d.store.SetCard(rootKey) if err != nil { - return nil, sdk.WrapError(err, "unsable to setCard %v", rootKey) + return nil, sdk.WrapError(err, "unable to setCard %v", rootKey) } tasks := make([]*sdk.Task, nbTasks, nbTasks) for i := 0; i < nbTasks; i++ { diff --git a/engine/hooks/dao_webhooks.go b/engine/hooks/dao_webhooks.go new file mode 100644 index 0000000000..6039138809 --- /dev/null +++ b/engine/hooks/dao_webhooks.go @@ -0,0 +1,37 @@ +package hooks + +import ( + "strings" + + "github.com/ovh/cds/engine/cache" + "github.com/ovh/cds/sdk" +) + +var ( + EntitiesHookRootKey = cache.Key("hooks", "entities") +) + +func (d *dao) SaveRepoWebHook(t *sdk.Task) error { + entitiesHookKey := strings.ToLower(cache.Key(EntitiesHookRootKey, + t.Configuration[sdk.HookConfigVCSType].Value, + t.Configuration[sdk.HookConfigVCSServer].Value, + t.Configuration[sdk.HookConfigRepoFullName].Value, + t.Configuration[sdk.HookConfigTypeProject].Value)) + // Need this to be able to retrieve a task when comming from /v2/webhook/repository, route without uuid + if err := d.store.SetWithTTL(entitiesHookKey, t.UUID, 0); err != nil { + return err + } + if err := d.SaveTask(t); err != nil { + _ = d.store.Delete(entitiesHookKey) // nolint + return err + } + return nil +} + +func (d *dao) GetAllEntitiesHookKeysByPattern(hookKey string) ([]string, error) { + keys, err := d.store.Keys(strings.ToLower(hookKey)) + if err != nil { + return nil, err + } + return keys, nil +} diff --git a/engine/hooks/hooks.go b/engine/hooks/hooks.go index 3a0cdf0575..07a1c12fc9 100644 --- a/engine/hooks/hooks.go +++ b/engine/hooks/hooks.go @@ -13,6 +13,7 @@ import ( "github.com/ovh/cds/engine/cache" "github.com/ovh/cds/sdk" "github.com/ovh/cds/sdk/cdsclient" + "github.com/ovh/cds/sdk/jws" ) // New returns a new service @@ -125,6 +126,14 @@ func (s *Service) Serve(c context.Context) error { }() } + if s.Cfg.WebhooksPublicKeySign != "" { + webhookKey, err := jws.NewPublicKeyFromPEM([]byte(s.Cfg.WebhooksPublicKeySign)) + if err != nil { + return sdk.WithStack(err) + } + s.WebHooksParsedPublicKey = webhookKey + } + //Init the http server s.initRouter(ctx) server := &http.Server{ diff --git a/engine/hooks/hooks_handlers.go b/engine/hooks/hooks_handlers.go index a6f3755f82..5fb60fc2f9 100644 --- a/engine/hooks/hooks_handlers.go +++ b/engine/hooks/hooks_handlers.go @@ -2,22 +2,163 @@ package hooks import ( "context" + "encoding/json" "errors" "fmt" "io" "net/http" "sort" "strconv" + "strings" "time" "github.com/gorilla/mux" "github.com/rockbears/log" "github.com/ovh/cds/engine/api" + "github.com/ovh/cds/engine/cache" "github.com/ovh/cds/engine/service" "github.com/ovh/cds/sdk" ) +func (s *Service) registerHookHandler() service.Handler { + return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { + + var newHook sdk.Hook + //Read the body + body, err := io.ReadAll(r.Body) + if err != nil { + return sdk.NewErrorFrom(sdk.ErrWrongRequest, "unable to read request") + } + if err := json.Unmarshal(body, &newHook); err != nil { + return sdk.NewErrorFrom(sdk.ErrInvalidData, "unable to unmarshal request") + } + + if len(newHook.Configuration) == 0 { + return sdk.NewErrorFrom(sdk.ErrInvalidData, "missing hook configuration") + } + + vcsType, has := newHook.Configuration[sdk.HookConfigVCSType] + if !has || vcsType.Value == "" { + return sdk.NewErrorFrom(sdk.ErrWrongRequest, "missing vcs type") + } + + vcsName, has := newHook.Configuration[sdk.HookConfigVCSServer] + if !has || vcsName.Value == "" { + return sdk.NewErrorFrom(sdk.ErrWrongRequest, "missing vcs name") + } + + repoName, has := newHook.Configuration[sdk.HookConfigRepoFullName] + if !has || repoName.Value == "" { + return sdk.NewErrorFrom(sdk.ErrWrongRequest, "missing repository name") + } + + project, has := newHook.Configuration[sdk.HookConfigProject] + if !has || project.Value == "" { + return sdk.NewErrorFrom(sdk.ErrWrongRequest, "missing project key") + } + + if err := s.addTaskFromHook(newHook); err != nil { + return sdk.WithStack(err) + } + return nil + } +} + +func (s *Service) repositoryHooksHandler() service.Handler { + return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { + // Get repository data + vcsName := r.Header.Get(sdk.SignHeaderVCSName) + repoName := r.Header.Get(sdk.SignHeaderRepoName) + vcsType := r.Header.Get(sdk.SignHeaderVCSType) + + defer r.Body.Close() + body, err := io.ReadAll(r.Body) + if err != nil { + return sdk.NewErrorFrom(sdk.ErrUnknownError, "unable to read body: %v", err) + } + + // Search for existing hooks + hookKey := strings.ToLower(cache.Key(EntitiesHookRootKey, vcsType, vcsName, repoName, "*")) + keys, err := s.Dao.GetAllEntitiesHookKeysByPattern(hookKey) + if err != nil { + log.Error(ctx, "unable to check if a hook exist for %s: %v", hookKey, err) + return err + } + if len(keys) == 0 { + log.Warn(ctx, "Receive hook from %s, but there is no tasks", hookKey) + } + for _, k := range keys { + var uuid string + if _, err := s.Dao.store.Get(k, &uuid); err != nil { + log.Error(ctx, "unable to retrieve hook uuid for %s: %v", k, err) + continue + } + hook := s.Dao.FindTask(ctx, uuid) + if hook == nil { + return sdk.WrapError(sdk.ErrNotFound, "no hook found on") + } + + // Enqueue execution + exec := &sdk.TaskExecution{ + Timestamp: time.Now().UnixNano(), + Type: hook.Type, + UUID: hook.UUID, + Configuration: hook.Configuration, + Status: TaskExecutionScheduled, + WebHook: &sdk.WebHookExecution{ + RequestBody: body, + RequestHeader: r.Header, + RequestURL: r.URL.RawQuery, + }, + } + log.Info(ctx, "Save Entities hook execution for task %v", hook.Configuration) + if err := s.Dao.SaveTaskExecution(exec); err != nil { + return err + } + } + return service.WriteJSON(w, nil, http.StatusAccepted) + } +} + +func (s *Service) repositoryWebHookHandler() service.Handler { + return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { + vars := mux.Vars(r) + uuid := vars["uuid"] + + defer r.Body.Close() + body, err := io.ReadAll(r.Body) + if err != nil { + return sdk.NewErrorFrom(sdk.ErrUnknownError, "unable to read body: %v", err) + } + + hook := s.Dao.FindTask(ctx, uuid) + if hook == nil { + return sdk.WrapError(sdk.ErrNotFound, "no hook found on") + } + + // Enqueue execution + exec := &sdk.TaskExecution{ + Timestamp: time.Now().UnixNano(), + Type: hook.Type, + UUID: hook.UUID, + Configuration: hook.Configuration, + Status: TaskExecutionScheduled, + WebHook: &sdk.WebHookExecution{ + RequestBody: body, + RequestHeader: r.Header, + RequestURL: r.URL.RawQuery, + }, + } + log.Debug(ctx, "Save execution for task %v", hook.Configuration) + if err := s.Dao.SaveTaskExecution(exec); err != nil { + return err + } + + return service.WriteJSON(w, exec, http.StatusAccepted) + } +} + func (s *Service) webhookHandler() service.Handler { return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { //Get the UUID of the webhook @@ -382,9 +523,11 @@ func (s *Service) deleteTaskBulkHandler() service.Handler { if err := s.stopTask(ctx, t); err != nil { return sdk.WrapError(sdk.ErrNotFound, "Stop task %s", err) } + if err := s.deleteTask(ctx, t); err != nil { return err } + } return nil @@ -415,7 +558,7 @@ func (s *Service) postTaskBulkHandler() service.Handler { func (s *Service) addTask(ctx context.Context, h *sdk.NodeHook) error { //Parse the hook as a task - t, err := s.hookToTask(h) + t, err := s.nodeHookToTask(h) if err != nil { return sdk.WrapError(err, "Unable to parse hook") } @@ -432,6 +575,18 @@ func (s *Service) addTask(ctx context.Context, h *sdk.NodeHook) error { return nil } +func (s *Service) addTaskFromHook(h sdk.Hook) error { + t, err := s.hookToTask(h) + if err != nil { + return err + } + + if err := s.Dao.SaveRepoWebHook(t); err != nil { + return err + } + return nil +} + func (s *Service) addAndExecuteTask(ctx context.Context, nr sdk.WorkflowNodeRun) (sdk.Task, sdk.TaskExecution, error) { // Parse the hook as a task t, err := s.nodeRunToTask(nr) @@ -456,7 +611,7 @@ var errNoTask = errors.New("task not found") func (s *Service) updateTask(ctx context.Context, h *sdk.NodeHook) error { //Parse the hook as a task - t, err := s.hookToTask(h) + t, err := s.nodeHookToTask(h) if err != nil { return sdk.WrapError(err, "Unable to parse hook") } @@ -490,6 +645,15 @@ func (s *Service) deleteTask(ctx context.Context, t *sdk.Task) error { switch t.Type { case TypeGerrit: s.stopGerritHookTask(t) + case TypeEntitiesHook: + entitiesHookKey := cache.Key(EntitiesHookRootKey, + t.Configuration[sdk.HookConfigVCSType].Value, + t.Configuration[sdk.HookConfigVCSServer].Value, + t.Configuration[sdk.HookConfigRepoFullName].Value, + t.Configuration[sdk.HookConfigTypeProject].Value) + if err := s.Dao.store.Delete(entitiesHookKey); err != nil { + return err + } } //Delete the task diff --git a/engine/hooks/hooks_router.go b/engine/hooks/hooks_router.go index a00a6e3dc1..ddaec9964b 100644 --- a/engine/hooks/hooks_router.go +++ b/engine/hooks/hooks_router.go @@ -2,8 +2,15 @@ package hooks import ( "context" + "crypto/rsa" + "net/http" + "sync" + + "github.com/rockbears/log" + "gopkg.in/spacemonkeygo/httpsig.v0" "github.com/ovh/cds/engine/service" + "github.com/ovh/cds/sdk" ) func (s *Service) initRouter(ctx context.Context) { @@ -23,6 +30,10 @@ func (s *Service) initRouter(ctx context.Context) { r.Handle("/mon/metrics", nil, r.GET(service.GetPrometheustMetricsHandler(s), service.OverrideAuth(service.NoAuthMiddleware))) r.Handle("/mon/metrics/all", nil, r.GET(service.GetMetricsHandler, service.OverrideAuth(service.NoAuthMiddleware))) + r.Handle("/v2/webhook/repository", nil, r.POST(s.repositoryHooksHandler, service.OverrideAuth(CheckWebhookRequestSignatureMiddleware(s.WebHooksParsedPublicKey)))) + r.Handle("/v2/webhook/repository/{uuid}", nil, r.POST(s.repositoryWebHookHandler, service.OverrideAuth(service.NoAuthMiddleware))) + r.Handle("/v2/task", nil, r.POST(s.registerHookHandler)) + r.Handle("/webhook/{uuid}", nil, r.POST(s.webhookHandler, service.OverrideAuth(service.NoAuthMiddleware)), r.GET(s.webhookHandler, service.OverrideAuth(service.NoAuthMiddleware)), r.DELETE(s.webhookHandler, service.OverrideAuth(service.NoAuthMiddleware)), r.PUT(s.webhookHandler, service.OverrideAuth(service.NoAuthMiddleware))) r.Handle("/task", nil, r.POST(s.postTaskHandler), r.GET(s.getTasksHandler)) r.Handle("/task/bulk/start", nil, r.GET(s.startTasksHandler)) @@ -36,3 +47,41 @@ func (s *Service) initRouter(ctx context.Context) { r.Handle("/task/{uuid}/execution/{timestamp}", nil, r.GET(s.getTaskExecutionHandler)) r.Handle("/task/{uuid}/execution/{timestamp}/stop", nil, r.POST(s.postStopTaskExecutionHandler)) } + +type webhookHttpVerifier struct { + sync.Mutex + pubKey *rsa.PublicKey +} + +func (v *webhookHttpVerifier) SetKey(pubKey *rsa.PublicKey) { + v.Lock() + defer v.Unlock() + v.pubKey = pubKey +} + +func (v *webhookHttpVerifier) GetKey(id string) interface{} { + v.Lock() + defer v.Unlock() + return v.pubKey +} + +var ( + webhookHTTPVerifier *webhookHttpVerifier +) + +func CheckWebhookRequestSignatureMiddleware(pubKey *rsa.PublicKey) service.Middleware { + webhookHTTPVerifier = new(webhookHttpVerifier) + webhookHTTPVerifier.SetKey(pubKey) + + verifier := httpsig.NewVerifier(webhookHTTPVerifier) + verifier.SetRequiredHeaders([]string{"(request-target)", "host", "date", sdk.SignHeaderVCSType, sdk.SignHeaderVCSName, sdk.SignHeaderRepoName}) + + return func(ctx context.Context, w http.ResponseWriter, req *http.Request, rc *service.HandlerConfig) (context.Context, error) { + if err := verifier.Verify(req); err != nil { + return ctx, sdk.NewError(sdk.ErrUnauthorized, err) + } + + log.Debug(ctx, "Request has been successfully verified") + return ctx, nil + } +} diff --git a/engine/hooks/tasks.go b/engine/hooks/tasks.go index 4c07d95d45..e1c04fd6a8 100644 --- a/engine/hooks/tasks.go +++ b/engine/hooks/tasks.go @@ -26,6 +26,7 @@ const ( TypeWorkflowHook = "Workflow" TypeOutgoingWebHook = "OutgoingWebhook" TypeOutgoingWorkflow = "OutgoingWorkflow" + TypeEntitiesHook = "EntitiesHook" GithubHeader = "X-Github-Event" GitlabHeader = "X-Gitlab-Event" @@ -66,6 +67,12 @@ func (s *Service) synchronizeTasks(ctx context.Context) error { log.Info(ctx, "Hooks> All tasks has been resynchronized (%.3fs)", time.Since(t0).Seconds()) }() + log.Info(ctx, "Hooks> Synchronizing entities hooks from CDS API (%s)", s.Cfg.API.HTTP.URL) + repos, err := s.Client.RepositoriesListAll(ctx) + if err != nil { + return sdk.WrapError(err, "unable to list all repositories") + } + log.Info(ctx, "Hooks> Synchronizing tasks from CDS API (%s)", s.Cfg.API.HTTP.URL) // Get all hooks from CDS, and synchronize the tasks in cache @@ -77,6 +84,9 @@ func (s *Service) synchronizeTasks(ctx context.Context) error { for i := range hooks { mHookIDs[hooks[i].UUID] = struct{}{} } + for i := range repos { + mHookIDs[repos[i].ID] = struct{}{} + } // Get all node run execution ids from CDS, and synchronize the outgoing tasks in cache executionIDs, err := s.Client.WorkflowAllHooksExecutions() @@ -119,7 +129,7 @@ func (s *Service) synchronizeTasks(ctx context.Context) error { log.Error(ctx, "Hook> Unable to synchronize task %+v: %v", h, err) continue } - t, err := s.hookToTask(&h) + t, err := s.nodeHookToTask(&h) if err != nil { log.Error(ctx, "Hook> Unable to transform hook to task %+v: %v", h, err) continue @@ -129,6 +139,17 @@ func (s *Service) synchronizeTasks(ctx context.Context) error { continue } } + for _, r := range repos { + h := sdk.Hook{ + UUID: r.ID, + HookType: sdk.RepositoryEntitiesHook, + Configuration: r.HookConfiguration, + } + if err := s.addTaskFromHook(h); err != nil { + log.Error(ctx, "Hook> Unable to save task %+v: %v", h, err) + continue + } + } // Start listening to gerrit event stream vcsGerritConfig, err := s.Client.VCSGerritConfiguration() @@ -174,7 +195,20 @@ func (s *Service) initGerritStreamEvent(ctx context.Context, vcsName string, vcs gerritRepoHooks[vcsName] = true } -func (s *Service) hookToTask(h *sdk.NodeHook) (*sdk.Task, error) { +func (s *Service) hookToTask(r sdk.Hook) (*sdk.Task, error) { + switch r.HookType { + case sdk.RepositoryEntitiesHook: + return &sdk.Task{ + UUID: r.UUID, + Type: TypeEntitiesHook, + Configuration: r.Configuration, + }, nil + default: + return nil, sdk.WithStack(sdk.ErrNotImplemented) + } +} + +func (s *Service) nodeHookToTask(h *sdk.NodeHook) (*sdk.Task, error) { switch h.HookModelName { case sdk.GerritHookModelName: return &sdk.Task{ @@ -284,7 +318,7 @@ func (s *Service) startTask(ctx context.Context, t *sdk.Task) (*sdk.TaskExecutio } switch t.Type { - case TypeWebHook, TypeRepoManagerWebHook, TypeWorkflowHook: + case TypeWebHook, TypeRepoManagerWebHook, TypeWorkflowHook, TypeEntitiesHook: return nil, nil case TypeScheduler, TypeRepoPoller: return nil, s.prepareNextScheduledTaskExecution(ctx, t) @@ -407,6 +441,9 @@ func (s *Service) doTask(ctx context.Context, t *sdk.Task, e *sdk.TaskExecution) switch { case e.GerritEvent != nil: h, err = s.doGerritExecution(e) + case e.Type == TypeEntitiesHook: + log.Info(ctx, "Entities hook executed") + return false, nil case e.WebHook != nil && e.Type == TypeOutgoingWebHook: err = s.doOutgoingWebHookExecution(ctx, e) case e.Type == TypeOutgoingWorkflow: diff --git a/engine/hooks/tasks_test.go b/engine/hooks/tasks_test.go index a7b07bf61f..c8a45e24a5 100644 --- a/engine/hooks/tasks_test.go +++ b/engine/hooks/tasks_test.go @@ -91,6 +91,7 @@ func Test_dequeueTaskExecutions_ScheduledTask(t *testing.T) { m.EXPECT().WorkflowAllHooksList().Return([]sdk.NodeHook{}, nil) m.EXPECT().WorkflowAllHooksExecutions().Return([]string{}, nil) m.EXPECT().VCSGerritConfiguration().Return(nil, nil).AnyTimes() + m.EXPECT().RepositoriesListAll(gomock.Any()).Return([]sdk.ProjectRepository{}, nil).Times(2) require.NoError(t, s.synchronizeTasks(ctx)) // Start the goroutine @@ -120,7 +121,7 @@ func Test_dequeueTaskExecutions_ScheduledTask(t *testing.T) { } // Create a new task - scheduledTask, err := s.hookToTask(h) + scheduledTask, err := s.nodeHookToTask(h) require.NoError(t, s.Dao.SaveTask(scheduledTask)) require.NoError(t, s.startTasks(ctx)) @@ -196,6 +197,7 @@ func Test_synchronizeTasks(t *testing.T) { m.EXPECT().WorkflowAllHooksList().Return([]sdk.NodeHook{}, nil) m.EXPECT().WorkflowAllHooksExecutions().Return([]string{}, nil) + m.EXPECT().RepositoriesListAll(gomock.Any()).Return([]sdk.ProjectRepository{}, nil).Times(2) require.NoError(t, s.synchronizeTasks(ctx)) tasks, err := s.Dao.FindAllTasks(ctx) diff --git a/engine/hooks/types.go b/engine/hooks/types.go index 22ab22f532..c01d22631d 100644 --- a/engine/hooks/types.go +++ b/engine/hooks/types.go @@ -1,6 +1,7 @@ package hooks import ( + "crypto/rsa" "github.com/ovh/cds/engine/api" "github.com/ovh/cds/engine/cache" "github.com/ovh/cds/engine/service" @@ -17,11 +18,12 @@ const ( // Service is the stuct representing a hooks µService type Service struct { service.Common - Cfg Configuration - Router *api.Router - Cache cache.Store - Dao dao - Maintenance bool + Cfg Configuration + Router *api.Router + Cache cache.Store + Dao dao + Maintenance bool + WebHooksParsedPublicKey *rsa.PublicKey } // Configuration is the hooks configuration structure @@ -42,4 +44,5 @@ type Configuration struct { Password string `toml:"password" json:"-"` } `toml:"redis" comment:"Connect CDS to a redis cache If you more than one CDS instance and to avoid losing data at startup" json:"redis"` } `toml:"cache" comment:"######################\n CDS Hooks Cache Settings \n######################" json:"cache"` + WebhooksPublicKeySign string `toml:"webhooksPublicKeySign" comment:"Public key to check call signature on handler /v2/webhook/repository"` } diff --git a/engine/sql/api/246_project_repo_hook.sql b/engine/sql/api/246_project_repo_hook.sql new file mode 100644 index 0000000000..2c36ef3bd5 --- /dev/null +++ b/engine/sql/api/246_project_repo_hook.sql @@ -0,0 +1,5 @@ +-- +migrate Up +ALTER TABLE project_repository ADD COLUMN hook_configuration JSONB; + +-- +migrate Down +ALTER TABLE project_repository DROP COLUMN hook_configuration; diff --git a/engine/vcs/vcs.go b/engine/vcs/vcs.go index 877a5a0567..c2b24a9a78 100644 --- a/engine/vcs/vcs.go +++ b/engine/vcs/vcs.go @@ -85,7 +85,7 @@ func (s *Service) CheckConfiguration(config interface{}) error { func (s *Service) getConsumer(name string, vcsAuth sdk.VCSAuth) (sdk.VCSServer, error) { if vcsAuth.URL != "" { switch vcsAuth.Type { - case "gitea": + case sdk.VCSTypeGitea: return gitea.New(strings.TrimSuffix(vcsAuth.URL, "/"), s.Cfg.API.HTTP.URL, s.UI.HTTP.URL, @@ -94,14 +94,14 @@ func (s *Service) getConsumer(name string, vcsAuth sdk.VCSAuth) (sdk.VCSServer, vcsAuth.Username, vcsAuth.Token, ), nil - case "bitbucketcloud": + case sdk.VCSTypeBitbucketCloud: return bitbucketcloud.New( strings.TrimSuffix(vcsAuth.URL, "/"), s.UI.HTTP.URL, s.Cfg.ProxyWebhook, s.Cache, ), nil - case "bitbucketserver": + case sdk.VCSTypeBitbucketServer: return bitbucketserver.New( strings.TrimSuffix(vcsAuth.URL, "/"), s.Cfg.API.HTTP.URL, @@ -111,7 +111,7 @@ func (s *Service) getConsumer(name string, vcsAuth sdk.VCSAuth) (sdk.VCSServer, vcsAuth.Username, vcsAuth.Token, ), nil - case "gerrit": + case sdk.VCSTypeGerrit: return gerrit.New( vcsAuth.URL, s.Cache, @@ -120,7 +120,7 @@ func (s *Service) getConsumer(name string, vcsAuth sdk.VCSAuth) (sdk.VCSServer, vcsAuth.Username, vcsAuth.Token, ), nil - case "github": + case sdk.VCSTypeGithub: return github.New( vcsAuth.URL, vcsAuth.URLApi, @@ -129,7 +129,7 @@ func (s *Service) getConsumer(name string, vcsAuth sdk.VCSAuth) (sdk.VCSServer, s.Cfg.ProxyWebhook, s.Cache, ), nil - case "gitlab": + case sdk.VCSTypeGitlab: return gitlab.New( vcsAuth.URL, s.UI.HTTP.URL, diff --git a/sdk/cdsclient/client_hook.go b/sdk/cdsclient/client_hook.go index bc783580d9..7c092f1eed 100644 --- a/sdk/cdsclient/client_hook.go +++ b/sdk/cdsclient/client_hook.go @@ -1,6 +1,7 @@ package cdsclient import ( + "context" "fmt" "strconv" "time" @@ -25,3 +26,10 @@ func (c *client) PollVCSEvents(uuid string, workflowID int64, vcsServer string, return events, interval, nil } + +func (c *client) RepositoriesListAll(ctx context.Context) ([]sdk.ProjectRepository, error) { + url := fmt.Sprintf("/v2/project/repositories") + var repos []sdk.ProjectRepository + _, err := c.GetJSON(ctx, url, &repos) + return repos, err +} diff --git a/sdk/cdsclient/interface.go b/sdk/cdsclient/interface.go index 1a6b99300d..6160ea71b8 100644 --- a/sdk/cdsclient/interface.go +++ b/sdk/cdsclient/interface.go @@ -310,6 +310,7 @@ type HookClient interface { PollVCSEvents(uuid string, workflowID int64, vcsServer string, timestamp int64) (events sdk.RepositoryEvents, interval time.Duration, err error) VCSConfiguration() (map[string]sdk.VCSConfiguration, error) VCSGerritConfiguration() (map[string]sdk.VCSGerritConfiguration, error) + RepositoriesListAll(ctx context.Context) ([]sdk.ProjectRepository, error) } // ServiceClient exposes functions used for services diff --git a/sdk/cdsclient/mock_cdsclient/interface_mock.go b/sdk/cdsclient/mock_cdsclient/interface_mock.go index a90fc7ceae..7614c08195 100644 --- a/sdk/cdsclient/mock_cdsclient/interface_mock.go +++ b/sdk/cdsclient/mock_cdsclient/interface_mock.go @@ -3849,6 +3849,21 @@ func (mr *MockHookClientMockRecorder) PollVCSEvents(uuid, workflowID, vcsServer, return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PollVCSEvents", reflect.TypeOf((*MockHookClient)(nil).PollVCSEvents), uuid, workflowID, vcsServer, timestamp) } +// RepositoriesListAll mocks base method. +func (m *MockHookClient) RepositoriesListAll(ctx context.Context) ([]sdk.ProjectRepository, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RepositoriesListAll", ctx) + ret0, _ := ret[0].([]sdk.ProjectRepository) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RepositoriesListAll indicates an expected call of RepositoriesListAll. +func (mr *MockHookClientMockRecorder) RepositoriesListAll(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RepositoriesListAll", reflect.TypeOf((*MockHookClient)(nil).RepositoriesListAll), ctx) +} + // VCSConfiguration mocks base method. func (m *MockHookClient) VCSConfiguration() (map[string]sdk.VCSConfiguration, error) { m.ctrl.T.Helper() @@ -7253,6 +7268,21 @@ func (mr *MockInterfaceMockRecorder) RepositoriesList(projectKey, repoManager, r return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RepositoriesList", reflect.TypeOf((*MockInterface)(nil).RepositoriesList), projectKey, repoManager, resync) } +// RepositoriesListAll mocks base method. +func (m *MockInterface) RepositoriesListAll(ctx context.Context) ([]sdk.ProjectRepository, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RepositoriesListAll", ctx) + ret0, _ := ret[0].([]sdk.ProjectRepository) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RepositoriesListAll indicates an expected call of RepositoriesListAll. +func (mr *MockInterfaceMockRecorder) RepositoriesListAll(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RepositoriesListAll", reflect.TypeOf((*MockInterface)(nil).RepositoriesListAll), ctx) +} + // Request mocks base method. func (m *MockInterface) Request(ctx context.Context, method, path string, body io.Reader, mods ...cdsclient.RequestModifier) ([]byte, http.Header, int, error) { m.ctrl.T.Helper() diff --git a/sdk/hook.go b/sdk/hook.go index cd6b8b3aa4..9fe831dbc5 100644 --- a/sdk/hook.go +++ b/sdk/hook.go @@ -17,6 +17,7 @@ const ( HookConfigTargetHook = "target_hook" HookConfigWorkflowID = "workflow_id" HookConfigWebHookID = "webHookID" + HookConfigVCSType = "vcsType" HookConfigVCSServer = "vcsServer" HookConfigEventFilter = "eventFilter" HookConfigRepoFullName = "repoFullName" diff --git a/sdk/hooks.go b/sdk/hooks.go index 243441f8ea..92f150b1d1 100644 --- a/sdk/hooks.go +++ b/sdk/hooks.go @@ -1,15 +1,91 @@ package sdk +import ( + "encoding/json" + "fmt" + + "database/sql/driver" +) + +const ( + RepositoryEntitiesHook = "EntitiesHook" + SignHeaderVCSName = "X-Cds-Hooks-Vcs-Name" + SignHeaderRepoName = "X-Cds-Hooks-Repo-Name" + SignHeaderVCSType = "X-Cds-Hooks-Vcs-Type" +) + +type Hook struct { + UUID string + HookType string + Configuration HookConfiguration +} +type HookConfiguration map[string]WorkflowNodeHookConfigValue + +func (hc HookConfiguration) Value() (driver.Value, error) { + j, err := json.Marshal(hc) + return j, WrapError(err, "cannot marshal HookConfiguration") +} + +func (hc *HookConfiguration) Scan(src interface{}) error { + if src == nil { + return nil + } + source, ok := src.([]byte) + if !ok { + return WithStack(fmt.Errorf("type assertion .([]byte) failed (%T)", src)) + } + return WrapError(JSONUnmarshal(source, hc), "cannot unmarshal HookConfiguration") +} + +func NewEntitiesHook(uuid, projectKey, vcsType, vcsName, repoName string) Hook { + return Hook{ + UUID: uuid, + HookType: RepositoryEntitiesHook, + Configuration: map[string]WorkflowNodeHookConfigValue{ + HookConfigProject: { + Value: projectKey, + Type: HookConfigTypeString, + Configurable: false, + }, + HookConfigVCSServer: { + Value: vcsName, + Type: HookConfigTypeString, + Configurable: false, + }, + HookConfigVCSType: { + Value: vcsType, + Type: HookConfigTypeString, + Configurable: false, + }, + HookConfigRepoFullName: { + Value: repoName, + Type: HookConfigTypeString, + Configurable: false, + }, + }, + } +} + +// HookConfigValue represents the value of a node hook config +type HookConfigValue struct { + Value string `json:"value"` + Configurable bool `json:"configurable"` + Type string `json:"type"` + MultipleChoiceList []string `json:"multiple_choice_list"` +} + // Task is a generic hook tasks such as webhook, scheduler,... which will be started and wait for execution type Task struct { UUID string `json:"uuid" cli:"UUID,key"` Type string `json:"type" cli:"Type"` - Config WorkflowNodeHookConfig `json:"config" cli:"Config"` Conditions WorkflowNodeConditions `json:"conditions" cli:"Conditions"` Stopped bool `json:"stopped" cli:"Stopped"` Executions []TaskExecution `json:"executions"` NbExecutionsTotal int `json:"nb_executions_total" cli:"nb_executions_total"` NbExecutionsTodo int `json:"nb_executions_todo" cli:"nb_executions_todo"` + Configuration HookConfiguration `json:"configuration" cli:"configuration"` + // DEPRECATED + Config WorkflowNodeHookConfig `json:"config" cli:"Config"` } // TaskExecution represents an execution instance of a task. It the task is a webhook; this represents the call of the webhook @@ -21,13 +97,15 @@ type TaskExecution struct { LastError string `json:"last_error,omitempty" cli:"last_error"` ProcessingTimestamp int64 `json:"processing_timestamp" cli:"processing_timestamp"` WorkflowRun int64 `json:"workflow_run" cli:"workflow_run"` - Config WorkflowNodeHookConfig `json:"config" cli:"-"` WebHook *WebHookExecution `json:"webhook,omitempty" cli:"-"` Kafka *KafkaTaskExecution `json:"kafka,omitempty" cli:"-"` RabbitMQ *RabbitMQTaskExecution `json:"rabbitmq,omitempty" cli:"-"` ScheduledTask *ScheduledTaskExecution `json:"scheduled_task,omitempty" cli:"-"` GerritEvent *GerritEventExecution `json:"gerrit,omitempty" cli:"-"` Status string `json:"status" cli:"status"` + Configuration HookConfiguration `json:"configuration" cli:"-"` + // DEPRECATED + Config WorkflowNodeHookConfig `json:"config" cli:"-"` } // GerritEventExecution contains specific data for a gerrit event execution diff --git a/sdk/project.go b/sdk/project.go index b7b3701a79..f880a9d515 100644 --- a/sdk/project.go +++ b/sdk/project.go @@ -12,7 +12,7 @@ import ( type ProjectIdentifiers struct { ID int64 `json:"-" yaml:"-" db:"id"` - Key string `json:"-" yaml:"-"db:"projectkey"` + Key string `json:"-" yaml:"-" db:"projectkey"` } type Projects []Project diff --git a/sdk/repository.go b/sdk/repository.go index 4c4c8a196e..d78cbef2e2 100644 --- a/sdk/repository.go +++ b/sdk/repository.go @@ -5,9 +5,10 @@ import ( ) type ProjectRepository struct { - ID string `json:"id" db:"id"` - Name string `json:"name" db:"name" cli:"name,key"` - Created time.Time `json:"created" db:"created"` - CreatedBy string `json:"created_by" db:"created_by"` - VCSProjectID string `json:"-" db:"vcs_project_id"` + ID string `json:"id" db:"id"` + Name string `json:"name" db:"name" cli:"name,key"` + Created time.Time `json:"created" db:"created"` + CreatedBy string `json:"created_by" db:"created_by"` + VCSProjectID string `json:"-" db:"vcs_project_id"` + HookConfiguration HookConfiguration `json:"hook_configuration" db:"hook_configuration"` } diff --git a/sdk/vcs.go b/sdk/vcs.go index 397569fd0e..33eba3d813 100644 --- a/sdk/vcs.go +++ b/sdk/vcs.go @@ -26,6 +26,13 @@ const ( HeaderXAccessToken = "X-CDS-ACCESS-TOKEN" // DEPRECATED HeaderXAccessTokenCreated = "X-CDS-ACCESS-TOKEN-CREATED" // DEPRECATED HeaderXAccessTokenSecret = "X-CDS-ACCESS-TOKEN-SECRET" // DEPRECATED + + VCSTypeGitea = "gitea" + VCSTypeGerrit = "gerrit" + VCSTypeGitlab = "gitlab" + VCSTypeBitbucketServer = "bitbucketserver" + VCSTypeBitbucketCloud = "bitbucketcloud" + VCSTypeGithub = "github" ) var (