Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(webhook)!: support build approval based on repository settings #1016

Merged
merged 14 commits into from
Dec 8, 2023
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()))
}
2 changes: 1 addition & 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
ecrupper marked this conversation as resolved.
Show resolved Hide resolved

default:
Expand Down
24 changes: 23 additions & 1 deletion api/build/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@

// check if the pipeline did not already exist in the database
//
//nolint:dupl // ignore duplicate code

Check failure on line 302 in api/build/create.go

View workflow job for this annotation

GitHub Actions / golangci

[golangci] api/build/create.go#L302

directive `//nolint:dupl // ignore duplicate code` is unused for linter "dupl" (nolintlint)
Raw output
api/build/create.go:302:2: directive `//nolint:dupl // ignore duplicate code` is unused for linter "dupl" (nolintlint)
	//nolint:dupl // ignore duplicate code
	^
if pipeline == nil {
pipeline = compiled
pipeline.SetRepoID(r.GetID())
Expand Down Expand Up @@ -348,15 +348,37 @@
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 @@
"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 @@ -291,7 +299,7 @@

// check if the pipeline did not already exist in the database
//
//nolint:dupl // ignore duplicate code

Check failure on line 302 in api/build/restart.go

View workflow job for this annotation

GitHub Actions / golangci

[golangci] api/build/restart.go#L302

directive `//nolint:dupl // ignore duplicate code` is unused for linter "dupl" (nolintlint)
Raw output
api/build/restart.go:302:2: directive `//nolint:dupl // ignore duplicate code` is unused for linter "dupl" (nolintlint)
	//nolint:dupl // ignore duplicate code
	^
if pipeline == nil {
pipeline = compiled
pipeline.SetRepoID(r.GetID())
Expand Down Expand Up @@ -339,14 +347,36 @@
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
Loading