Skip to content

Commit

Permalink
feat(repo)!: add pending approval timeout (#1227)
Browse files Browse the repository at this point in the history
* init commit

* feat(repo): add pending approval timeout

* fix test

* remove dead code

---------

Co-authored-by: David May <[email protected]>
  • Loading branch information
ecrupper and wass3rw3rk authored Dec 30, 2024
1 parent ada42d5 commit 70ca430
Show file tree
Hide file tree
Showing 29 changed files with 1,100 additions and 651 deletions.
5 changes: 3 additions & 2 deletions api/build/auto_cancel.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func AutoCancel(c *gin.Context, b *types.Build, rB *types.Build, cancelOpts *pip
}).Info("build updated - build canceled")
}

return true, nil
return false, nil
}

// cancelRunning is a helper function that determines the executor currently running a build and sends an API call
Expand Down Expand Up @@ -220,7 +220,8 @@ func isCancelable(target *types.Build, current *types.Build) bool {
// target is cancelable if current build is also a push event and the branches are the same
return strings.EqualFold(current.GetEvent(), constants.EventPush) && strings.EqualFold(current.GetBranch(), target.GetBranch())
case constants.EventPull:
cancelableAction := strings.EqualFold(target.GetEventAction(), constants.ActionOpened) || strings.EqualFold(target.GetEventAction(), constants.ActionSynchronize)
cancelableAction := strings.EqualFold(target.GetEventAction(), constants.ActionOpened) ||
strings.EqualFold(target.GetEventAction(), constants.ActionSynchronize)

// target is cancelable if current build is also a pull event, target is an opened / synchronize action, and the current head ref matches target head ref
return strings.EqualFold(current.GetEvent(), constants.EventPull) && cancelableAction && strings.EqualFold(current.GetHeadRef(), target.GetHeadRef())
Expand Down
48 changes: 48 additions & 0 deletions api/build/enqueue.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ import (
"encoding/json"
"time"

"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"

"github.com/go-vela/server/api/types"
"github.com/go-vela/server/constants"
"github.com/go-vela/server/database"
"github.com/go-vela/server/queue"
"github.com/go-vela/server/queue/models"
"github.com/go-vela/server/scm"
)

// Enqueue is a helper function that pushes a queue item (build, repo, user) to the queue.
Expand Down Expand Up @@ -65,3 +69,47 @@ func Enqueue(ctx context.Context, queue queue.Service, db database.Interface, it

l.Info("updated build as enqueued")
}

// ShouldEnqueue is a helper function that will determine whether to publish a build to the queue or place it
// in pending approval status.
func ShouldEnqueue(c *gin.Context, l *logrus.Entry, b *types.Build, r *types.Repo) (bool, error) {
// if the webhook was from a Pull event from a forked repository, verify it is allowed to run
if b.GetFork() {
l.Tracef("inside %s workflow for fork PR build %s/%d", r.GetApproveBuild(), r.GetFullName(), b.GetNumber())

switch r.GetApproveBuild() {
case constants.ApproveForkAlways:
return false, nil
case constants.ApproveForkNoWrite:
// determine if build sender has write access to parent repo. If not, this call will result in an error
level, err := scm.FromContext(c).RepoAccess(c.Request.Context(), b.GetSender(), r.GetOwner().GetToken(), r.GetOrg(), r.GetName())
if err != nil || (level != "admin" && level != "write") {
//nolint:nilerr // an error here is not something we want to return since we are gating it anyway
return false, nil
}

l.Debugf("fork PR build %s/%d automatically running without approval. sender %s has %s access", r.GetFullName(), b.GetNumber(), b.GetSender(), level)
case constants.ApproveOnce:
// determine if build sender is in the contributors list for the repo
//
// NOTE: this call is cumbersome for repos with lots of contributors. Potential TODO: improve this if
// GitHub adds a single-contributor API endpoint.
contributor, err := scm.FromContext(c).RepoContributor(c.Request.Context(), r.GetOwner(), b.GetSender(), r.GetOrg(), r.GetName())
if err != nil {
return false, err
}

if !contributor {
return false, nil
}

fallthrough
case constants.ApproveNever:
fallthrough
default:
l.Debugf("fork PR build %s/%d automatically running without approval", r.GetFullName(), b.GetNumber())
}
}

return true, nil
}
54 changes: 54 additions & 0 deletions api/build/gatekeep.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// SPDX-License-Identifier: Apache-2.0

package build

import (
"fmt"

"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"

"github.com/go-vela/server/api/types"
"github.com/go-vela/server/constants"
"github.com/go-vela/server/database"
"github.com/go-vela/server/scm"
)

// 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 *types.Build, r *types.Repo) error {
l := c.MustGet("logger").(*logrus.Entry)

l = l.WithFields(logrus.Fields{
"org": r.GetOrg(),
"repo": r.GetName(),
"repo_id": r.GetID(),
"build": b.GetNumber(),
"build_id": b.GetID(),
})

l.Debug("fork PR build waiting for approval")

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)
}

