From 8cdcecb56f3eb9cbab04ff02c865f3dfb272d46e Mon Sep 17 00:00:00 2001 From: Vistaar Juneja Date: Thu, 3 Aug 2023 15:31:32 +0100 Subject: [PATCH] add pipelines and executions handlers to gitness --- cmd/gitness/wire.go | 4 + cmd/gitness/wire_gen.go | 8 +- .../api/controller/execution/controller.go | 31 +++ internal/api/controller/execution/wire.go | 24 ++ .../api/controller/pipeline/controller.go | 39 +++ internal/api/controller/pipeline/create.go | 113 +++++++++ internal/api/controller/pipeline/delete.go | 34 +++ internal/api/controller/pipeline/find.go | 26 ++ internal/api/controller/pipeline/update.go | 50 ++++ internal/api/controller/pipeline/wire.go | 25 ++ internal/api/handler/pipeline/create.go | 33 +++ internal/api/handler/pipeline/delete.go | 40 +++ internal/api/handler/pipeline/find.go | 49 ++++ internal/api/handler/pipeline/update.go | 45 ++++ internal/api/openapi/openapi.go | 1 + internal/api/openapi/pipeline.go | 81 +++++++ internal/api/request/pipeline.go | 54 +++++ internal/router/api.go | 25 +- internal/router/wire.go | 6 +- internal/store/database.go | 81 +++++++ internal/store/database/execution.go | 59 +++++ .../sqlite/0020_create_table_builds.up.sql | 48 ++++ .../0020_create_table_pipelines.down.sql | 1 + .../sqlite/0020_create_table_pipelines.up.sql | 30 +++ .../sqlite/0021_create_table_builds.down.sql | 1 + .../0021_create_table_executions.up.sql | 46 ++++ internal/store/database/pipeline.go | 228 ++++++++++++++++++ internal/store/database/wire.go | 12 + types/enum/scm.go | 20 ++ types/execution.go | 42 ++++ types/pipeline.go | 27 +++ 31 files changed, 1280 insertions(+), 3 deletions(-) create mode 100644 internal/api/controller/execution/controller.go create mode 100644 internal/api/controller/execution/wire.go create mode 100644 internal/api/controller/pipeline/controller.go create mode 100644 internal/api/controller/pipeline/create.go create mode 100644 internal/api/controller/pipeline/delete.go create mode 100644 internal/api/controller/pipeline/find.go create mode 100644 internal/api/controller/pipeline/update.go create mode 100644 internal/api/controller/pipeline/wire.go create mode 100644 internal/api/handler/pipeline/create.go create mode 100644 internal/api/handler/pipeline/delete.go create mode 100644 internal/api/handler/pipeline/find.go create mode 100644 internal/api/handler/pipeline/update.go create mode 100644 internal/api/openapi/pipeline.go create mode 100644 internal/api/request/pipeline.go create mode 100644 internal/store/database/execution.go create mode 100644 internal/store/database/migrate/sqlite/0020_create_table_builds.up.sql create mode 100644 internal/store/database/migrate/sqlite/0020_create_table_pipelines.down.sql create mode 100644 internal/store/database/migrate/sqlite/0020_create_table_pipelines.up.sql create mode 100644 internal/store/database/migrate/sqlite/0021_create_table_builds.down.sql create mode 100644 internal/store/database/migrate/sqlite/0021_create_table_executions.up.sql create mode 100644 internal/store/database/pipeline.go create mode 100644 types/enum/scm.go create mode 100644 types/execution.go create mode 100644 types/pipeline.go diff --git a/cmd/gitness/wire.go b/cmd/gitness/wire.go index ddd1bd0aa..6e9c05fd4 100644 --- a/cmd/gitness/wire.go +++ b/cmd/gitness/wire.go @@ -16,7 +16,9 @@ import ( gitrpcserver "github.com/harness/gitness/gitrpc/server" gitrpccron "github.com/harness/gitness/gitrpc/server/cron" checkcontroller "github.com/harness/gitness/internal/api/controller/check" + "github.com/harness/gitness/internal/api/controller/execution" "github.com/harness/gitness/internal/api/controller/githook" + "github.com/harness/gitness/internal/api/controller/pipeline" "github.com/harness/gitness/internal/api/controller/principal" "github.com/harness/gitness/internal/api/controller/pullreq" "github.com/harness/gitness/internal/api/controller/repo" @@ -90,6 +92,8 @@ func initSystem(ctx context.Context, config *types.Config) (*cliserver.System, e codecomments.WireSet, gitrpccron.WireSet, checkcontroller.WireSet, + execution.WireSet, + pipeline.WireSet, ) return &cliserver.System{}, nil } diff --git a/cmd/gitness/wire_gen.go b/cmd/gitness/wire_gen.go index ee8870a0f..55ad410dd 100644 --- a/cmd/gitness/wire_gen.go +++ b/cmd/gitness/wire_gen.go @@ -14,7 +14,9 @@ import ( server3 "github.com/harness/gitness/gitrpc/server" "github.com/harness/gitness/gitrpc/server/cron" check2 "github.com/harness/gitness/internal/api/controller/check" + "github.com/harness/gitness/internal/api/controller/execution" "github.com/harness/gitness/internal/api/controller/githook" + "github.com/harness/gitness/internal/api/controller/pipeline" "github.com/harness/gitness/internal/api/controller/principal" "github.com/harness/gitness/internal/api/controller/pullreq" "github.com/harness/gitness/internal/api/controller/repo" @@ -84,7 +86,11 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro return nil, err } repoController := repo.ProvideController(config, db, provider, pathUID, authorizer, pathStore, repoStore, spaceStore, principalStore, gitrpcInterface) + executionStore := database.ProvideExecutionStore(db) + executionController := execution.ProvideController(db, authorizer, executionStore, repoStore, spaceStore) spaceController := space.ProvideController(db, provider, pathUID, authorizer, pathStore, spaceStore, repoStore, principalStore, repoController, membershipStore) + pipelineStore := database.ProvidePipelineStore(db) + pipelineController := pipeline.ProvideController(db, pathUID, pathStore, repoStore, authorizer, pipelineStore, spaceStore) pullReqStore := database.ProvidePullReqStore(db, principalInfoCache) pullReqActivityStore := database.ProvidePullReqActivityStore(db, principalInfoCache) codeCommentView := database.ProvideCodeCommentView(db) @@ -138,7 +144,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro principalController := principal.ProvideController(principalStore) checkStore := database.ProvideCheckStore(db, principalInfoCache) checkController := check2.ProvideController(db, authorizer, repoStore, checkStore, gitrpcInterface) - apiHandler := router.ProvideAPIHandler(config, authenticator, repoController, spaceController, pullreqController, webhookController, githookController, serviceaccountController, controller, principalController, checkController) + apiHandler := router.ProvideAPIHandler(config, authenticator, repoController, executionController, spaceController, pipelineController, pullreqController, webhookController, githookController, serviceaccountController, controller, principalController, checkController) gitHandler := router.ProvideGitHandler(config, provider, repoStore, authenticator, authorizer, gitrpcInterface) webHandler := router.ProvideWebHandler(config) routerRouter := router.ProvideRouter(config, apiHandler, gitHandler, webHandler) diff --git a/internal/api/controller/execution/controller.go b/internal/api/controller/execution/controller.go new file mode 100644 index 000000000..0fcdb96c8 --- /dev/null +++ b/internal/api/controller/execution/controller.go @@ -0,0 +1,31 @@ +package execution + +import ( + "github.com/harness/gitness/internal/auth/authz" + "github.com/harness/gitness/internal/store" + "github.com/jmoiron/sqlx" +) + +type Controller struct { + db *sqlx.DB + authorizer authz.Authorizer + executionStore store.ExecutionStore + repoStore store.RepoStore + spaceStore store.SpaceStore +} + +func NewController( + db *sqlx.DB, + authorizer authz.Authorizer, + executionStore store.ExecutionStore, + repoStore store.RepoStore, + spaceStore store.SpaceStore, +) *Controller { + return &Controller{ + db: db, + authorizer: authorizer, + executionStore: executionStore, + repoStore: repoStore, + spaceStore: spaceStore, + } +} diff --git a/internal/api/controller/execution/wire.go b/internal/api/controller/execution/wire.go new file mode 100644 index 000000000..919579f5e --- /dev/null +++ b/internal/api/controller/execution/wire.go @@ -0,0 +1,24 @@ +package execution + +import ( + "github.com/google/wire" + "github.com/harness/gitness/internal/auth/authz" + "github.com/harness/gitness/internal/store" + "github.com/jmoiron/sqlx" +) + +// WireSet provides a wire set for this package. +var WireSet = wire.NewSet( + ProvideController, +) + +func ProvideController(db *sqlx.DB, + authorizer authz.Authorizer, + executionStore store.ExecutionStore, + repoStore store.RepoStore, + spaceStore store.SpaceStore, +) *Controller { + return NewController(db, authorizer, executionStore, + repoStore, + spaceStore) +} diff --git a/internal/api/controller/pipeline/controller.go b/internal/api/controller/pipeline/controller.go new file mode 100644 index 000000000..54bb1c3bd --- /dev/null +++ b/internal/api/controller/pipeline/controller.go @@ -0,0 +1,39 @@ +package pipeline + +import ( + "github.com/harness/gitness/internal/auth/authz" + "github.com/harness/gitness/internal/store" + "github.com/harness/gitness/types/check" + "github.com/jmoiron/sqlx" +) + +type Controller struct { + defaultBranch string + db *sqlx.DB + uidCheck check.PathUID + pathStore store.PathStore + repoStore store.RepoStore + authorizer authz.Authorizer + pipelineStore store.PipelineStore + spaceStore store.SpaceStore +} + +func NewController( + db *sqlx.DB, + uidCheck check.PathUID, + authorizer authz.Authorizer, + pathStore store.PathStore, + repoStore store.RepoStore, + pipelineStore store.PipelineStore, + spaceStore store.SpaceStore, +) *Controller { + return &Controller{ + db: db, + uidCheck: uidCheck, + pathStore: pathStore, + repoStore: repoStore, + authorizer: authorizer, + pipelineStore: pipelineStore, + spaceStore: spaceStore, + } +} diff --git a/internal/api/controller/pipeline/create.go b/internal/api/controller/pipeline/create.go new file mode 100644 index 000000000..f2f2f00e3 --- /dev/null +++ b/internal/api/controller/pipeline/create.go @@ -0,0 +1,113 @@ +package pipeline + +import ( + "context" + "fmt" + "strconv" + "strings" + "time" + + "github.com/harness/gitness/internal/api/usererror" + "github.com/harness/gitness/internal/auth" + "github.com/harness/gitness/store/database/dbtx" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/check" + "github.com/harness/gitness/types/enum" +) + +var ( + // errRepositoryRequiresParent if the user tries to create a repo without a parent space. + errPipelineRequiresParent = usererror.BadRequest( + "Parent space required - standalone pipelines are not supported.") +) + +type CreateInput struct { + Description string `json:"description"` + ParentRef string `json:"parent_ref"` // Ref of the parent space + UID string `json:"uid"` + RepoRef string `json:"repo_ref"` // null if repo_type != gitness + RepoType enum.ScmType `json:"repo_type"` + DefaultBranch string `json:"default_branch"` + ConfigPath string `json:"config_path"` +} + +// Create creates a new pipeline +func (c *Controller) Create(ctx context.Context, session *auth.Session, in *CreateInput) (*types.Pipeline, error) { + // TODO: Add auth + // parentSpace, err := c.getSpaceCheckAuthRepoCreation(ctx, session, in.ParentRef) + // if err != nil { + // return nil, err + // } + + parentSpace, err := c.spaceStore.FindByRef(ctx, in.ParentRef) + if err != nil { + return nil, fmt.Errorf("could not find parent by ref: %w", err) + } + var repoID int64 + + if in.RepoType == enum.ScmTypeGitness { + repo, err := c.repoStore.FindByRef(ctx, in.RepoRef) + if err != nil { + return nil, fmt.Errorf("could not find repo by ref: %w", err) + } + repoID = repo.ID + } + + if err := c.sanitizeCreateInput(in); err != nil { + return nil, fmt.Errorf("failed to sanitize input: %w", err) + } + + var pipeline *types.Pipeline + err = dbtx.New(c.db).WithTx(ctx, func(ctx context.Context) error { + // lock parent space path to ensure it doesn't get updated while we setup new pipeline + _, err := c.pathStore.FindPrimaryWithLock(ctx, enum.PathTargetTypeSpace, parentSpace.ID) + if err != nil { + return usererror.BadRequest("Parent not found") + } + + now := time.Now().UnixMilli() + pipeline = &types.Pipeline{ + Description: in.Description, + ParentID: parentSpace.ID, + UID: in.UID, + Seq: 0, + RepoID: repoID, + RepoType: in.RepoType, + DefaultBranch: in.DefaultBranch, + ConfigPath: in.ConfigPath, + Created: now, + Updated: now, + Version: 0, + } + err = c.pipelineStore.Create(ctx, pipeline) + if err != nil { + return fmt.Errorf("pipeline creation failed: %w", err) + } + return nil + }) + + return pipeline, nil +} + +func (c *Controller) sanitizeCreateInput(in *CreateInput) error { + parentRefAsID, err := strconv.ParseInt(in.ParentRef, 10, 64) + + if (err == nil && parentRefAsID <= 0) || (len(strings.TrimSpace(in.ParentRef)) == 0) { + return errPipelineRequiresParent + } + + if err := c.uidCheck(in.UID, false); err != nil { + return err + } + + in.Description = strings.TrimSpace(in.Description) + if err := check.Description(in.Description); err != nil { + return err + } + + if in.DefaultBranch == "" { + in.DefaultBranch = c.defaultBranch + } + + return nil +} diff --git a/internal/api/controller/pipeline/delete.go b/internal/api/controller/pipeline/delete.go new file mode 100644 index 000000000..4dc12c239 --- /dev/null +++ b/internal/api/controller/pipeline/delete.go @@ -0,0 +1,34 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package pipeline + +import ( + "context" + "fmt" + + "github.com/harness/gitness/internal/auth" +) + +// Delete deletes a pipeline. +func (c *Controller) Delete(ctx context.Context, session *auth.Session, spaceRef string, uid string) error { + space, err := c.spaceStore.FindByRef(ctx, spaceRef) + if err != nil { + return err + } + // TODO: Add auth + // if err = apiauth.CheckSpace(ctx, c.authorizer, session, space, enum.PermissionSpaceDelete, false); err != nil { + // return err + // } + // TODO: uncomment when soft delete is implemented + pipeline, err := c.pipelineStore.FindByUID(ctx, space.ID, uid) + if err != nil { + return err + } + err = c.pipelineStore.Delete(ctx, pipeline.ID) + if err != nil { + return fmt.Errorf("could not delete pipeline: %w", err) + } + return nil +} diff --git a/internal/api/controller/pipeline/find.go b/internal/api/controller/pipeline/find.go new file mode 100644 index 000000000..c880dbb8e --- /dev/null +++ b/internal/api/controller/pipeline/find.go @@ -0,0 +1,26 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package pipeline + +import ( + "context" + + "github.com/harness/gitness/internal/auth" + "github.com/harness/gitness/types" +) + +// Find finds a repo. +func (c *Controller) Find(ctx context.Context, session *auth.Session, spaceRef string, uid string) (*types.Pipeline, error) { + space, err := c.spaceStore.FindByRef(ctx, spaceRef) + if err != nil { + return nil, err + } + // TODO: Add auth + // if err = apiauth.CheckSpace(ctx, c.authorizer, session, space, enum.PermissionSpaceDelete, false); err != nil { + // return err + // } + // TODO: uncomment when soft delete is implemented + return c.pipelineStore.FindByUID(ctx, space.ID, uid) +} diff --git a/internal/api/controller/pipeline/update.go b/internal/api/controller/pipeline/update.go new file mode 100644 index 000000000..4430ec26a --- /dev/null +++ b/internal/api/controller/pipeline/update.go @@ -0,0 +1,50 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package pipeline + +import ( + "context" + + "github.com/harness/gitness/internal/auth" + "github.com/harness/gitness/types" +) + +// UpdateInput is used for updating a repo. +type UpdateInput struct { + Description string `json:"description"` + UID string `json:"uid"` + ConfigPath string `json:"config_path"` +} + +// Update updates a repository. +func (c *Controller) Update(ctx context.Context, session *auth.Session, + spaceRef string, uid string, in *UpdateInput) (*types.Pipeline, error) { + space, err := c.spaceStore.FindByRef(ctx, spaceRef) + if err != nil { + return nil, err + } + + pipeline, err := c.pipelineStore.FindByUID(ctx, space.ID, uid) + if err != nil { + return nil, err + } + + if in.Description != "" { + pipeline.Description = in.Description + } + if in.UID != "" { + pipeline.UID = in.UID + } + if in.ConfigPath != "" { + pipeline.ConfigPath = in.ConfigPath + } + + // TODO: Add auth + // if err = apiauth.CheckRepo(ctx, c.authorizer, session, repo, enum.PermissionRepoEdit, false); err != nil { + // return nil, err + // } + + return c.pipelineStore.Update(ctx, pipeline) +} diff --git a/internal/api/controller/pipeline/wire.go b/internal/api/controller/pipeline/wire.go new file mode 100644 index 000000000..b66ce5dcd --- /dev/null +++ b/internal/api/controller/pipeline/wire.go @@ -0,0 +1,25 @@ +package pipeline + +import ( + "github.com/google/wire" + "github.com/harness/gitness/internal/auth/authz" + "github.com/harness/gitness/internal/store" + "github.com/harness/gitness/types/check" + "github.com/jmoiron/sqlx" +) + +// WireSet provides a wire set for this package. +var WireSet = wire.NewSet( + ProvideController, +) + +func ProvideController(db *sqlx.DB, + uidCheck check.PathUID, + pathStore store.PathStore, + repoStore store.RepoStore, + authorizer authz.Authorizer, + pipelineStore store.PipelineStore, + spaceStore store.SpaceStore, +) *Controller { + return NewController(db, uidCheck, authorizer, pathStore, repoStore, pipelineStore, spaceStore) +} diff --git a/internal/api/handler/pipeline/create.go b/internal/api/handler/pipeline/create.go new file mode 100644 index 000000000..161ade494 --- /dev/null +++ b/internal/api/handler/pipeline/create.go @@ -0,0 +1,33 @@ +package pipeline + +import ( + "encoding/json" + "net/http" + + "github.com/harness/gitness/internal/api/controller/pipeline" + "github.com/harness/gitness/internal/api/render" + "github.com/harness/gitness/internal/api/request" +) + +// HandleCreate returns a http.HandlerFunc that creates a new pipelinesitory. +func HandleCreate(pipelineCtrl *pipeline.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + + in := new(pipeline.CreateInput) + err := json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequestf(w, "Invalid Request Body: %s.", err) + return + } + + pipeline, err := pipelineCtrl.Create(ctx, session, in) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + render.JSON(w, http.StatusCreated, pipeline) + } +} diff --git a/internal/api/handler/pipeline/delete.go b/internal/api/handler/pipeline/delete.go new file mode 100644 index 000000000..02673b3d0 --- /dev/null +++ b/internal/api/handler/pipeline/delete.go @@ -0,0 +1,40 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package pipeline + +import ( + "net/http" + + "github.com/harness/gitness/internal/api/controller/pipeline" + "github.com/harness/gitness/internal/api/render" + "github.com/harness/gitness/internal/api/request" +) + +/* + * Deletes a pipeline. + */ +func HandleDelete(pipelineCtrl *pipeline.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + pipelineRef, err := request.GetPipelinePathRefFromPath(r) + if err != nil { + render.TranslatedUserError(w, err) + return + } + spaceRef, pipelineUID, err := SplitRef(pipelineRef) + if err != nil { + render.TranslatedUserError(w, err) + } + + err = pipelineCtrl.Delete(ctx, session, spaceRef, pipelineUID) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + render.DeleteSuccessful(w) + } +} diff --git a/internal/api/handler/pipeline/find.go b/internal/api/handler/pipeline/find.go new file mode 100644 index 000000000..aeb69e02b --- /dev/null +++ b/internal/api/handler/pipeline/find.go @@ -0,0 +1,49 @@ +package pipeline + +import ( + "errors" + "net/http" + "strings" + + "github.com/harness/gitness/internal/api/controller/pipeline" + "github.com/harness/gitness/internal/api/render" + "github.com/harness/gitness/internal/api/request" +) + +// HandleFind writes json-encoded repository information to the http response body. +func HandleFind(pipelineCtrl *pipeline.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + pipelineRef, err := request.GetPipelinePathRefFromPath(r) + if err != nil { + render.TranslatedUserError(w, err) + return + } + spaceRef, pipelineUID, err := SplitRef(pipelineRef) + if err != nil { + render.TranslatedUserError(w, err) + } + + pipeline, err := pipelineCtrl.Find(ctx, session, spaceRef, pipelineUID) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + render.JSON(w, http.StatusOK, pipeline) + } +} + +func SplitRef(ref string) (string, string, error) { + lastIndex := strings.LastIndex(ref, "/") + if lastIndex == -1 { + // The input string does not contain a "/". + return "", "", errors.New("could not split ref") + } + + spaceRef := ref[:lastIndex] + uid := ref[lastIndex+1:] + + return spaceRef, uid, nil +} diff --git a/internal/api/handler/pipeline/update.go b/internal/api/handler/pipeline/update.go new file mode 100644 index 000000000..80842a335 --- /dev/null +++ b/internal/api/handler/pipeline/update.go @@ -0,0 +1,45 @@ +package pipeline + +import ( + "encoding/json" + "net/http" + + "github.com/harness/gitness/internal/api/controller/pipeline" + "github.com/harness/gitness/internal/api/render" + "github.com/harness/gitness/internal/api/request" +) + +/* + * Updates an existing pipeline. + */ +func HandleUpdate(pipelineCtrl *pipeline.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + + in := new(pipeline.UpdateInput) + err := json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequestf(w, "Invalid Request Body: %s.", err) + return + } + + pipelineRef, err := request.GetPipelinePathRefFromPath(r) + if err != nil { + render.TranslatedUserError(w, err) + return + } + spaceRef, pipelineUID, err := SplitRef(pipelineRef) + if err != nil { + render.TranslatedUserError(w, err) + } + + pipeline, err := pipelineCtrl.Update(ctx, session, spaceRef, pipelineUID, in) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + render.JSON(w, http.StatusOK, pipeline) + } +} diff --git a/internal/api/openapi/openapi.go b/internal/api/openapi/openapi.go index 2e55f2e1d..5110b9377 100644 --- a/internal/api/openapi/openapi.go +++ b/internal/api/openapi/openapi.go @@ -41,6 +41,7 @@ func Generate() *openapi3.Spec { buildPrincipals(&reflector) spaceOperations(&reflector) repoOperations(&reflector) + pipelineOperations(&reflector) resourceOperations(&reflector) pullReqOperations(&reflector) webhookOperations(&reflector) diff --git a/internal/api/openapi/pipeline.go b/internal/api/openapi/pipeline.go new file mode 100644 index 000000000..61b7c9fb4 --- /dev/null +++ b/internal/api/openapi/pipeline.go @@ -0,0 +1,81 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this pipelinesitory. + +package openapi + +import ( + "net/http" + + "github.com/harness/gitness/internal/api/controller/pipeline" + "github.com/harness/gitness/internal/api/usererror" + "github.com/harness/gitness/types" + + "github.com/swaggest/openapi-go/openapi3" +) + +type createPipelineRequest struct { + pipeline.CreateInput +} + +type pipelineRequest struct { + Ref string `path:"pipeline_ref"` +} + +type updatePipelineRequest struct { + pipelineRequest + pipeline.UpdateInput +} + +type scmType string + +type pipelineGetResponse struct { + types.Pipeline +} + +func pipelineOperations(reflector *openapi3.Reflector) { + opCreate := openapi3.Operation{} + opCreate.WithTags("pipeline") + opCreate.WithMapOfAnything(map[string]interface{}{"operationId": "createPipeline"}) + _ = reflector.SetRequest(&opCreate, new(createPipelineRequest), http.MethodPost) + _ = reflector.SetJSONResponse(&opCreate, new(types.Pipeline), http.StatusCreated) + _ = reflector.SetJSONResponse(&opCreate, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&opCreate, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opCreate, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opCreate, new(usererror.Error), http.StatusForbidden) + _ = reflector.Spec.AddOperation(http.MethodPost, "/pipelines", opCreate) + + opFind := openapi3.Operation{} + opFind.WithTags("pipeline") + opFind.WithMapOfAnything(map[string]interface{}{"operationId": "findPipeline"}) + _ = reflector.SetRequest(&opFind, new(pipelineRequest), http.MethodGet) + _ = reflector.SetJSONResponse(&opFind, new(pipelineGetResponse), http.StatusOK) + _ = reflector.SetJSONResponse(&opFind, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opFind, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opFind, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opFind, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodGet, "/pipelines/{pipeline_ref}", opFind) + + opDelete := openapi3.Operation{} + opDelete.WithTags("pipeline") + opDelete.WithMapOfAnything(map[string]interface{}{"operationId": "deletePipeline"}) + _ = reflector.SetRequest(&opDelete, new(pipelineRequest), http.MethodDelete) + _ = reflector.SetJSONResponse(&opDelete, nil, http.StatusNoContent) + _ = reflector.SetJSONResponse(&opDelete, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opDelete, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opDelete, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opDelete, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodDelete, "/pipelines/{pipeline_ref}", opDelete) + + opUpdate := openapi3.Operation{} + opUpdate.WithTags("pipeline") + opUpdate.WithMapOfAnything(map[string]interface{}{"operationId": "updatePipeline"}) + _ = reflector.SetRequest(&opUpdate, new(updatePipelineRequest), http.MethodPatch) + _ = reflector.SetJSONResponse(&opUpdate, new(types.Pipeline), http.StatusOK) + _ = reflector.SetJSONResponse(&opUpdate, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&opUpdate, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opUpdate, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opUpdate, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opUpdate, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodPatch, "/pipelines/{pipeline_ref}", opUpdate) +} diff --git a/internal/api/request/pipeline.go b/internal/api/request/pipeline.go new file mode 100644 index 000000000..fea17c598 --- /dev/null +++ b/internal/api/request/pipeline.go @@ -0,0 +1,54 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package request + +import ( + "net/http" + "net/url" +) + +const ( + PipelinePathRef = "pipeline_ref" + PipelineUID = "pipeline_uid" +) + +func GetPipelinePathRefFromPath(r *http.Request) (string, error) { + rawRef, err := PathParamOrError(r, PipelinePathRef) + if err != nil { + return "", err + } + + // paths are unescaped + return url.PathUnescape(rawRef) +} + +func GetPipelineUIDFromPath(r *http.Request) (string, error) { + rawRef, err := PathParamOrError(r, PipelineUID) + if err != nil { + return "", err + } + + // paths are unescaped + return url.PathUnescape(rawRef) +} + +// TODO: Add list filters +// // ParseSortRepo extracts the repo sort parameter from the url. +// func ParseSortRepo(r *http.Request) enum.RepoAttr { +// return enum.ParseRepoAtrr( +// r.URL.Query().Get(QueryParamSort), +// ) +// } + +// // ParseRepoFilter extracts the repository filter from the url. +// func ParseRepoFilter(r *http.Request) *types.RepoFilter { +// return &types.RepoFilter{ +// Query: ParseQuery(r), +// Order: ParseOrder(r), +// Page: ParsePage(r), +// Sort: ParseSortRepo(r), +// Size: ParseLimit(r), +// } +// } diff --git a/internal/router/api.go b/internal/router/api.go index 35dc4238d..80546dc8a 100644 --- a/internal/router/api.go +++ b/internal/router/api.go @@ -10,7 +10,9 @@ import ( "github.com/harness/gitness/githook" "github.com/harness/gitness/internal/api/controller/check" + "github.com/harness/gitness/internal/api/controller/execution" controllergithook "github.com/harness/gitness/internal/api/controller/githook" + "github.com/harness/gitness/internal/api/controller/pipeline" "github.com/harness/gitness/internal/api/controller/principal" "github.com/harness/gitness/internal/api/controller/pullreq" "github.com/harness/gitness/internal/api/controller/repo" @@ -21,6 +23,7 @@ import ( "github.com/harness/gitness/internal/api/handler/account" handlercheck "github.com/harness/gitness/internal/api/handler/check" handlergithook "github.com/harness/gitness/internal/api/handler/githook" + handlerpipeline "github.com/harness/gitness/internal/api/handler/pipeline" handlerprincipal "github.com/harness/gitness/internal/api/handler/principal" handlerpullreq "github.com/harness/gitness/internal/api/handler/pullreq" handlerrepo "github.com/harness/gitness/internal/api/handler/repo" @@ -62,7 +65,9 @@ func NewAPIHandler( config *types.Config, authenticator authn.Authenticator, repoCtrl *repo.Controller, + executionCtrl *execution.Controller, spaceCtrl *space.Controller, + pipelineCtrl *pipeline.Controller, pullreqCtrl *pullreq.Controller, webhookCtrl *webhook.Controller, githookCtrl *controllergithook.Controller, @@ -92,7 +97,7 @@ func NewAPIHandler( r.Use(middlewareauthn.Attempt(authenticator, authn.SourceRouterAPI)) r.Route("/v1", func(r chi.Router) { - setupRoutesV1(r, repoCtrl, spaceCtrl, pullreqCtrl, webhookCtrl, githookCtrl, + setupRoutesV1(r, config, repoCtrl, executionCtrl, pipelineCtrl, spaceCtrl, pullreqCtrl, webhookCtrl, githookCtrl, saCtrl, userCtrl, principalCtrl, checkCtrl) }) @@ -114,7 +119,10 @@ func corsHandler(config *types.Config) func(http.Handler) http.Handler { } func setupRoutesV1(r chi.Router, + config *types.Config, repoCtrl *repo.Controller, + executionCtrl *execution.Controller, + pipelineCtrl *pipeline.Controller, spaceCtrl *space.Controller, pullreqCtrl *pullreq.Controller, webhookCtrl *webhook.Controller, @@ -126,6 +134,7 @@ func setupRoutesV1(r chi.Router, ) { setupSpaces(r, spaceCtrl) setupRepos(r, repoCtrl, pullreqCtrl, webhookCtrl, checkCtrl) + setupPipelines(r, pipelineCtrl, executionCtrl) setupUser(r, userCtrl) setupServiceAccounts(r, saCtrl) setupPrincipals(r, principalCtrl) @@ -266,6 +275,20 @@ func setupRepos(r chi.Router, }) } +func setupPipelines(r chi.Router, pipelineCtrl *pipeline.Controller, executionCtrl *execution.Controller) { + r.Route("/pipelines", func(r chi.Router) { + // Create takes path and parentId via body, not uri + r.Post("/", handlerpipeline.HandleCreate(pipelineCtrl)) + r.Route(fmt.Sprintf("/{%s}", request.PipelinePathRef), func(r chi.Router) { + r.Get("/", handlerpipeline.HandleFind(pipelineCtrl)) + r.Patch("/", handlerpipeline.HandleUpdate(pipelineCtrl)) + r.Delete("/", handlerpipeline.HandleDelete(pipelineCtrl)) + // TODO: setup executions here + // SetupExecutions(r, executionCtrl) + }) + }) +} + func setupInternal(r chi.Router, githookCtrl *controllergithook.Controller) { r.Route("/internal", func(r chi.Router) { SetupGitHooks(r, githookCtrl) diff --git a/internal/router/wire.go b/internal/router/wire.go index 3118ca790..bd834b12e 100644 --- a/internal/router/wire.go +++ b/internal/router/wire.go @@ -7,7 +7,9 @@ package router import ( "github.com/harness/gitness/gitrpc" "github.com/harness/gitness/internal/api/controller/check" + "github.com/harness/gitness/internal/api/controller/execution" "github.com/harness/gitness/internal/api/controller/githook" + "github.com/harness/gitness/internal/api/controller/pipeline" "github.com/harness/gitness/internal/api/controller/principal" "github.com/harness/gitness/internal/api/controller/pullreq" "github.com/harness/gitness/internal/api/controller/repo" @@ -57,7 +59,9 @@ func ProvideAPIHandler( config *types.Config, authenticator authn.Authenticator, repoCtrl *repo.Controller, + executionCtrl *execution.Controller, spaceCtrl *space.Controller, + pipelineCtrl *pipeline.Controller, pullreqCtrl *pullreq.Controller, webhookCtrl *webhook.Controller, githookCtrl *githook.Controller, @@ -66,7 +70,7 @@ func ProvideAPIHandler( principalCtrl principal.Controller, checkCtrl *check.Controller, ) APIHandler { - return NewAPIHandler(config, authenticator, repoCtrl, spaceCtrl, pullreqCtrl, + return NewAPIHandler(config, authenticator, repoCtrl, executionCtrl, spaceCtrl, pipelineCtrl, pullreqCtrl, webhookCtrl, githookCtrl, saCtrl, userCtrl, principalCtrl, checkCtrl) } diff --git a/internal/store/database.go b/internal/store/database.go index 5dc84cf30..ab9c7ae0e 100644 --- a/internal/store/database.go +++ b/internal/store/database.go @@ -439,4 +439,85 @@ type ( // Delete removes a required status checks for a repo. Delete(ctx context.Context, repoID, reqCheckID int64) error } + PipelineStore interface { + // Find returns a pipeline given a pipeline ID from the datastore. + Find(context.Context, int64) (*types.Pipeline, error) + + // FindByUID returns a pipeline with a given UID in a space + FindByUID(context.Context, int64, string) (*types.Pipeline, error) + + // Create creates a new pipeline in the datastore. + Create(context.Context, *types.Pipeline) error + + // Update tries to update a pipeline in the datastore with optimistic locking. + Update(context.Context, *types.Pipeline) (*types.Pipeline, error) + + // List lists the pipelines present in a parent space ID in the datastore. + List(context.Context, int64, *types.PipelineFilter) ([]*types.Pipeline, error) + + // Delete deletes a pipeline ID from the datastore. + Delete(context.Context, int64) error + } + + // TODO: Implement the execution store interface + ExecutionStore interface { + // Find returns a build from the datastore. + // Find(context.Context, int64) (*types.Execution, error) + + // FindNumber returns a build from the datastore by build number. + // FindNumber(context.Context, int64, int64) (*types.Execution, error) + + // FindLast returns the last build from the datastore by ref. + // FindRef(context.Context, int64, string) (*types.Execution, error) + + // List returns a list of builds from the datastore by repository id. + // List(context.Context, int64, int, int) ([]*types.Execution, error) + + // ListRef returns a list of builds from the datastore by ref. + // ListRef(context.Context, int64, string, int, int) ([]*types.Execution, error) + + // LatestBranches returns the latest builds from the + // datastore by branch. + // LatestBranches(context.Context, int64) ([]*types.Execution, error) + + // LatestPulls returns the latest builds from the + // datastore by pull request. + // LatestPulls(context.Context, int64) ([]*types.Execution, error) + + // LatestDeploys returns the latest builds from the + // datastore by deployment target. + // LatestDeploys(context.Context, int64) ([]*types.Execution, error) + + // Pending returns a list of pending builds from the + // datastore by repository id (DEPRECATED). + // Pending(context.Context) ([]*types.Execution, error) + + // Running returns a list of running builds from the + // datastore by repository id (DEPRECATED). + // Running(context.Context) ([]*types.Execution, error) + + // Create persists a build to the datastore. + // Create(context.Context, *types.Execution, []*Stage) error + + // Update updates a build in the datastore. + // Update(context.Context, *types.Execution) error + + // // Delete deletes a build from the datastore. + // Delete(context.Context, *types.Execution) error + + // // DeletePull deletes a pull request index from the datastore. + // DeletePull(context.Context, int64, int) error + + // // DeleteBranch deletes a branch index from the datastore. + // DeleteBranch(context.Context, int64, string) error + + // // DeleteDeploy deletes a deploy index from the datastore. + // DeleteDeploy(context.Context, int64, string) error + + // // Purge deletes builds from the database where the build number is less than n. + // Purge(context.Context, int64, int64) error + + // // Count returns a count of builds. + // Count(context.Context) (int64, error) + } ) diff --git a/internal/store/database/execution.go b/internal/store/database/execution.go new file mode 100644 index 000000000..b851fb6c5 --- /dev/null +++ b/internal/store/database/execution.go @@ -0,0 +1,59 @@ +package database + +import ( + "github.com/harness/gitness/internal/store" + "github.com/jmoiron/sqlx" +) + +var _ store.ExecutionStore = (*executionStore)(nil) + +// NewSpaceStore returns a new PathStore. +func NewExecutionStore(db *sqlx.DB) *executionStore { + return &executionStore{ + db: db, + } +} + +type executionStore struct { + db *sqlx.DB +} + +const ( + executionColumns = ` + execution_id + ,execution_scm_type + ,execution_repo_id + ,execution_trigger + ,execution_number + ,execution_parent + ,execution_status + ,execution_error + ,execution_event + ,execution_action + ,execution_link + ,execution_timestamp + ,execution_title + ,execution_message + ,execution_before + ,execution_after + ,execution_ref + ,execution_source_repo + ,execution_source + ,execution_target + ,execution_author + ,execution_author_name + ,execution_author_email + ,execution_author_avatar + ,execution_sender + ,execution_params + ,execution_cron + ,execution_deploy + ,execution_deploy_id + ,execution_debug + ,execution_started + ,execution_finished + ,execution_created + ,execution_updated + ,execution_version + ` +) diff --git a/internal/store/database/migrate/sqlite/0020_create_table_builds.up.sql b/internal/store/database/migrate/sqlite/0020_create_table_builds.up.sql new file mode 100644 index 000000000..52bb5c590 --- /dev/null +++ b/internal/store/database/migrate/sqlite/0020_create_table_builds.up.sql @@ -0,0 +1,48 @@ +CREATE TABLE IF NOT EXISTS executions ( + execution_id INTEGER PRIMARY KEY AUTOINCREMENT, + execution_pipeline_id INTEGER NOT NULL, + execution_repo_id INTEGER, + execution_repo_type TEXT, + execution_repo_name TEXT, + execution_trigger TEXT, + execution_number INTEGER NOT NULL, + execution_parent INTEGER, + execution_status TEXT, + execution_error TEXT, + execution_event TEXT, + execution_action TEXT, + execution_link TEXT, + execution_timestamp INTEGER, + execution_title TEXT, + execution_message TEXT, + execution_before TEXT, + execution_after TEXT, + execution_ref TEXT, + execution_source_repo TEXT, + execution_source TEXT, + execution_target TEXT, + execution_author TEXT, + execution_author_name TEXT, + execution_author_email TEXT, + execution_author_avatar TEXT, + execution_sender TEXT, + execution_params TEXT, + execution_cron TEXT, + execution_deploy TEXT, + execution_deploy_id INTEGER, + execution_debug BOOLEAN NOT NULL DEFAULT 0, + execution_started INTEGER, + execution_finished INTEGER, + execution_created INTEGER, + execution_updated INTEGER, + execution_version INTEGER, + + -- Ensure unique combination of pipeline ID and number + UNIQUE (execution_pipeline_id, execution_number), + + -- Foreign key to pipelines table + CONSTRAINT fk_execution_pipeline_id FOREIGN KEY (execution_pipeline_id) + REFERENCES pipelines (pipeline_id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE CASCADE +); \ No newline at end of file diff --git a/internal/store/database/migrate/sqlite/0020_create_table_pipelines.down.sql b/internal/store/database/migrate/sqlite/0020_create_table_pipelines.down.sql new file mode 100644 index 000000000..acbdf9bdb --- /dev/null +++ b/internal/store/database/migrate/sqlite/0020_create_table_pipelines.down.sql @@ -0,0 +1 @@ +DROP TABLE pipelines; \ No newline at end of file diff --git a/internal/store/database/migrate/sqlite/0020_create_table_pipelines.up.sql b/internal/store/database/migrate/sqlite/0020_create_table_pipelines.up.sql new file mode 100644 index 000000000..d5324e18a --- /dev/null +++ b/internal/store/database/migrate/sqlite/0020_create_table_pipelines.up.sql @@ -0,0 +1,30 @@ +CREATE TABLE IF NOT EXISTS pipelines ( + pipeline_id INTEGER PRIMARY KEY AUTOINCREMENT, + pipeline_description TEXT, + pipeline_parent_id INTEGER NOT NULL, + pipeline_uid TEXT NOT NULL, + pipeline_seq INTEGER NOT NULL DEFAULT 0, + pipeline_repo_id INTEGER, + pipeline_repo_type TEXT NOT NULL, + pipeline_repo_name TEXT, + pipeline_default_branch TEXT, + pipeline_config_path TEXT, + pipeline_created INTEGER, + pipeline_updated INTEGER, + pipeline_version INTEGER, + + -- Ensure unique combination of UID and ParentID + UNIQUE (pipeline_parent_id, pipeline_uid), + + -- Foreign key to spaces table + CONSTRAINT fk_pipeline_parent_id FOREIGN KEY (pipeline_parent_id) + REFERENCES spaces (space_id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE CASCADE + + -- Foreign key to repositories table + CONSTRAINT fk_pipeline_repo_id FOREIGN KEY (pipeline_repo_id) + REFERENCES repositories (repo_id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE CASCADE +); \ No newline at end of file diff --git a/internal/store/database/migrate/sqlite/0021_create_table_builds.down.sql b/internal/store/database/migrate/sqlite/0021_create_table_builds.down.sql new file mode 100644 index 000000000..cc476de4a --- /dev/null +++ b/internal/store/database/migrate/sqlite/0021_create_table_builds.down.sql @@ -0,0 +1 @@ +DROP TABLE executions; \ No newline at end of file diff --git a/internal/store/database/migrate/sqlite/0021_create_table_executions.up.sql b/internal/store/database/migrate/sqlite/0021_create_table_executions.up.sql new file mode 100644 index 000000000..466b193d3 --- /dev/null +++ b/internal/store/database/migrate/sqlite/0021_create_table_executions.up.sql @@ -0,0 +1,46 @@ +CREATE TABLE IF NOT EXISTS executions ( + execution_id INTEGER PRIMARY KEY AUTOINCREMENT, + execution_pipeline_id INTEGER NOT NULL, + execution_repo_id INTEGER, + execution_trigger TEXT, + execution_number INTEGER NOT NULL, + execution_parent INTEGER, + execution_status TEXT, + execution_error TEXT, + execution_event TEXT, + execution_action TEXT, + execution_link TEXT, + execution_timestamp INTEGER, + execution_title TEXT, + execution_message TEXT, + execution_before TEXT, + execution_after TEXT, + execution_ref TEXT, + execution_source_repo TEXT, + execution_source TEXT, + execution_target TEXT, + execution_author TEXT, + execution_author_name TEXT, + execution_author_email TEXT, + execution_author_avatar TEXT, + execution_sender TEXT, + execution_params TEXT, + execution_cron TEXT, + execution_deploy TEXT, + execution_deploy_id INTEGER, + execution_debug BOOLEAN NOT NULL DEFAULT 0, + execution_started INTEGER, + execution_finished INTEGER, + execution_created INTEGER, + execution_updated INTEGER, + execution_version INTEGER, + + -- Ensure unique combination of pipeline ID and number + UNIQUE (execution_pipeline_id, execution_number), + + -- Foreign key to pipelines table + CONSTRAINT fk_execution_pipeline_id FOREIGN KEY (execution_pipeline_id) + REFERENCES pipelines (pipeline_id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE CASCADE +); \ No newline at end of file diff --git a/internal/store/database/pipeline.go b/internal/store/database/pipeline.go new file mode 100644 index 000000000..c6a942b2d --- /dev/null +++ b/internal/store/database/pipeline.go @@ -0,0 +1,228 @@ +package database + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/harness/gitness/internal/store" + gitness_store "github.com/harness/gitness/store" + "github.com/harness/gitness/store/database" + "github.com/harness/gitness/store/database/dbtx" + "github.com/harness/gitness/types" + "github.com/jmoiron/sqlx" + "github.com/pkg/errors" +) + +var _ store.PipelineStore = (*pipelineStore)(nil) + +const ( + pipelineQueryBase = ` + SELECT + pipeline_id, + pipeline_description, + pipeline_parent_id, + pipeline_uid, + pipeline_seq, + pipeline_repo_id, + pipeline_repo_type, + pipeline_repo_name, + pipeline_default_branch, + pipeline_config_path, + pipeline_created, + pipeline_updated, + pipeline_version + FROM pipelines + ` + + pipelineColumns = ` + pipeline_id, + pipeline_description, + pipeline_parent_id, + pipeline_uid, + pipeline_seq, + pipeline_repo_id, + pipeline_repo_type, + pipeline_repo_name, + pipeline_default_branch, + pipeline_config_path, + pipeline_created, + pipeline_updated, + pipeline_version + ` + + pipelineInsertStmt = ` + INSERT INTO pipelines ( + pipeline_description, + pipeline_parent_id, + pipeline_uid, + pipeline_seq, + pipeline_repo_id, + pipeline_repo_type, + pipeline_repo_name, + pipeline_default_branch, + pipeline_config_path, + pipeline_created, + pipeline_updated, + pipeline_version + ) VALUES ( + :pipeline_description, + :pipeline_parent_id, + :pipeline_uid, + :pipeline_seq, + :pipeline_repo_id, + :pipeline_repo_type, + :pipeline_repo_name, + :pipeline_default_branch, + :pipeline_config_path, + :pipeline_created, + :pipeline_updated, + :pipeline_version + ) RETURNING pipeline_id` + + pipelineUpdateStmt = ` + UPDATE pipelines + SET + pipeline_description = :pipeline_description, + pipeline_parent_id = :pipeline_parent_id, + pipeline_uid = :pipeline_uid, + pipeline_seq = :pipeline_seq, + pipeline_repo_id = :pipeline_repo_id, + pipeline_repo_type = :pipeline_repo_type, + pipeline_repo_name = :pipeline_repo_name, + pipeline_default_branch = :pipeline_default_branch, + pipeline_config_path = :pipeline_config_path, + pipeline_created = :pipeline_created, + pipeline_updated = :pipeline_updated, + pipeline_version = :pipeline_version + WHERE pipeline_id = :pipeline_id AND pipeline_version = :pipeline_version - 1` +) + +// NewPipelineStore returns a new PipelineStore. +func NewPipelineStore(db *sqlx.DB) *pipelineStore { + return &pipelineStore{ + db: db, + } +} + +type pipelineStore struct { + db *sqlx.DB +} + +// Find returns a pipeline given a pipeline ID +func (s *pipelineStore) Find(ctx context.Context, id int64) (*types.Pipeline, error) { + const findQueryStmt = pipelineQueryBase + ` + WHERE pipeline_id = $1` + db := dbtx.GetAccessor(ctx, s.db) + + dst := new(types.Pipeline) + if err := db.GetContext(ctx, dst, findQueryStmt, id); err != nil { + return nil, database.ProcessSQLErrorf(err, "Failed to find pipeline") + } + return dst, nil +} + +// FindByUID returns a pipeline in a given space with a given UID +func (s *pipelineStore) FindByUID(ctx context.Context, spaceID int64, uid string) (*types.Pipeline, error) { + const findQueryStmt = pipelineQueryBase + ` + WHERE pipeline_parent_id = $1 AND pipeline_uid = $2` + db := dbtx.GetAccessor(ctx, s.db) + + dst := new(types.Pipeline) + if err := db.GetContext(ctx, dst, findQueryStmt, spaceID, uid); err != nil { + return nil, database.ProcessSQLErrorf(err, "Failed to find pipeline") + } + return dst, nil +} + +// Create creates a pipeline +func (s *pipelineStore) Create(ctx context.Context, pipeline *types.Pipeline) error { + db := dbtx.GetAccessor(ctx, s.db) + + query, arg, err := db.BindNamed(pipelineInsertStmt, pipeline) + if err != nil { + return database.ProcessSQLErrorf(err, "Failed to bind pipeline object") + } + + if err = db.QueryRowContext(ctx, query, arg...).Scan(&pipeline.ID); err != nil { + return database.ProcessSQLErrorf(err, "Pipeline query failed") + } + + return nil +} + +func (s *pipelineStore) Update(ctx context.Context, pipeline *types.Pipeline) (*types.Pipeline, error) { + updatedAt := time.Now() + + pipeline.Version++ + pipeline.Updated = updatedAt.UnixMilli() + + db := dbtx.GetAccessor(ctx, s.db) + + query, arg, err := db.BindNamed(pipelineUpdateStmt, pipeline) + if err != nil { + return nil, database.ProcessSQLErrorf(err, "Failed to bind pipeline object") + } + + result, err := db.ExecContext(ctx, query, arg...) + if err != nil { + return nil, database.ProcessSQLErrorf(err, "Failed to update pipeline") + } + + count, err := result.RowsAffected() + if err != nil { + return nil, database.ProcessSQLErrorf(err, "Failed to get number of updated rows") + } + + if count == 0 { + return nil, gitness_store.ErrVersionConflict + } + + return s.Find(ctx, pipeline.ID) + +} + +// List lists all the pipelines present in a space +func (s *pipelineStore) List(ctx context.Context, parentID int64, opts *types.PipelineFilter) ([]*types.Pipeline, error) { + stmt := database.Builder. + Select(pipelineColumns). + From("pipelines"). + Where("pipeline_parent_id = ?", fmt.Sprint(parentID)) + + if opts.Query != "" { + stmt = stmt.Where("LOWER(pipeline_uid) LIKE ?", fmt.Sprintf("%%%s%%", strings.ToLower(opts.Query))) + } + + stmt = stmt.Limit(database.Limit(opts.Size)) + stmt = stmt.Offset(database.Offset(opts.Page, opts.Size)) + + sql, args, err := stmt.ToSql() + if err != nil { + return nil, errors.Wrap(err, "Failed to convert query to sql") + } + + db := dbtx.GetAccessor(ctx, s.db) + + dst := []*types.Pipeline{} + if err = db.SelectContext(ctx, &dst, sql, args...); err != nil { + return nil, database.ProcessSQLErrorf(err, "Failed executing custom list query") + } + + return dst, nil +} + +// Delete deletes a pipeline given a pipeline ID +func (s *pipelineStore) Delete(ctx context.Context, id int64) error { + const pipelineDeleteStmt = ` + DELETE FROM pipelines + WHERE pipeline_id = $1` + + db := dbtx.GetAccessor(ctx, s.db) + + if _, err := db.ExecContext(ctx, pipelineDeleteStmt, id); err != nil { + return database.ProcessSQLErrorf(err, "Could not delete pipeline") + } + + return nil +} diff --git a/internal/store/database/wire.go b/internal/store/database/wire.go index ff6a66870..6863ddbea 100644 --- a/internal/store/database/wire.go +++ b/internal/store/database/wire.go @@ -23,6 +23,8 @@ var WireSet = wire.NewSet( ProvidePathStore, ProvideSpaceStore, ProvideRepoStore, + ProvideExecutionStore, + ProvidePipelineStore, ProvideRepoGitInfoView, ProvideMembershipStore, ProvideTokenStore, @@ -78,6 +80,16 @@ func ProvideRepoStore(db *sqlx.DB, pathCache store.PathCache) store.RepoStore { return NewRepoStore(db, pathCache) } +// ProvidePipelineStore provides a pipeline store. +func ProvidePipelineStore(db *sqlx.DB) store.PipelineStore { + return NewPipelineStore(db) +} + +// ProvideExecutionStore provides a build store +func ProvideExecutionStore(db *sqlx.DB) store.ExecutionStore { + return NewExecutionStore(db) +} + // ProvideRepoGitInfoView provides a repo git UID view. func ProvideRepoGitInfoView(db *sqlx.DB) store.RepoGitInfoView { return NewRepoGitInfoView(db) diff --git a/types/enum/scm.go b/types/enum/scm.go new file mode 100644 index 000000000..2b28b8978 --- /dev/null +++ b/types/enum/scm.go @@ -0,0 +1,20 @@ +package enum + +// ScmType defines the different types of principal types at harness. +type ScmType string + +func (ScmType) Enum() []interface{} { return toInterfaceSlice(scmTypes) } + +var scmTypes = ([]ScmType{ + ScmTypeGitness, + ScmTypeGithub, + ScmTypeGitlab, + ScmTypeUnknown, +}) + +const ( + ScmTypeUnknown ScmType = "UNKNOWN" + ScmTypeGitness ScmType = "GITNESS" + ScmTypeGithub ScmType = "GITHUB" + ScmTypeGitlab ScmType = "GITLAB" +) diff --git a/types/execution.go b/types/execution.go new file mode 100644 index 000000000..c9b4aee95 --- /dev/null +++ b/types/execution.go @@ -0,0 +1,42 @@ +package types + +// Execution represents an instance of a pipeline execution +type Execution struct { + ID int64 `db:"execution_id" json:"id"` + PipelineID int64 `db:"execution_pipeline_id" json:"pipeline_id"` + RepoID int64 `db:"execution_repo_id" json:"repo_id"` + Trigger string `db:"execution_trigger" json:"trigger"` + Number int64 `db:"execution_number" json:"number"` + Parent int64 `db:"execution_parent" json:"parent,omitempty"` + Status string `db:"execution_status" json:"status"` + Error string `db:"execution_error" json:"error,omitempty"` + Event string `db:"execution_event" json:"event"` + Action string `db:"execution_action" json:"action"` + Link string `db:"execution_link" json:"link"` + Timestamp int64 `db:"execution_timestamp" json:"timestamp"` + Title string `db:"execution_title" json:"title,omitempty"` + Message string `db:"execution_message" json:"message"` + Before string `db:"execution_before" json:"before"` + After string `db:"execution_after" json:"after"` + Ref string `db:"execution_ref" json:"ref"` + Fork string `db:"execution_source_repo" json:"source_repo"` + Source string `db:"execution_source" json:"source"` + Target string `db:"execution_target" json:"target"` + Author string `db:"execution_author" json:"author_login"` + AuthorName string `db:"execution_author_name" json:"author_name"` + AuthorEmail string `db:"execution_author_email" json:"author_email"` + AuthorAvatar string `db:"execution_author_avatar" json:"author_avatar"` + Sender string `db:"execution_sender" json:"sender"` + Params map[string]string `db:"execution_params" json:"params,omitempty"` + Cron string `db:"execution_cron" json:"cron,omitempty"` + Deploy string `db:"execution_deploy" json:"deploy_to,omitempty"` + DeployID int64 `db:"execution_deploy_id" json:"deploy_id,omitempty"` + Debug bool `db:"execution_debug" json:"debug,omitempty"` + Started int64 `db:"execution_started" json:"started"` + Finished int64 `db:"execution_finished" json:"finished"` + Created int64 `db:"execution_created" json:"created"` + Updated int64 `db:"execution_updated" json:"updated"` + Version int64 `db:"execution_version" json:"version"` + // TODO: (Vistaar) Add stages + // Stages []*Stage `db:"-" json:"stages,omitempty"` +} diff --git a/types/pipeline.go b/types/pipeline.go new file mode 100644 index 000000000..56ea321bb --- /dev/null +++ b/types/pipeline.go @@ -0,0 +1,27 @@ +package types + +import "github.com/harness/gitness/types/enum" + +type Pipeline struct { + ID int64 `db:"pipeline_id" json:"id"` + Description string `db:"pipeline_description" json:"description"` + ParentID int64 `db:"pipeline_parent_id" json:"parent_id"` // ID of the parent space + UID string `db:"pipeline_uid" json:"uid"` + Seq int64 `db:"pipeline_seq" json:"seq"` // last execution number for this pipeline + RepoID int64 `db:"pipeline_repo_id" json:"repo_id"` // null if repo_type != gitness + RepoType enum.ScmType `db:"pipeline_repo_type" json:"repo_type"` + RepoName string `db:"pipeline_repo_name" json:"repo_name"` + DefaultBranch string `db:"pipeline_default_branch" json:"default_branch"` + ConfigPath string `db:"pipeline_config_path" json:"config_path"` + Created int64 `db:"pipeline_created" json:"created"` + Updated int64 `db:"pipeline_updated" json:"updated"` + Version int64 `db:"pipeline_version" json:"version"` +} + +// RepoFilter stores repo query parameters. +type PipelineFilter struct { + Page int `json:"page"` + Size int `json:"size"` + Query string `json:"query"` + Order enum.Order `json:"order"` +}