From 9ee9eb252852c042c196c98ecf4bb11ce79c0b0f Mon Sep 17 00:00:00 2001 From: ecrupper Date: Mon, 2 Oct 2023 14:06:29 -0500 Subject: [PATCH 1/9] init commit --- api/build/approve.go | 129 +++++++++++++++++++++++++++++++++ api/build/create.go | 24 +++++- api/build/executable.go | 37 ++++++++++ api/build/get_id.go | 2 +- api/build/publish.go | 44 +---------- api/build/restart.go | 32 +++++++- api/repo/create.go | 8 ++ api/repo/update.go | 5 ++ api/scm/sync.go | 2 +- api/webhook/post.go | 77 +++++++++++++++++++- cmd/vela-server/schedule.go | 25 ++++++- database/repo/table.go | 2 + go.mod | 2 + go.sum | 2 - router/build.go | 2 + router/middleware/perm/perm.go | 14 ++-- scm/github/access.go | 14 ++-- scm/github/access_test.go | 4 +- scm/github/repo.go | 3 + scm/github/webhook.go | 1 + scm/service.go | 2 +- 21 files changed, 362 insertions(+), 69 deletions(-) create mode 100644 api/build/approve.go diff --git a/api/build/approve.go b/api/build/approve.go new file mode 100644 index 000000000..671eed72e --- /dev/null +++ b/api/build/approve.go @@ -0,0 +1,129 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "fmt" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/queue" + "github.com/go-vela/server/router/middleware/build" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/go-vela/types/constants" + "github.com/sirupsen/logrus" +) + +// swagger:operation POST /api/v1/repos/{org}/{repo}/builds/{build}/approve builds ApproveBuild +// +// Sign off on a build to run from an outside contributor +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: build +// description: Build number to retrieve +// required: true +// type: integer +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Request processed but build was skipped +// schema: +// type: string +// '201': +// description: Successfully created the build +// type: json +// schema: +// "$ref": "#/definitions/Build" +// '400': +// description: Unable to create the build +// schema: +// "$ref": "#/definitions/Error" +// '404': +// description: Unable to create the build +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to create the build +// schema: +// "$ref": "#/definitions/Error" + +// CreateBuild represents the API handler to approve a build to run in the configured backend. +func ApproveBuild(c *gin.Context) { + // capture middleware values + b := build.Retrieve(c) + o := org.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + ctx := c.Request.Context() + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logger := logrus.WithFields(logrus.Fields{ + "org": o, + "repo": r.GetName(), + "user": u.GetName(), + }) + + if !strings.EqualFold(b.GetStatus(), constants.StatusPendingApproval) { + retErr := fmt.Errorf("unable to approve build %s/%d: build not in pending approval state", r.GetFullName(), b.GetNumber()) + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + logger.Debugf("user %s approved build %s/%d for execution", u.GetName(), r.GetFullName(), b.GetNumber()) + + // send API call to capture the repo owner + u, err := database.FromContext(c).GetUser(ctx, r.GetUserID()) + if err != nil { + retErr := fmt.Errorf("unable to get owner for %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + b.SetStatus(constants.StatusPending) + + // update the build in the db + _, err = database.FromContext(c).UpdateBuild(ctx, b) + if err != nil { + logrus.Errorf("Failed to update build %d during publish to queue for %s: %v", b.GetNumber(), r.GetFullName(), err) + } + + // publish the build to the queue + go PublishToQueue( + ctx, + queue.FromGinContext(c), + database.FromContext(c), + b, + r, + u, + b.GetHost(), + ) + + c.JSON(http.StatusOK, fmt.Sprintf("Successfully approved build %s/%d", r.GetFullName(), b.GetNumber())) +} diff --git a/api/build/create.go b/api/build/create.go index c2885f096..32cf9686e 100644 --- a/api/build/create.go +++ b/api/build/create.go @@ -350,15 +350,37 @@ func CreateBuild(c *gin.Context) { logger.Errorf("unable to set commit status for build %s/%d: %v", r.GetFullName(), input.GetNumber(), err) } + // determine queue route + route, err := queue.FromGinContext(c).Route(&p.Worker) + if err != nil { + logrus.Errorf("unable to set route for build %d for %s: %v", input.GetNumber(), r.GetFullName(), err) + + // error out the build + CleanBuild(ctx, database.FromContext(c), input, nil, nil, err) + + return + } + + // temporarily set host to the route before it gets picked up by a worker + input.SetHost(route) + + err = PublishBuildExecutable(ctx, database.FromContext(c), p, input) + if err != nil { + retErr := fmt.Errorf("unable to publish build executable for %s/%d: %w", r.GetFullName(), input.GetNumber(), err) + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + // publish the build to the queue go PublishToQueue( ctx, queue.FromGinContext(c), database.FromContext(c), - p, input, r, u, + route, ) } diff --git a/api/build/executable.go b/api/build/executable.go index 17ca1c082..0ac3d88cf 100644 --- a/api/build/executable.go +++ b/api/build/executable.go @@ -5,6 +5,8 @@ package build import ( + "context" + "encoding/json" "fmt" "net/http" @@ -15,6 +17,8 @@ import ( "github.com/go-vela/server/router/middleware/org" "github.com/go-vela/server/router/middleware/repo" "github.com/go-vela/server/util" + "github.com/go-vela/types/library" + "github.com/go-vela/types/pipeline" "github.com/sirupsen/logrus" ) @@ -93,3 +97,36 @@ func GetBuildExecutable(c *gin.Context) { c.JSON(http.StatusOK, bExecutable) } + +// PublishBuildExecutable marshals a pipeline.Build into bytes and pushes that data to the build_executables table to be +// requested by a worker whenever the build has been picked up. +func PublishBuildExecutable(ctx context.Context, db database.Interface, p *pipeline.Build, b *library.Build) error { + // marshal pipeline build into byte data to add to the build executable object + byteExecutable, err := json.Marshal(p) + if err != nil { + logrus.Errorf("Failed to marshal build executable: %v", err) + + // error out the build + CleanBuild(ctx, db, b, nil, nil, err) + + return err + } + + // create build executable to push to database + bExecutable := new(library.BuildExecutable) + bExecutable.SetBuildID(b.GetID()) + bExecutable.SetData(byteExecutable) + + // send database call to create a build executable + err = db.CreateBuildExecutable(ctx, bExecutable) + if err != nil { + logrus.Errorf("Failed to publish build executable to database: %v", err) + + // error out the build + CleanBuild(ctx, db, b, nil, nil, err) + + return err + } + + return nil +} diff --git a/api/build/get_id.go b/api/build/get_id.go index 7a122c8f5..231144f34 100644 --- a/api/build/get_id.go +++ b/api/build/get_id.go @@ -101,7 +101,7 @@ func GetBuildByID(c *gin.Context) { // Capture user access from SCM. We do this in order to ensure user has access and is not // just retrieving any build using a random id number. - perm, err := scm.FromContext(c).RepoAccess(ctx, u, u.GetToken(), r.GetOrg(), r.GetName()) + perm, err := scm.FromContext(c).RepoAccess(ctx, u.GetName(), u.GetToken(), r.GetOrg(), r.GetName()) if err != nil { logrus.Errorf("unable to get user %s access level for repo %s", u.GetName(), r.GetFullName()) } diff --git a/api/build/publish.go b/api/build/publish.go index 3cf20ff02..8afed68d9 100644 --- a/api/build/publish.go +++ b/api/build/publish.go @@ -13,40 +13,11 @@ import ( "github.com/go-vela/server/queue" "github.com/go-vela/types" "github.com/go-vela/types/library" - "github.com/go-vela/types/pipeline" "github.com/sirupsen/logrus" ) -// PublishToQueue is a helper function that pushes the build executable to the database -// and publishes a queue item (build, repo, user) to the queue. -func PublishToQueue(ctx context.Context, queue queue.Service, db database.Interface, p *pipeline.Build, b *library.Build, r *library.Repo, u *library.User) { - // marshal pipeline build into byte data to add to the build executable object - byteExecutable, err := json.Marshal(p) - if err != nil { - logrus.Errorf("Failed to marshal build executable %d for %s: %v", b.GetNumber(), r.GetFullName(), err) - - // error out the build - CleanBuild(ctx, db, b, nil, nil, err) - - return - } - - // create build executable to push to database - bExecutable := new(library.BuildExecutable) - bExecutable.SetBuildID(b.GetID()) - bExecutable.SetData(byteExecutable) - - // send database call to create a build executable - err = db.CreateBuildExecutable(ctx, bExecutable) - if err != nil { - logrus.Errorf("Failed to publish build executable to database %d for %s: %v", b.GetNumber(), r.GetFullName(), err) - - // error out the build - CleanBuild(ctx, db, b, nil, nil, err) - - return - } - +// PublishToQueue is a helper function that publishes a queue item (build, repo, user) to the queue. +func PublishToQueue(ctx context.Context, queue queue.Service, db database.Interface, b *library.Build, r *library.Repo, u *library.User, route string) { // convert build, repo, and user into queue item item := types.ToItem(b, r, u) @@ -64,17 +35,6 @@ func PublishToQueue(ctx context.Context, queue queue.Service, db database.Interf logrus.Infof("Establishing route for build %d for %s", b.GetNumber(), r.GetFullName()) - // determine the route on which to publish the queue item - route, err := queue.Route(&p.Worker) - if err != nil { - logrus.Errorf("unable to set route for build %d for %s: %v", b.GetNumber(), r.GetFullName(), err) - - // error out the build - CleanBuild(ctx, db, b, nil, nil, err) - - return - } - logrus.Infof("Publishing item for build %d for %s to queue %s", b.GetNumber(), r.GetFullName(), route) // push item on to the queue diff --git a/api/build/restart.go b/api/build/restart.go index 6ee1e7fe9..52d17f64e 100644 --- a/api/build/restart.go +++ b/api/build/restart.go @@ -100,6 +100,14 @@ func RestartBuild(c *gin.Context) { "user": u.GetName(), }) + if strings.EqualFold(b.GetStatus(), constants.StatusPendingApproval) { + retErr := fmt.Errorf("unable to restart build %s/%d: cannot restart a build pending approval", r.GetFullName(), b.GetNumber()) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + logger.Infof("restarting build %s", entry) // send API call to capture the repo owner @@ -341,14 +349,36 @@ func RestartBuild(c *gin.Context) { logger.Errorf("unable to set commit status for build %s: %v", entry, err) } + // determine queue route + route, err := queue.FromContext(c).Route(&p.Worker) + if err != nil { + logrus.Errorf("unable to set route for build %d for %s: %v", b.GetNumber(), r.GetFullName(), err) + + // error out the build + CleanBuild(ctx, database.FromContext(c), b, nil, nil, err) + + return + } + + // temporarily set host to the route before it gets picked up by a worker + b.SetHost(route) + + err = PublishBuildExecutable(ctx, database.FromContext(c), p, b) + if err != nil { + retErr := fmt.Errorf("unable to publish build executable for %s/%d: %w", r.GetFullName(), b.GetNumber(), err) + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + // publish the build to the queue go PublishToQueue( ctx, queue.FromGinContext(c), database.FromContext(c), - p, b, r, u, + route, ) } diff --git a/api/repo/create.go b/api/repo/create.go index efa1611df..d46bab4d4 100644 --- a/api/repo/create.go +++ b/api/repo/create.go @@ -146,6 +146,14 @@ func CreateRepo(c *gin.Context) { r.SetVisibility(input.GetVisibility()) } + // set the fork policy field based off the input provided + if len(input.GetApproveForkBuild()) > 0 { + // default fork policy field to the input fork policy + r.SetApproveForkBuild(input.GetApproveForkBuild()) + } else { + r.SetApproveForkBuild(constants.ApproveNever) + } + // fields restricted to platform admins if u.GetAdmin() { // trusted default is false diff --git a/api/repo/update.go b/api/repo/update.go index 162c78ca9..fbf414533 100644 --- a/api/repo/update.go +++ b/api/repo/update.go @@ -157,6 +157,11 @@ func UpdateRepo(c *gin.Context) { r.SetVisibility(input.GetVisibility()) } + if len(input.GetApproveForkBuild()) > 0 { + // update fork policy if set + r.SetApproveForkBuild(input.GetApproveForkBuild()) + } + if input.Private != nil { // update private if set r.SetPrivate(input.GetPrivate()) diff --git a/api/scm/sync.go b/api/scm/sync.go index c462b6730..ff33799b0 100644 --- a/api/scm/sync.go +++ b/api/scm/sync.go @@ -98,7 +98,7 @@ func SyncRepo(c *gin.Context) { // verify the user is an admin of the repo // we cannot use our normal permissions check due to the possibility the repo was deleted - perm, err := scm.FromContext(c).RepoAccess(ctx, u, u.GetToken(), o, r.GetName()) + perm, err := scm.FromContext(c).RepoAccess(ctx, u.GetName(), u.GetToken(), o, r.GetName()) if err != nil { logger.Errorf("unable to get user %s access level for org %s", u.GetName(), o) } diff --git a/api/webhook/post.go b/api/webhook/post.go index 120f7ac95..724a8c14c 100644 --- a/api/webhook/post.go +++ b/api/webhook/post.go @@ -298,7 +298,7 @@ func PostWebhook(c *gin.Context) { } // confirm current repo owner has at least write access to repo (needed for status update later) - _, err = scm.FromContext(c).RepoAccess(ctx, u, u.GetToken(), r.GetOrg(), r.GetName()) + _, err = scm.FromContext(c).RepoAccess(ctx, u.GetName(), u.GetToken(), r.GetOrg(), r.GetName()) if err != nil { retErr := fmt.Errorf("unable to publish build to queue: repository owner %s no longer has write access to repository %s", u.GetName(), r.GetFullName()) util.HandleError(c, http.StatusUnauthorized, retErr) @@ -663,6 +663,59 @@ func PostWebhook(c *gin.Context) { c.JSON(http.StatusOK, b) + // determine queue route + route, err := queue.FromContext(c).Route(&p.Worker) + if err != nil { + logrus.Errorf("unable to set route for build %d for %s: %v", b.GetNumber(), r.GetFullName(), err) + + // error out the build + build.CleanBuild(ctx, database.FromContext(c), b, nil, nil, err) + + return + } + + // temporarily set host to the route before it gets picked up by a worker + b.SetHost(route) + + // publish the pipeline.Build to the build_executables table to be requested by a worker + err = build.PublishBuildExecutable(ctx, database.FromContext(c), p, b) + if err != nil { + retErr := fmt.Errorf("unable to publish build executable for %s/%d: %w", repo.GetFullName(), b.GetNumber(), err) + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // if the webhook was from a Pull event from a forked repository, verify it is allowed to run + if webhook.PRFork { + switch repo.GetApproveForkBuild() { + case constants.ApproveAlways: + err = gatekeepBuild(c, b, repo, u) + if err != nil { + util.HandleError(c, http.StatusInternalServerError, err) + } + + return + case constants.ApproveNoWrite: + // determine if build sender has write access to parent repo. If not, this call will result in an error + _, err = scm.FromContext(c).RepoAccess(ctx, b.GetSender(), u.GetToken(), r.GetOrg(), r.GetName()) + if err != nil { + err = gatekeepBuild(c, b, repo, u) + if err != nil { + util.HandleError(c, http.StatusInternalServerError, err) + } + + return + } + + fallthrough + case constants.ApproveNever: + fallthrough + default: + logrus.Debugf("fork PR build %s/%d automatically running without approval", repo.GetFullName(), b.GetNumber()) + } + } + // send API call to set the status on the commit err = scm.FromContext(c).Status(ctx, u, b, repo.GetOrg(), repo.GetName()) if err != nil { @@ -674,10 +727,10 @@ func PostWebhook(c *gin.Context) { ctx, queue.FromGinContext(c), database.FromContext(c), - p, b, repo, u, + route, ) } @@ -897,3 +950,23 @@ func renameRepository(ctx context.Context, h *library.Hook, r *library.Repo, c * return dbR, nil } + +// gatekeepBuild is a helper function that will set the status of a build to 'pending approval' and +// send a status update to the SCM. +func gatekeepBuild(c *gin.Context, b *library.Build, r *library.Repo, u *library.User) error { + logrus.Debugf("fork PR build %s/%d waiting for approval", r.GetFullName(), b.GetNumber()) + b.SetStatus(constants.StatusPendingApproval) + + _, err := database.FromContext(c).UpdateBuild(c, b) + if err != nil { + return fmt.Errorf("unable to update build for %s/%d: %w", r.GetFullName(), b.GetNumber(), err) + } + + // send API call to set the status on the commit + err = scm.FromContext(c).Status(c, u, b, r.GetOrg(), r.GetName()) + if err != nil { + logrus.Errorf("unable to set commit status for %s/%d: %v", r.GetFullName(), b.GetNumber(), err) + } + + return nil +} diff --git a/cmd/vela-server/schedule.go b/cmd/vela-server/schedule.go index 0ca6024c4..54383e05a 100644 --- a/cmd/vela-server/schedule.go +++ b/cmd/vela-server/schedule.go @@ -162,7 +162,7 @@ func processSchedule(ctx context.Context, s *library.Schedule, compiler compiler } // send API call to confirm repo owner has at least write access to repo - _, err = scm.RepoAccess(ctx, u, u.GetToken(), r.GetOrg(), r.GetName()) + _, err = scm.RepoAccess(ctx, u.GetName(), u.GetToken(), r.GetOrg(), r.GetName()) if err != nil { return fmt.Errorf("%s does not have at least write access for repo %s", u.GetName(), r.GetFullName()) } @@ -385,15 +385,36 @@ func processSchedule(ctx context.Context, s *library.Schedule, compiler compiler return fmt.Errorf("unable to get new build %s/%d: %w", r.GetFullName(), b.GetNumber(), err) } + // determine queue route + route, err := queue.Route(&p.Worker) + if err != nil { + logrus.Errorf("unable to set route for build %d for %s: %v", b.GetNumber(), r.GetFullName(), err) + + // error out the build + build.CleanBuild(ctx, database, b, nil, nil, err) + + return err + } + + // temporarily set host to the route before it gets picked up by a worker + b.SetHost(route) + + err = build.PublishBuildExecutable(ctx, database, p, b) + if err != nil { + retErr := fmt.Errorf("unable to publish build executable for %s/%d: %w", r.GetFullName(), b.GetNumber(), err) + + return retErr + } + // publish the build to the queue go build.PublishToQueue( ctx, queue, database, - p, b, r, u, + route, ) return nil diff --git a/database/repo/table.go b/database/repo/table.go index aa570dd43..57385b73d 100644 --- a/database/repo/table.go +++ b/database/repo/table.go @@ -40,6 +40,7 @@ repos ( allow_comment BOOLEAN, pipeline_type TEXT, previous_name VARCHAR(100), + approve_fork_build VARCHAR(20), UNIQUE(full_name) ); ` @@ -73,6 +74,7 @@ repos ( allow_comment BOOLEAN, pipeline_type TEXT, previous_name TEXT, + approve_fork_build TEXT, UNIQUE(full_name) ); ` diff --git a/go.mod b/go.mod index 2dfe1590d..08c1a22f3 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,8 @@ module github.com/go-vela/server go 1.21 +replace github.com/go-vela/types => ../types + require ( github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb github.com/DATA-DOG/go-sqlmock v1.5.0 diff --git a/go.sum b/go.sum index bd304a7cd..87a70089f 100644 --- a/go.sum +++ b/go.sum @@ -146,8 +146,6 @@ github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/go-vela/types v0.21.0-rc1 h1:k5XgPh4h5oVGwto7qPELoSp6Y8OL/z/z6FCjqTNZQHE= -github.com/go-vela/types v0.21.0-rc1/go.mod h1:Jn8K28uj7mACc55fkFgaIzL0q45iXydOFGEeoSeHUtQ= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= diff --git a/router/build.go b/router/build.go index c8cc0d688..92e9795b7 100644 --- a/router/build.go +++ b/router/build.go @@ -23,6 +23,7 @@ import ( // GET /api/v1/repos/:org/:repo/builds/:build // PUT /api/v1/repos/:org/:repo/builds/:build // DELETE /api/v1/repos/:org/:repo/builds/:build +// POST /api/v1/repos/:org/:repo/builds/:build/approve // DELETE /api/v1/repos/:org/:repo/builds/:build/cancel // GET /api/v1/repos/:org/:repo/builds/:build/logs // GET /api/v1/repos/:org/:repo/builds/:build/token @@ -59,6 +60,7 @@ func BuildHandlers(base *gin.RouterGroup) { b.GET("", perm.MustRead(), build.GetBuild) b.PUT("", perm.MustBuildAccess(), middleware.Payload(), build.UpdateBuild) b.DELETE("", perm.MustPlatformAdmin(), build.DeleteBuild) + b.POST("/approve", perm.MustAdmin(), build.ApproveBuild) b.DELETE("/cancel", executors.Establish(), perm.MustWrite(), build.CancelBuild) b.GET("/logs", perm.MustRead(), log.ListLogsForBuild) b.GET("/token", perm.MustWorkerAuthToken(), build.GetBuildToken) diff --git a/router/middleware/perm/perm.go b/router/middleware/perm/perm.go index ff960a549..9a18391e7 100644 --- a/router/middleware/perm/perm.go +++ b/router/middleware/perm/perm.go @@ -278,7 +278,7 @@ func MustSecretAdmin() gin.HandlerFunc { case constants.SecretRepo: logger.Debugf("verifying user %s has 'admin' permissions for repo %s/%s", u.GetName(), o, n) - perm, err := scm.FromContext(c).RepoAccess(ctx, u, u.GetToken(), o, n) + perm, err := scm.FromContext(c).RepoAccess(ctx, u.GetName(), u.GetToken(), o, n) if err != nil { logger.Errorf("unable to get user %s access level for repo %s/%s: %v", u.GetName(), o, n, err) } @@ -367,7 +367,7 @@ func MustAdmin() gin.HandlerFunc { } // query source to determine requesters permissions for the repo using the requester's token - perm, err := scm.FromContext(c).RepoAccess(ctx, u, u.GetToken(), r.GetOrg(), r.GetName()) + perm, err := scm.FromContext(c).RepoAccess(ctx, u.GetName(), u.GetToken(), r.GetOrg(), r.GetName()) if err != nil { // requester may not have permissions to use the Github API endpoint (requires read access) // try again using the repo owner token @@ -382,7 +382,7 @@ func MustAdmin() gin.HandlerFunc { return } - perm, err = scm.FromContext(c).RepoAccess(ctx, u, ro.GetToken(), r.GetOrg(), r.GetName()) + perm, err = scm.FromContext(c).RepoAccess(ctx, u.GetName(), ro.GetToken(), r.GetOrg(), r.GetName()) if err != nil { logger.Errorf("unable to get user %s access level for repo %s", u.GetName(), r.GetFullName()) } @@ -426,7 +426,7 @@ func MustWrite() gin.HandlerFunc { } // query source to determine requesters permissions for the repo using the requester's token - perm, err := scm.FromContext(c).RepoAccess(ctx, u, u.GetToken(), r.GetOrg(), r.GetName()) + perm, err := scm.FromContext(c).RepoAccess(ctx, u.GetName(), u.GetToken(), r.GetOrg(), r.GetName()) if err != nil { // requester may not have permissions to use the Github API endpoint (requires read access) // try again using the repo owner token @@ -441,7 +441,7 @@ func MustWrite() gin.HandlerFunc { return } - perm, err = scm.FromContext(c).RepoAccess(ctx, u, ro.GetToken(), r.GetOrg(), r.GetName()) + perm, err = scm.FromContext(c).RepoAccess(ctx, u.GetName(), ro.GetToken(), r.GetOrg(), r.GetName()) if err != nil { logger.Errorf("unable to get user %s access level for repo %s", u.GetName(), r.GetFullName()) } @@ -509,7 +509,7 @@ func MustRead() gin.HandlerFunc { } // query source to determine requesters permissions for the repo using the requester's token - perm, err := scm.FromContext(c).RepoAccess(ctx, u, u.GetToken(), r.GetOrg(), r.GetName()) + perm, err := scm.FromContext(c).RepoAccess(ctx, u.GetName(), u.GetToken(), r.GetOrg(), r.GetName()) if err != nil { // requester may not have permissions to use the Github API endpoint (requires read access) // try again using the repo owner token @@ -524,7 +524,7 @@ func MustRead() gin.HandlerFunc { return } - perm, err = scm.FromContext(c).RepoAccess(ctx, u, ro.GetToken(), r.GetOrg(), r.GetName()) + perm, err = scm.FromContext(c).RepoAccess(ctx, u.GetName(), ro.GetToken(), r.GetOrg(), r.GetName()) if err != nil { logger.Errorf("unable to get user %s access level for repo %s", u.GetName(), r.GetFullName()) } diff --git a/scm/github/access.go b/scm/github/access.go index 03a39f588..1df242639 100644 --- a/scm/github/access.go +++ b/scm/github/access.go @@ -50,20 +50,20 @@ func (c *client) OrgAccess(ctx context.Context, u *library.User, org string) (st } // RepoAccess captures the user's access level for a repo. -func (c *client) RepoAccess(ctx context.Context, u *library.User, token, org, repo string) (string, error) { +func (c *client) RepoAccess(ctx context.Context, name, token, org, repo string) (string, error) { c.Logger.WithFields(logrus.Fields{ "org": org, "repo": repo, - "user": u.GetName(), - }).Tracef("capturing %s access level to repo %s/%s", u.GetName(), org, repo) + "user": name, + }).Tracef("capturing %s access level to repo %s/%s", name, org, repo) // check if user is accessing repo in personal org - if strings.EqualFold(org, u.GetName()) { + if strings.EqualFold(org, name) { c.Logger.WithFields(logrus.Fields{ "org": org, "repo": repo, - "user": u.GetName(), - }).Debugf("skipping access level check for user %s with repo %s/%s", u.GetName(), org, repo) + "user": name, + }).Debugf("skipping access level check for user %s with repo %s/%s", name, org, repo) return "admin", nil } @@ -72,7 +72,7 @@ func (c *client) RepoAccess(ctx context.Context, u *library.User, token, org, re client := c.newClientToken(token) // send API call to capture repo access level for user - perm, _, err := client.Repositories.GetPermissionLevel(ctx, org, repo, u.GetName()) + perm, _, err := client.Repositories.GetPermissionLevel(ctx, org, repo, name) if err != nil { return "", err } diff --git a/scm/github/access_test.go b/scm/github/access_test.go index 79648baa0..2aa87ffaa 100644 --- a/scm/github/access_test.go +++ b/scm/github/access_test.go @@ -221,7 +221,7 @@ func TestGithub_RepoAccess_Admin(t *testing.T) { client, _ := NewTest(s.URL) // run test - got, err := client.RepoAccess(context.TODO(), u, u.GetToken(), "github", "octocat") + got, err := client.RepoAccess(context.TODO(), "foo", u.GetToken(), "github", "octocat") if resp.Code != http.StatusOK { t.Errorf("RepoAccess returned %v, want %v", resp.Code, http.StatusOK) @@ -251,7 +251,7 @@ func TestGithub_RepoAccess_NotFound(t *testing.T) { client, _ := NewTest(s.URL) // run test - got, err := client.RepoAccess(context.TODO(), u, u.GetToken(), "github", "octocat") + got, err := client.RepoAccess(context.TODO(), "foo", u.GetToken(), "github", "octocat") if err == nil { t.Errorf("RepoAccess should have returned err") diff --git a/scm/github/repo.go b/scm/github/repo.go index c148cd17f..0d6e43d41 100644 --- a/scm/github/repo.go +++ b/scm/github/repo.go @@ -292,6 +292,9 @@ func (c *client) Status(ctx context.Context, u *library.User, b *library.Build, case constants.StatusRunning, constants.StatusPending: state = "pending" description = fmt.Sprintf("the build is %s", b.GetStatus()) + case constants.StatusPendingApproval: + state = "pending" + description = fmt.Sprintf("build needs approval from repo admin to run") case constants.StatusSuccess: state = "success" description = "the build was successful" diff --git a/scm/github/webhook.go b/scm/github/webhook.go index 63b1a0632..5c89b337d 100644 --- a/scm/github/webhook.go +++ b/scm/github/webhook.go @@ -288,6 +288,7 @@ func (c *client) processPREvent(h *library.Hook, payload *github.PullRequestEven Hook: h, Repo: r, Build: b, + PRFork: payload.GetPullRequest().GetHead().GetRepo().GetFork(), }, nil } diff --git a/scm/service.go b/scm/service.go index 4586c1e26..fbf89d5e5 100644 --- a/scm/service.go +++ b/scm/service.go @@ -49,7 +49,7 @@ type Service interface { OrgAccess(context.Context, *library.User, string) (string, error) // RepoAccess defines a function that captures // the user's access level for a repo. - RepoAccess(context.Context, *library.User, string, string, string) (string, error) + RepoAccess(context.Context, string, string, string, string) (string, error) // TeamAccess defines a function that captures // the user's access level for a team. TeamAccess(context.Context, *library.User, string, string) (string, error) From 0c341f0bb7f723aa0074310028100c31e512556b Mon Sep 17 00:00:00 2001 From: ecrupper Date: Thu, 5 Oct 2023 09:21:18 -0500 Subject: [PATCH 2/9] handle cancellation --- api/build/cancel.go | 2 +- router/middleware/executors/executors.go | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/api/build/cancel.go b/api/build/cancel.go index cbc415024..2c553ad47 100644 --- a/api/build/cancel.go +++ b/api/build/cancel.go @@ -174,7 +174,7 @@ func CancelBuild(c *gin.Context) { return } } - case constants.StatusPending: + case constants.StatusPending, constants.StatusPendingApproval: break default: diff --git a/router/middleware/executors/executors.go b/router/middleware/executors/executors.go index 705bac0d4..bb4ac3c17 100644 --- a/router/middleware/executors/executors.go +++ b/router/middleware/executors/executors.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "net/http" + "strings" "time" "github.com/gin-gonic/gin" @@ -31,8 +32,8 @@ func Establish() gin.HandlerFunc { b := build.Retrieve(c) ctx := c.Request.Context() - // if build has no host, we cannot establish executors - if len(b.GetHost()) == 0 { + // if build is pending or pending approval, there is no host to establish executors + if strings.EqualFold(b.GetStatus(), constants.StatusPending) || strings.EqualFold(b.GetStatus(), constants.StatusPendingApproval) { ToContext(c, *e) c.Next() From c2d376197210a35c8e4d4ae3f5d92c3fe0c0b014 Mon Sep 17 00:00:00 2001 From: ecrupper Date: Tue, 28 Nov 2023 11:05:40 -0600 Subject: [PATCH 3/9] set approve fields --- api/build/approve.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/build/approve.go b/api/build/approve.go index 671eed72e..6bd74a61a 100644 --- a/api/build/approve.go +++ b/api/build/approve.go @@ -8,6 +8,7 @@ import ( "fmt" "net/http" "strings" + "time" "github.com/gin-gonic/gin" "github.com/go-vela/server/database" @@ -107,6 +108,8 @@ func ApproveBuild(c *gin.Context) { } b.SetStatus(constants.StatusPending) + b.SetApprovedAt(time.Now().Unix()) + b.SetApprovedBy(u.GetName()) // update the build in the db _, err = database.FromContext(c).UpdateBuild(ctx, b) From 018d1a3eb4b3e22e7617a1305ad1388285bc88f7 Mon Sep 17 00:00:00 2001 From: ecrupper Date: Tue, 28 Nov 2023 11:09:32 -0600 Subject: [PATCH 4/9] remove local replace go mod --- go.mod | 2 -- go.sum | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 00993b325..294cddf93 100644 --- a/go.mod +++ b/go.mod @@ -2,8 +2,6 @@ module github.com/go-vela/server go 1.21 -replace github.com/go-vela/types => ../types - require ( github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb github.com/DATA-DOG/go-sqlmock v1.5.0 diff --git a/go.sum b/go.sum index 88a73f80d..8ff193fdd 100644 --- a/go.sum +++ b/go.sum @@ -141,6 +141,8 @@ github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/go-vela/types v0.22.1-0.20231127165528-b43cd77e4d9a h1:gzDousRhfFnYV5YxceAaxu4F4PjEZ7gcHB3cBsQn148= +github.com/go-vela/types v0.22.1-0.20231127165528-b43cd77e4d9a/go.mod h1:ljNY36D6YkpObBbNF7Xslv3oxN4mGuQAwWhnnK/V06I= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= From 810455c402ecf498869d346be5526929da84a89c Mon Sep 17 00:00:00 2001 From: ecrupper Date: Tue, 28 Nov 2023 11:26:18 -0600 Subject: [PATCH 5/9] host check in establish --- router/middleware/executors/executors.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/router/middleware/executors/executors.go b/router/middleware/executors/executors.go index bb4ac3c17..688434f07 100644 --- a/router/middleware/executors/executors.go +++ b/router/middleware/executors/executors.go @@ -33,7 +33,9 @@ func Establish() gin.HandlerFunc { ctx := c.Request.Context() // if build is pending or pending approval, there is no host to establish executors - if strings.EqualFold(b.GetStatus(), constants.StatusPending) || strings.EqualFold(b.GetStatus(), constants.StatusPendingApproval) { + if strings.EqualFold(b.GetStatus(), constants.StatusPending) || + strings.EqualFold(b.GetStatus(), constants.StatusPendingApproval) || + len(b.GetHost()) == 0 { ToContext(c, *e) c.Next() From 338ba946b52deaa8895ebce085891553fbc4eee0 Mon Sep 17 00:00:00 2001 From: ecrupper Date: Tue, 28 Nov 2023 11:42:03 -0600 Subject: [PATCH 6/9] linter overlord appeasement --- api/build/approve.go | 4 +--- scm/github/access.go | 2 ++ scm/github/repo.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/build/approve.go b/api/build/approve.go index 6bd74a61a..84aedb418 100644 --- a/api/build/approve.go +++ b/api/build/approve.go @@ -1,6 +1,4 @@ -// Copyright (c) 2023 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. +// SPDX-License-Identifier: Apache-2.0 package build diff --git a/scm/github/access.go b/scm/github/access.go index 08b4090b8..96fc1aedd 100644 --- a/scm/github/access.go +++ b/scm/github/access.go @@ -67,6 +67,8 @@ func (c *client) RepoAccess(ctx context.Context, name, token, org, repo string) } // create github oauth client with the given token + // + //nolint:contextcheck // ignore context passing client := c.newClientToken(token) // send API call to capture repo access level for user diff --git a/scm/github/repo.go b/scm/github/repo.go index d3a1751ff..6fca1a92b 100644 --- a/scm/github/repo.go +++ b/scm/github/repo.go @@ -292,7 +292,7 @@ func (c *client) Status(ctx context.Context, u *library.User, b *library.Build, description = fmt.Sprintf("the build is %s", b.GetStatus()) case constants.StatusPendingApproval: state = "pending" - description = fmt.Sprintf("build needs approval from repo admin to run") + description = fmt.Sprint("build needs approval from repo admin to run") case constants.StatusSuccess: state = "success" description = "the build was successful" From 63fd0369264d6948c676320c56a440e67f1ce694 Mon Sep 17 00:00:00 2001 From: ecrupper Date: Tue, 28 Nov 2023 12:48:53 -0600 Subject: [PATCH 7/9] strings can be strings --- scm/github/repo.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm/github/repo.go b/scm/github/repo.go index 6fca1a92b..083597b55 100644 --- a/scm/github/repo.go +++ b/scm/github/repo.go @@ -292,7 +292,7 @@ func (c *client) Status(ctx context.Context, u *library.User, b *library.Build, description = fmt.Sprintf("the build is %s", b.GetStatus()) case constants.StatusPendingApproval: state = "pending" - description = fmt.Sprint("build needs approval from repo admin to run") + description = "build needs approval from repo admin to run" case constants.StatusSuccess: state = "success" description = "the build was successful" From a1ee828e782f33935e5423b7b18e4aa1bf1fc4f8 Mon Sep 17 00:00:00 2001 From: ecrupper Date: Wed, 29 Nov 2023 13:39:54 -0600 Subject: [PATCH 8/9] add error message for canceling --- api/build/cancel.go | 1 + 1 file changed, 1 insertion(+) diff --git a/api/build/cancel.go b/api/build/cancel.go index f4cc293dd..b34948492 100644 --- a/api/build/cancel.go +++ b/api/build/cancel.go @@ -185,6 +185,7 @@ func CancelBuild(c *gin.Context) { } } case constants.StatusPending, constants.StatusPendingApproval: + b.SetError(fmt.Sprintf("build was canceled by %s", user.GetName())) break default: From f6a10cbe59c57949d0159ec11c9b78bbe7adf154 Mon Sep 17 00:00:00 2001 From: ecrupper Date: Thu, 30 Nov 2023 11:16:51 -0600 Subject: [PATCH 9/9] linty --- api/build/cancel.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/build/cancel.go b/api/build/cancel.go index b34948492..7e72541db 100644 --- a/api/build/cancel.go +++ b/api/build/cancel.go @@ -185,7 +185,6 @@ func CancelBuild(c *gin.Context) { } } case constants.StatusPending, constants.StatusPendingApproval: - b.SetError(fmt.Sprintf("build was canceled by %s", user.GetName())) break default: @@ -199,6 +198,7 @@ func CancelBuild(c *gin.Context) { // build has been abandoned // update the status in the build table b.SetStatus(constants.StatusCanceled) + b.SetError(fmt.Sprintf("build was canceled by %s", user.GetName())) b, err := database.FromContext(c).UpdateBuild(ctx, b) if err != nil {