l.Info("build updated")

// update the build components to pending approval status
err = UpdateComponentStatuses(c, b, constants.StatusPendingApproval)
if err != nil {
return fmt.Errorf("unable to update build components 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, r.GetOwner(), b, r.GetOrg(), r.GetName())
if err != nil {
l.Errorf("unable to set commit status for %s/%d: %v", r.GetFullName(), b.GetNumber(), err)
}

return nil
}
41 changes: 32 additions & 9 deletions api/build/restart.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,19 +149,42 @@ func RestartBuild(c *gin.Context) {
return
}

// determine whether or not to send compiled build to queue
shouldEnqueue, err := ShouldEnqueue(c, l, item.Build, r)
if err != nil {
util.HandleError(c, http.StatusInternalServerError, err)

return
}

if shouldEnqueue {
// send API call to set the status on the commit
err := scm.Status(c.Request.Context(), r.GetOwner(), b, r.GetOrg(), r.GetName())
if err != nil {
l.Errorf("unable to set commit status for %s/%d: %v", r.GetFullName(), b.GetNumber(), err)
}

// publish the build to the queue
go Enqueue(
context.WithoutCancel(c.Request.Context()),
queue.FromGinContext(c),
database.FromContext(c),
item,
b.GetHost(),
)
} else {
err := GatekeepBuild(c, b, r)
if err != nil {
util.HandleError(c, http.StatusInternalServerError, err)

return
}
}

l.WithFields(logrus.Fields{
"new_build": item.Build.GetNumber(),
"new_build_id": item.Build.GetID(),
}).Info("build created via restart")

c.JSON(http.StatusCreated, item.Build)

// publish the build to the queue
go Enqueue(
context.WithoutCancel(ctx),
queue.FromGinContext(c),
database.FromContext(c),
item,
item.Build.GetHost(),
)
}
17 changes: 17 additions & 0 deletions api/repo/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ func CreateRepo(c *gin.Context) {

defaultBuildLimit := c.Value("defaultBuildLimit").(int64)
defaultTimeout := c.Value("defaultTimeout").(int64)
defaultApprovalTimeout := c.Value("defaultApprovalTimeout").(int64)
maxBuildLimit := c.Value("maxBuildLimit").(int64)
defaultRepoEvents := c.Value("defaultRepoEvents").([]string)
defaultRepoEventsMask := c.Value("defaultRepoEventsMask").(int64)
Expand Down Expand Up @@ -138,10 +139,26 @@ func CreateRepo(c *gin.Context) {
r.SetTimeout(constants.BuildTimeoutDefault)
} else if input.GetTimeout() == 0 {
r.SetTimeout(defaultTimeout)
} else if input.GetTimeout() > constants.BuildTimeoutMax {
// set build timeout to max value to prevent timeout from exceeding max
r.SetTimeout(constants.BuildTimeoutMax)
} else {
r.SetTimeout(input.GetTimeout())
}

// set the approval timeout field based of the input provided
if input.GetApprovalTimeout() == 0 && defaultApprovalTimeout == 0 {
// default approval timeout to 7d
r.SetApprovalTimeout(constants.ApprovalTimeoutDefault)
} else if input.GetApprovalTimeout() == 0 {
r.SetApprovalTimeout(defaultApprovalTimeout)
} else if input.GetApprovalTimeout() > constants.ApprovalTimeoutMax {
// set approval timeout to max value to prevent timeout from exceeding max
r.SetApprovalTimeout(constants.ApprovalTimeoutMax)
} else {
r.SetApprovalTimeout(input.GetApprovalTimeout())
}

// set the visibility field based off the input provided
if len(input.GetVisibility()) > 0 {
// default visibility field to the input visibility
Expand Down
6 changes: 6 additions & 0 deletions api/repo/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@ func UpdateRepo(c *gin.Context) {
r.SetTimeout(limit)
}

if input.GetApprovalTimeout() > 0 {
// update build approval timeout if set
limit := max(constants.ApprovalTimeoutMin, min(input.GetApprovalTimeout(), constants.ApprovalTimeoutMax))
r.SetApprovalTimeout(limit)
}

if input.GetCounter() > 0 {
if input.GetCounter() <= r.GetCounter() {
retErr := fmt.Errorf("unable to set counter for repo %s: must be greater than current %d",
Expand Down
108 changes: 69 additions & 39 deletions api/types/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,51 +11,53 @@ import (
//
// swagger:model Repo
type Repo struct {
ID *int64 `json:"id,omitempty"`
Owner *User `json:"owner,omitempty"`
Hash *string `json:"-"`
Org *string `json:"org,omitempty"`
Name *string `json:"name,omitempty"`
FullName *string `json:"full_name,omitempty"`
Link *string `json:"link,omitempty"`
Clone *string `json:"clone,omitempty"`
Branch *string `json:"branch,omitempty"`
Topics *[]string `json:"topics,omitempty"`
BuildLimit *int64 `json:"build_limit,omitempty"`
Timeout *int64 `json:"timeout,omitempty"`
Counter *int `json:"counter,omitempty"`
Visibility *string `json:"visibility,omitempty"`
Private *bool `json:"private,omitempty"`
Trusted *bool `json:"trusted,omitempty"`
Active *bool `json:"active,omitempty"`
AllowEvents *Events `json:"allow_events,omitempty"`
PipelineType *string `json:"pipeline_type,omitempty"`
PreviousName *string `json:"previous_name,omitempty"`
ApproveBuild *string `json:"approve_build,omitempty"`
InstallID *int64 `json:"install_id,omitempty"`
ID *int64 `json:"id,omitempty"`
Owner *User `json:"owner,omitempty"`
Hash *string `json:"-"`
Org *string `json:"org,omitempty"`
Name *string `json:"name,omitempty"`
FullName *string `json:"full_name,omitempty"`
Link *string `json:"link,omitempty"`
Clone *string `json:"clone,omitempty"`
Branch *string `json:"branch,omitempty"`
Topics *[]string `json:"topics,omitempty"`
BuildLimit *int64 `json:"build_limit,omitempty"`
Timeout *int64 `json:"timeout,omitempty"`
Counter *int `json:"counter,omitempty"`
Visibility *string `json:"visibility,omitempty"`
Private *bool `json:"private,omitempty"`
Trusted *bool `json:"trusted,omitempty"`
Active *bool `json:"active,omitempty"`
AllowEvents *Events `json:"allow_events,omitempty"`
PipelineType *string `json:"pipeline_type,omitempty"`
PreviousName *string `json:"previous_name,omitempty"`
ApproveBuild *string `json:"approve_build,omitempty"`
ApprovalTimeout *int64 `json:"approval_timeout,omitempty"`
InstallID *int64 `json:"install_id,omitempty"`
}

// Environment returns a list of environment variables
// provided from the fields of the Repo type.
func (r *Repo) Environment() map[string]string {
return map[string]string{
"VELA_REPO_ACTIVE": ToString(r.GetActive()),
"VELA_REPO_ALLOW_EVENTS": strings.Join(r.GetAllowEvents().List()[:], ","),
"VELA_REPO_BRANCH": ToString(r.GetBranch()),
"VELA_REPO_TOPICS": strings.Join(r.GetTopics()[:], ","),
"VELA_REPO_BUILD_LIMIT": ToString(r.GetBuildLimit()),
"VELA_REPO_CLONE": ToString(r.GetClone()),
"VELA_REPO_FULL_NAME": ToString(r.GetFullName()),
"VELA_REPO_LINK": ToString(r.GetLink()),
"VELA_REPO_NAME": ToString(r.GetName()),
"VELA_REPO_ORG": ToString(r.GetOrg()),
"VELA_REPO_PRIVATE": ToString(r.GetPrivate()),
"VELA_REPO_TIMEOUT": ToString(r.GetTimeout()),
"VELA_REPO_TRUSTED": ToString(r.GetTrusted()),
"VELA_REPO_VISIBILITY": ToString(r.GetVisibility()),
"VELA_REPO_PIPELINE_TYPE": ToString(r.GetPipelineType()),
"VELA_REPO_APPROVE_BUILD": ToString(r.GetApproveBuild()),
"VELA_REPO_OWNER": ToString(r.GetOwner().GetName()),
"VELA_REPO_ACTIVE": ToString(r.GetActive()),
"VELA_REPO_ALLOW_EVENTS": strings.Join(r.GetAllowEvents().List()[:], ","),
"VELA_REPO_BRANCH": ToString(r.GetBranch()),
"VELA_REPO_TOPICS": strings.Join(r.GetTopics()[:], ","),
"VELA_REPO_BUILD_LIMIT": ToString(r.GetBuildLimit()),
"VELA_REPO_CLONE": ToString(r.GetClone()),
"VELA_REPO_FULL_NAME": ToString(r.GetFullName()),
"VELA_REPO_LINK": ToString(r.GetLink()),
"VELA_REPO_NAME": ToString(r.GetName()),
"VELA_REPO_ORG": ToString(r.GetOrg()),
"VELA_REPO_PRIVATE": ToString(r.GetPrivate()),
"VELA_REPO_TIMEOUT": ToString(r.GetTimeout()),
"VELA_REPO_TRUSTED": ToString(r.GetTrusted()),
"VELA_REPO_VISIBILITY": ToString(r.GetVisibility()),
"VELA_REPO_PIPELINE_TYPE": ToString(r.GetPipelineType()),
"VELA_REPO_APPROVE_BUILD": ToString(r.GetApproveBuild()),
"VELA_REPO_APPROVAL_TIMEOUT": ToString(r.GetApprovalTimeout()),
"VELA_REPO_OWNER": ToString(r.GetOwner().GetName()),

// deprecated environment variables
"REPOSITORY_ACTIVE": ToString(r.GetActive()),
Expand Down Expand Up @@ -346,6 +348,19 @@ func (r *Repo) GetApproveBuild() string {
return *r.ApproveBuild
}

// GetApprovalTimeout returns the ApprovalTimeout field.
//
// When the provided Repo type is nil, or the field within
// the type is nil, it returns the zero value for the field.
func (r *Repo) GetApprovalTimeout() int64 {
// return zero value if Repo type or ApprovalTimeout field is nil
if r == nil || r.ApprovalTimeout == nil {
return 0
}

return *r.ApprovalTimeout
}

// GetInstallID returns the InstallID field.
//
// When the provided Repo type is nil, or the field within
Expand Down Expand Up @@ -632,6 +647,19 @@ func (r *Repo) SetApproveBuild(v string) {
r.ApproveBuild = &v
}

// SetApprovalTimeout sets the ApprovalTimeout field.
//
// When the provided Repo type is nil, it
// will set nothing and immediately return.
func (r *Repo) SetApprovalTimeout(v int64) {
// return if Repo type is nil
if r == nil {
return
}

r.ApprovalTimeout = &v
}

// SetInstallID sets the InstallID field.
//
// When the provided Repo type is nil, it
Expand All @@ -650,6 +678,7 @@ func (r *Repo) String() string {
return fmt.Sprintf(`{
Active: %t,
AllowEvents: %s,
ApprovalTimeout: %d,
ApproveBuild: %s,
Branch: %s,
BuildLimit: %d,
Expand All @@ -672,6 +701,7 @@ func (r *Repo) String() string {
}`,
r.GetActive(),
r.GetAllowEvents().List(),
r.GetApprovalTimeout(),
r.GetApproveBuild(),
r.GetBranch(),
r.GetBuildLimit(),
Expand Down
Loading

0 comments on commit 70ca430

Please sign in to comment.