Skip to content

Commit

Permalink
feat(webhook)!: support build approval based on repository settings (#…
Browse files Browse the repository at this point in the history
…1016)

* init commit

* handle cancellation

* set approve fields

* remove local replace go mod

* host check in establish

* linter overlord appeasement

* strings can be strings

* add error message for canceling

* linty

---------

Co-authored-by: Kelly Merrick <[email protected]>
  • Loading branch information
ecrupper and KellyMerrick authored Dec 8, 2023
1 parent 5e1a7d3 commit 5fc317f
Show file tree
Hide file tree
Showing 20 changed files with 368 additions and 71 deletions.
130 changes: 130 additions & 0 deletions api/build/approve.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// SPDX-License-Identifier: Apache-2.0

package build

import (
"fmt"
"net/http"
"strings"
"time"

"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)
b.SetApprovedAt(time.Now().Unix())
b.SetApprovedBy(u.GetName())

// 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()))
}
3 changes: 2 additions & 1 deletion api/build/cancel.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ func CancelBuild(c *gin.Context) {
return
}
}
case constants.StatusPending:
case constants.StatusPending, constants.StatusPendingApproval:
break

default:
Expand All @@ -198,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 {
Expand Down
24 changes: 23 additions & 1 deletion api/build/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,15 +348,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,
)
}

Expand Down
37 changes: 37 additions & 0 deletions api/build/executable.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
package build

import (
"context"
"encoding/json"
"fmt"
"net/http"

Expand All @@ -13,6 +15,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"
)

Expand Down Expand Up @@ -91,3 +95,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
}
2 changes: 1 addition & 1 deletion api/build/get_id.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,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())
}
Expand Down
44 changes: 2 additions & 42 deletions api/build/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,40 +11,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)

Expand All @@ -62,17 +33,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
Expand Down
32 changes: 31 additions & 1 deletion api/build/restart.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,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
Expand Down Expand Up @@ -339,14 +347,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,
)
}
7 changes: 7 additions & 0 deletions api/repo/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,13 @@ func CreateRepo(c *gin.Context) {
r.SetVisibility(input.GetVisibility())
}

// set the fork policy field based off the input provided
if len(input.GetApproveBuild()) > 0 {
r.SetApproveBuild(input.GetApproveBuild())
} else {
r.SetApproveBuild(constants.ApproveForkAlways)
}

// fields restricted to platform admins
if u.GetAdmin() {
// trusted default is false
Expand Down
5 changes: 5 additions & 0 deletions api/repo/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,11 @@ func UpdateRepo(c *gin.Context) {
r.SetVisibility(input.GetVisibility())
}

if len(input.GetApproveBuild()) > 0 {
// update fork policy if set
r.SetApproveBuild(input.GetApproveBuild())
}

if input.Private != nil {
// update private if set
r.SetPrivate(input.GetPrivate())
Expand Down
2 changes: 1 addition & 1 deletion api/scm/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,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)
}
Expand Down
Loading

0 comments on commit 5fc317f

Please sign in to comment.