From 70109ed292152ce1847c8277a89dd2c46c38ccd9 Mon Sep 17 00:00:00 2001 From: ecrupper Date: Thu, 29 Jun 2023 09:13:32 -0500 Subject: [PATCH 1/8] init commit --- api/build/cancel.go | 13 +- api/build/token.go | 3 + api/webhook/post.go | 192 ++++++++++++++++++++ database/build/interface.go | 1 + database/build/list_pending_running_repo.go | 45 +++++ go.mod | 2 + go.sum | 2 - 7 files changed, 254 insertions(+), 4 deletions(-) create mode 100644 database/build/list_pending_running_repo.go diff --git a/api/build/cancel.go b/api/build/cancel.go index 350573911..4128b7bf0 100644 --- a/api/build/cancel.go +++ b/api/build/cancel.go @@ -79,7 +79,7 @@ func CancelBuild(c *gin.Context) { e := executors.Retrieve(c) o := org.Retrieve(c) r := repo.Retrieve(c) - u := user.Retrieve(c) + user := user.Retrieve(c) entry := fmt.Sprintf("%s/%d", r.GetFullName(), b.GetNumber()) @@ -90,7 +90,7 @@ func CancelBuild(c *gin.Context) { "build": b.GetNumber(), "org": o, "repo": r.GetName(), - "user": u.GetName(), + "user": user.GetName(), }).Infof("canceling build %s", entry) switch b.GetStatus() { @@ -172,6 +172,15 @@ func CancelBuild(c *gin.Context) { c.JSON(resp.StatusCode, b) + b.SetError(fmt.Sprintf("build was canceled by %s", user.GetName())) + b, err = database.FromContext(c).UpdateBuild(b) + if err != nil { + retErr := fmt.Errorf("unable to update status for build %s: %w", entry, err) + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + return } } diff --git a/api/build/token.go b/api/build/token.go index da18a0322..e8a98e5c4 100644 --- a/api/build/token.go +++ b/api/build/token.go @@ -83,6 +83,9 @@ func GetBuildToken(c *gin.Context) { "user": cl.Subject, }).Infof("generating build token for build %s/%d", r.GetFullName(), b.GetNumber()) + fmt.Println("EASTON THIS IS THE BUILD WE COMPARE STATUS TO") + fmt.Println(b.GetStatus()) + fmt.Println(b.GetNumber()) // if build is not in a pending state, then a build token should not be needed - conflict if !strings.EqualFold(b.GetStatus(), constants.StatusPending) { retErr := fmt.Errorf("unable to mint build token: build is not in pending state") diff --git a/api/webhook/post.go b/api/webhook/post.go index 7eab225f5..fb5e86c0e 100644 --- a/api/webhook/post.go +++ b/api/webhook/post.go @@ -7,6 +7,7 @@ package webhook import ( "bytes" "context" + "encoding/json" "fmt" "io" "net/http" @@ -18,6 +19,7 @@ import ( "github.com/go-vela/server/api/build" "github.com/go-vela/server/compiler" "github.com/go-vela/server/database" + "github.com/go-vela/server/internal/token" "github.com/go-vela/server/queue" "github.com/go-vela/server/scm" "github.com/go-vela/server/util" @@ -677,6 +679,33 @@ func PostWebhook(c *gin.Context) { logrus.Errorf("unable to set commit status for %s/%d: %v", repo.GetFullName(), b.GetNumber(), err) } + // if anything is provided in the auto_cancel metadata, then we start with true + runAutoCancel := p.Metadata.AutoCancel.Running || p.Metadata.AutoCancel.Pending || p.Metadata.AutoCancel.DefaultBranch + + // if the event is a push to the default branch and the AutoCancel.DefaultBranch value is false, bypass auto cancel + if strings.EqualFold(b.GetEvent(), constants.EventPush) && strings.EqualFold(b.GetBranch(), repo.GetBranch()) && !p.Metadata.AutoCancel.DefaultBranch { + runAutoCancel = false + } + + // if event is push or pull_request:synchronize, there is a chance this build could be superceding a stale build + // + // fetch pending and running builds for this repo in order to validate their merit to continue running. + if runAutoCancel && + ((strings.EqualFold(b.GetEvent(), constants.EventPull) && strings.EqualFold(b.GetEventAction(), constants.ActionSynchronize)) || + strings.EqualFold(b.GetEvent(), constants.EventPush)) { + // fetch pending and running builds + rBs, err := database.FromContext(c).ListPendingAndRunningBuildsForRepo(repo) + if err != nil { + logrus.Errorf("unable to fetch pending and running builds for %s: %v", repo.GetFullName(), err) + } + + // call auto cancel routine + err = autoCancel(c, b, rBs, repo, p.Metadata.AutoCancel.Pending, p.Metadata.AutoCancel.Running) + if err != nil { + logrus.Errorf("unable to cancel running build: %v", err) + } + } + // publish the build to the queue go build.PublishToQueue( queue.FromGinContext(c), @@ -904,3 +933,166 @@ func renameRepository(h *library.Hook, r *library.Repo, c *gin.Context, m *types return dbR, nil } + +// autoCancel is a helper function that checks to see if any pending or running +// builds for the repo can be replaced by the current build. +func autoCancel(c *gin.Context, b *library.Build, rBs []*library.Build, r *library.Repo, pending, running bool) error { + // iterate through pending and running builds + for _, rB := range rBs { + // if build is the current build, continue + if rB.GetID() == b.GetID() { + continue + } + + // ensure criteria is met before auto canceling + if (strings.EqualFold(rB.GetEvent(), constants.EventPush) && + strings.EqualFold(b.GetEvent(), constants.EventPush) && + strings.EqualFold(b.GetBranch(), rB.GetBranch())) || + (strings.EqualFold(rB.GetEvent(), constants.EventPull) && + strings.EqualFold(b.GetEventAction(), rB.GetEventAction()) && + strings.EqualFold(b.GetHeadRef(), rB.GetHeadRef())) { + switch { + case strings.EqualFold(rB.GetStatus(), constants.StatusPending) && pending: + // pending build will be handled gracefully by worker once pulled off queue + rB.SetStatus(constants.StatusCanceled) + + _, err := database.FromContext(c).UpdateBuild(rB) + if err != nil { + return err + } + case strings.EqualFold(rB.GetStatus(), constants.StatusRunning) && running: + // call cancelRunning routine for builds already running on worker + err := cancelRunning(c, rB, r) + if err != nil { + return err + } + default: + continue + } + + // set error message that references current build + rB.SetError(fmt.Sprintf("build was auto canceled in favor of build %d", b.GetNumber())) + + _, err := database.FromContext(c).UpdateBuild(rB) + if err != nil { + return err + } + } + } + + return nil +} + +// cancelRunning is a helper function that determines the executor currently running a build and sends an API call +// to that executor's worker to cancel the build. +func cancelRunning(c *gin.Context, b *library.Build, r *library.Repo) error { + e := new([]library.Executor) + // retrieve the worker + w, err := database.FromContext(c).GetWorkerForHostname(b.GetHost()) + if err != nil { + return err + } + + // prepare the request to the worker to retrieve executors + client := http.DefaultClient + client.Timeout = 30 * time.Second + endpoint := fmt.Sprintf("%s/api/v1/executors", w.GetAddress()) + + req, err := http.NewRequestWithContext(context.Background(), "GET", endpoint, nil) + if err != nil { + return err + } + + tm := c.MustGet("token-manager").(*token.Manager) + + // set mint token options + mto := &token.MintTokenOpts{ + Hostname: "vela-server", + TokenType: constants.WorkerAuthTokenType, + TokenDuration: time.Minute * 1, + } + + // mint token + tkn, err := tm.MintToken(mto) + if err != nil { + return err + } + + // add the token to authenticate to the worker + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", tkn)) + + // make the request to the worker and check the response + resp, err := client.Do(req) + if err != nil { + return err + } + + defer resp.Body.Close() + + // Read Response Body + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + // parse response and validate at least one item was returned + err = json.Unmarshal(respBody, e) + if err != nil { + return err + } + + for _, executor := range *e { + // check each executor on the worker running the build to see if it's running the build we want to cancel + if strings.EqualFold(executor.Repo.GetFullName(), r.GetFullName()) && *executor.GetBuild().Number == b.GetNumber() { + // prepare the request to the worker + client := http.DefaultClient + client.Timeout = 30 * time.Second + + // set the API endpoint path we send the request to + u := fmt.Sprintf("%s/api/v1/executors/%d/build/cancel", w.GetAddress(), executor.GetID()) + + req, err := http.NewRequestWithContext(context.Background(), "DELETE", u, nil) + if err != nil { + return err + } + + tm := c.MustGet("token-manager").(*token.Manager) + + // set mint token options + mto := &token.MintTokenOpts{ + Hostname: "vela-server", + TokenType: constants.WorkerAuthTokenType, + TokenDuration: time.Minute * 1, + } + + // mint token + tkn, err := tm.MintToken(mto) + if err != nil { + return err + } + + // add the token to authenticate to the worker + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", tkn)) + + // perform the request to the worker + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + // Read Response Body + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + err = json.Unmarshal(respBody, b) + if err != nil { + return err + } + } + } + + return nil +} diff --git a/database/build/interface.go b/database/build/interface.go index 6d05fa48b..d7d9334c8 100644 --- a/database/build/interface.go +++ b/database/build/interface.go @@ -58,6 +58,7 @@ type BuildInterface interface { ListBuildsForRepo(*library.Repo, map[string]interface{}, int64, int64, int, int) ([]*library.Build, int64, error) // ListPendingAndRunningBuilds defines a function that gets a list of pending and running builds. ListPendingAndRunningBuilds(string) ([]*library.BuildQueue, error) + ListPendingAndRunningBuildsForRepo(*library.Repo) ([]*library.Build, error) // UpdateBuild defines a function that updates an existing build. UpdateBuild(*library.Build) (*library.Build, error) } diff --git a/database/build/list_pending_running_repo.go b/database/build/list_pending_running_repo.go new file mode 100644 index 000000000..e834e4855 --- /dev/null +++ b/database/build/list_pending_running_repo.go @@ -0,0 +1,45 @@ +// 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 ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +// ListPendingAndRunningBuilds gets a list of all pending and running builds in the provided timeframe from the database. +func (e *engine) ListPendingAndRunningBuildsForRepo(repo *library.Repo) ([]*library.Build, error) { + e.logger.Trace("listing all pending and running builds from the database") + + // variables to store query results and return value + b := new([]database.Build) + builds := []*library.Build{} + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableBuild). + Select("*"). + Where("repo_id = ?", repo.GetID()). + Where("status = 'running' OR status = 'pending'"). + Find(&b). + Error + if err != nil { + return nil, err + } + + // iterate through all query results + for _, build := range *b { + // https://golang.org/doc/faq#closures_and_goroutines + tmp := build + + // convert query result to library type + // + // https://pkg.go.dev/github.com/go-vela/types/database#Build.ToLibrary + builds = append(builds, tmp.ToLibrary()) + } + + return builds, nil +} diff --git a/go.mod b/go.mod index dea6f6204..95a2d86f3 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,8 @@ module github.com/go-vela/server go 1.19 +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 2465b7cca..59e6d33d3 100644 --- a/go.sum +++ b/go.sum @@ -142,8 +142,6 @@ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91 github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= 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-vela/types v0.20.0-rc1 h1:t4tz9YjExtrFMFTq6w+0xWens8b0UPC1kcI642Ta3yc= -github.com/go-vela/types v0.20.0-rc1/go.mod h1:1ZSmKWX9MamKogwaIb53mzzRpZMV34mJFKiGfVFadFk= 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 64b94b222b5a22d982f3226c82592dd82901653a Mon Sep 17 00:00:00 2001 From: ecrupper Date: Fri, 30 Jun 2023 14:25:55 -0500 Subject: [PATCH 2/8] move auto cancel to build pkg --- api/build/auto_cancel.go | 184 +++++++++++++++++++++++++++++++++++++++ api/webhook/post.go | 167 +---------------------------------- 2 files changed, 185 insertions(+), 166 deletions(-) create mode 100644 api/build/auto_cancel.go diff --git a/api/build/auto_cancel.go b/api/build/auto_cancel.go new file mode 100644 index 000000000..30bd62ed9 --- /dev/null +++ b/api/build/auto_cancel.go @@ -0,0 +1,184 @@ +// 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 ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/internal/token" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" +) + +// AutoCancel is a helper function that checks to see if any pending or running +// builds for the repo can be replaced by the current build. +func AutoCancel(c *gin.Context, b *library.Build, rBs []*library.Build, r *library.Repo, pending, running bool) error { + // iterate through pending and running builds + for _, rB := range rBs { + // if build is the current build, continue + if rB.GetID() == b.GetID() { + continue + } + + // ensure criteria is met before auto canceling + if (strings.EqualFold(rB.GetEvent(), constants.EventPush) && + strings.EqualFold(b.GetEvent(), constants.EventPush) && + strings.EqualFold(b.GetBranch(), rB.GetBranch())) || + (strings.EqualFold(rB.GetEvent(), constants.EventPull) && + strings.EqualFold(b.GetEventAction(), rB.GetEventAction()) && + strings.EqualFold(b.GetHeadRef(), rB.GetHeadRef())) { + switch { + case strings.EqualFold(rB.GetStatus(), constants.StatusPending) && pending: + // pending build will be handled gracefully by worker once pulled off queue + rB.SetStatus(constants.StatusCanceled) + + _, err := database.FromContext(c).UpdateBuild(rB) + if err != nil { + return err + } + case strings.EqualFold(rB.GetStatus(), constants.StatusRunning) && running: + // call cancelRunning routine for builds already running on worker + err := cancelRunning(c, rB, r) + if err != nil { + return err + } + default: + continue + } + + // set error message that references current build + rB.SetError(fmt.Sprintf("build was auto canceled in favor of build %d", b.GetNumber())) + + _, err := database.FromContext(c).UpdateBuild(rB) + if err != nil { + return err + } + } + } + + return nil +} + +// cancelRunning is a helper function that determines the executor currently running a build and sends an API call +// to that executor's worker to cancel the build. +func cancelRunning(c *gin.Context, b *library.Build, r *library.Repo) error { + e := new([]library.Executor) + // retrieve the worker + w, err := database.FromContext(c).GetWorkerForHostname(b.GetHost()) + if err != nil { + return err + } + + // prepare the request to the worker to retrieve executors + client := http.DefaultClient + client.Timeout = 30 * time.Second + endpoint := fmt.Sprintf("%s/api/v1/executors", w.GetAddress()) + + req, err := http.NewRequestWithContext(context.Background(), "GET", endpoint, nil) + if err != nil { + return err + } + + tm := c.MustGet("token-manager").(*token.Manager) + + // set mint token options + mto := &token.MintTokenOpts{ + Hostname: "vela-server", + TokenType: constants.WorkerAuthTokenType, + TokenDuration: time.Minute * 1, + } + + // mint token + tkn, err := tm.MintToken(mto) + if err != nil { + return err + } + + // add the token to authenticate to the worker + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", tkn)) + + // make the request to the worker and check the response + resp, err := client.Do(req) + if err != nil { + return err + } + + defer resp.Body.Close() + + // Read Response Body + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + // parse response and validate at least one item was returned + err = json.Unmarshal(respBody, e) + if err != nil { + return err + } + + for _, executor := range *e { + // check each executor on the worker running the build to see if it's running the build we want to cancel + if strings.EqualFold(executor.Repo.GetFullName(), r.GetFullName()) && *executor.GetBuild().Number == b.GetNumber() { + // prepare the request to the worker + client := http.DefaultClient + client.Timeout = 30 * time.Second + + // set the API endpoint path we send the request to + u := fmt.Sprintf("%s/api/v1/executors/%d/build/cancel", w.GetAddress(), executor.GetID()) + + req, err := http.NewRequestWithContext(context.Background(), "DELETE", u, nil) + if err != nil { + return err + } + + tm := c.MustGet("token-manager").(*token.Manager) + + // set mint token options + mto := &token.MintTokenOpts{ + Hostname: "vela-server", + TokenType: constants.WorkerAuthTokenType, + TokenDuration: time.Minute * 1, + } + + // mint token + tkn, err := tm.MintToken(mto) + if err != nil { + return err + } + + // add the token to authenticate to the worker + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", tkn)) + + // perform the request to the worker + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + // Read Response Body + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + err = json.Unmarshal(respBody, b) + if err != nil { + return err + } + } + } + + return nil +} diff --git a/api/webhook/post.go b/api/webhook/post.go index fb5e86c0e..456c7107f 100644 --- a/api/webhook/post.go +++ b/api/webhook/post.go @@ -7,7 +7,6 @@ package webhook import ( "bytes" "context" - "encoding/json" "fmt" "io" "net/http" @@ -19,7 +18,6 @@ import ( "github.com/go-vela/server/api/build" "github.com/go-vela/server/compiler" "github.com/go-vela/server/database" - "github.com/go-vela/server/internal/token" "github.com/go-vela/server/queue" "github.com/go-vela/server/scm" "github.com/go-vela/server/util" @@ -700,7 +698,7 @@ func PostWebhook(c *gin.Context) { } // call auto cancel routine - err = autoCancel(c, b, rBs, repo, p.Metadata.AutoCancel.Pending, p.Metadata.AutoCancel.Running) + err = build.AutoCancel(c, b, rBs, repo, p.Metadata.AutoCancel.Pending, p.Metadata.AutoCancel.Running) if err != nil { logrus.Errorf("unable to cancel running build: %v", err) } @@ -933,166 +931,3 @@ func renameRepository(h *library.Hook, r *library.Repo, c *gin.Context, m *types return dbR, nil } - -// autoCancel is a helper function that checks to see if any pending or running -// builds for the repo can be replaced by the current build. -func autoCancel(c *gin.Context, b *library.Build, rBs []*library.Build, r *library.Repo, pending, running bool) error { - // iterate through pending and running builds - for _, rB := range rBs { - // if build is the current build, continue - if rB.GetID() == b.GetID() { - continue - } - - // ensure criteria is met before auto canceling - if (strings.EqualFold(rB.GetEvent(), constants.EventPush) && - strings.EqualFold(b.GetEvent(), constants.EventPush) && - strings.EqualFold(b.GetBranch(), rB.GetBranch())) || - (strings.EqualFold(rB.GetEvent(), constants.EventPull) && - strings.EqualFold(b.GetEventAction(), rB.GetEventAction()) && - strings.EqualFold(b.GetHeadRef(), rB.GetHeadRef())) { - switch { - case strings.EqualFold(rB.GetStatus(), constants.StatusPending) && pending: - // pending build will be handled gracefully by worker once pulled off queue - rB.SetStatus(constants.StatusCanceled) - - _, err := database.FromContext(c).UpdateBuild(rB) - if err != nil { - return err - } - case strings.EqualFold(rB.GetStatus(), constants.StatusRunning) && running: - // call cancelRunning routine for builds already running on worker - err := cancelRunning(c, rB, r) - if err != nil { - return err - } - default: - continue - } - - // set error message that references current build - rB.SetError(fmt.Sprintf("build was auto canceled in favor of build %d", b.GetNumber())) - - _, err := database.FromContext(c).UpdateBuild(rB) - if err != nil { - return err - } - } - } - - return nil -} - -// cancelRunning is a helper function that determines the executor currently running a build and sends an API call -// to that executor's worker to cancel the build. -func cancelRunning(c *gin.Context, b *library.Build, r *library.Repo) error { - e := new([]library.Executor) - // retrieve the worker - w, err := database.FromContext(c).GetWorkerForHostname(b.GetHost()) - if err != nil { - return err - } - - // prepare the request to the worker to retrieve executors - client := http.DefaultClient - client.Timeout = 30 * time.Second - endpoint := fmt.Sprintf("%s/api/v1/executors", w.GetAddress()) - - req, err := http.NewRequestWithContext(context.Background(), "GET", endpoint, nil) - if err != nil { - return err - } - - tm := c.MustGet("token-manager").(*token.Manager) - - // set mint token options - mto := &token.MintTokenOpts{ - Hostname: "vela-server", - TokenType: constants.WorkerAuthTokenType, - TokenDuration: time.Minute * 1, - } - - // mint token - tkn, err := tm.MintToken(mto) - if err != nil { - return err - } - - // add the token to authenticate to the worker - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", tkn)) - - // make the request to the worker and check the response - resp, err := client.Do(req) - if err != nil { - return err - } - - defer resp.Body.Close() - - // Read Response Body - respBody, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - - // parse response and validate at least one item was returned - err = json.Unmarshal(respBody, e) - if err != nil { - return err - } - - for _, executor := range *e { - // check each executor on the worker running the build to see if it's running the build we want to cancel - if strings.EqualFold(executor.Repo.GetFullName(), r.GetFullName()) && *executor.GetBuild().Number == b.GetNumber() { - // prepare the request to the worker - client := http.DefaultClient - client.Timeout = 30 * time.Second - - // set the API endpoint path we send the request to - u := fmt.Sprintf("%s/api/v1/executors/%d/build/cancel", w.GetAddress(), executor.GetID()) - - req, err := http.NewRequestWithContext(context.Background(), "DELETE", u, nil) - if err != nil { - return err - } - - tm := c.MustGet("token-manager").(*token.Manager) - - // set mint token options - mto := &token.MintTokenOpts{ - Hostname: "vela-server", - TokenType: constants.WorkerAuthTokenType, - TokenDuration: time.Minute * 1, - } - - // mint token - tkn, err := tm.MintToken(mto) - if err != nil { - return err - } - - // add the token to authenticate to the worker - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", tkn)) - - // perform the request to the worker - resp, err := client.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - // Read Response Body - respBody, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - - err = json.Unmarshal(respBody, b) - if err != nil { - return err - } - } - } - - return nil -} From 9783d4b300815a83ee44795ab00734256bf755ee Mon Sep 17 00:00:00 2001 From: ecrupper Date: Mon, 9 Oct 2023 15:31:24 -0500 Subject: [PATCH 3/8] db test file for new func --- .../build/list_pending_running_repo_test.go | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 database/build/list_pending_running_repo_test.go diff --git a/database/build/list_pending_running_repo_test.go b/database/build/list_pending_running_repo_test.go new file mode 100644 index 000000000..089a6d7b3 --- /dev/null +++ b/database/build/list_pending_running_repo_test.go @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: Apache-2.0 + +package build + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +func TestBuild_Engine_ListPendingAndRunningBuildsForRepo(t *testing.T) { + // setup types + _buildOne := testBuild() + _buildOne.SetID(1) + _buildOne.SetRepoID(1) + _buildOne.SetNumber(1) + _buildOne.SetStatus("running") + _buildOne.SetCreated(1) + _buildOne.SetDeployPayload(nil) + + _buildTwo := testBuild() + _buildTwo.SetID(2) + _buildTwo.SetRepoID(1) + _buildTwo.SetNumber(2) + _buildTwo.SetStatus("pending") + _buildTwo.SetCreated(1) + _buildTwo.SetDeployPayload(nil) + + _buildThree := testBuild() + _buildThree.SetID(3) + _buildThree.SetRepoID(2) + _buildThree.SetNumber(1) + _buildThree.SetStatus("pending") + _buildThree.SetCreated(1) + _buildThree.SetDeployPayload(nil) + + _repo := testRepo() + _repo.SetID(1) + _repo.SetUserID(1) + _repo.SetHash("baz") + _repo.SetOrg("foo") + _repo.SetName("bar") + _repo.SetFullName("foo/bar") + _repo.SetVisibility("public") + + _repoTwo := testRepo() + _repoTwo.SetID(2) + _repoTwo.SetUserID(1) + _repoTwo.SetHash("bazzy") + _repoTwo.SetOrg("foo") + _repoTwo.SetName("baz") + _repoTwo.SetFullName("foo/baz") + _repoTwo.SetVisibility("public") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected name query result in mock + _rows := sqlmock.NewRows( + []string{"id", "repo_id", "pipeline_id", "number", "parent", "event", "event_action", "status", "error", "enqueued", "created", "started", "finished", "deploy", "deploy_payload", "clone", "source", "title", "message", "commit", "sender", "author", "email", "link", "branch", "ref", "base_ref", "head_ref", "host", "runtime", "distribution", "timestamp"}). + AddRow(2, 1, nil, 2, 0, "", "", "pending", "", 0, 1, 0, 0, "", nil, "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 0). + AddRow(1, 1, nil, 1, 0, "", "", "running", "", 0, 1, 0, 0, "", nil, "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 0) + + // ensure the mock expects the name query + _mock.ExpectQuery(`SELECT * FROM "builds" WHERE repo_id = $1 AND (status = 'running' OR status = 'pending')`).WithArgs(1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateBuild(context.TODO(), _buildOne) + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + _, err = _sqlite.CreateBuild(context.TODO(), _buildTwo) + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + _, err = _sqlite.CreateBuild(context.TODO(), _buildThree) + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + err = _sqlite.client.AutoMigrate(&database.Repo{}) + if err != nil { + t.Errorf("unable to create repo table for sqlite: %v", err) + } + + err = _sqlite.client.Table(constants.TableRepo).Create(database.RepoFromLibrary(_repo)).Error + if err != nil { + t.Errorf("unable to create test repo for sqlite: %v", err) + } + + err = _sqlite.client.Table(constants.TableRepo).Create(database.RepoFromLibrary(_repoTwo)).Error + if err != nil { + t.Errorf("unable to create test repo for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want []*library.Build + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: []*library.Build{_buildTwo, _buildOne}, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: []*library.Build{_buildOne, _buildTwo}, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.ListPendingAndRunningBuildsForRepo(context.TODO(), _repo) + + if test.failure { + if err == nil { + t.Errorf("ListPendingAndRunningBuildsForRepo for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListPendingAndRunningBuildsForRepo for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListPendingAndRunningBuildsForRepo for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} From 1fc8e76024b7019d272f5a9cc577a672482fe43a Mon Sep 17 00:00:00 2001 From: ecrupper Date: Mon, 9 Oct 2023 15:52:11 -0500 Subject: [PATCH 4/8] integration test --- database/integration_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/database/integration_test.go b/database/integration_test.go index 6aa55502d..838d8a723 100644 --- a/database/integration_test.go +++ b/database/integration_test.go @@ -296,6 +296,19 @@ func testBuilds(t *testing.T, db Interface, resources *Resources) { } methods["ListBuildsForRepo"] = true + // list the pending / running builds for a repo + list, err = db.ListPendingAndRunningBuildsForRepo(context.TODO(), resources.Repos[0]) + if err != nil { + t.Errorf("unable to list pending and running builds for repo %d: %v", resources.Repos[0].GetID(), err) + } + if int(count) != len(resources.Builds) { + t.Errorf("ListPendingAndRunningBuildsForRepo() is %v, want %v", count, len(resources.Builds)) + } + if !cmp.Equal(list, []*library.Build{resources.Builds[0], resources.Builds[1]}) { + t.Errorf("ListPendingAndRunningBuildsForRepo() is %v, want %v", list, []*library.Build{resources.Builds[0], resources.Builds[1]}) + } + methods["ListPendingAndRunningBuildsForRepo"] = true + // list the pending and running builds queueList, err := db.ListPendingAndRunningBuilds(context.TODO(), "0") if err != nil { From ae5eb9e4b012e3cd7b39c6ae8384d01e0d110d83 Mon Sep 17 00:00:00 2001 From: ecrupper Date: Mon, 9 Oct 2023 16:13:10 -0500 Subject: [PATCH 5/8] linter and fmt debug statements --- api/auth/validate_oauth.go | 4 +--- api/build/token.go | 3 --- api/queue/queue.go | 4 +--- database/build/list_pending_running_repo.go | 4 +--- database/resource.go | 4 +--- database/validate.go | 4 +--- database/validate_test.go | 4 +--- router/middleware/signing_test.go | 4 +--- router/queue.go | 4 +--- 9 files changed, 8 insertions(+), 27 deletions(-) diff --git a/api/auth/validate_oauth.go b/api/auth/validate_oauth.go index a941050ee..5a31c0069 100644 --- a/api/auth/validate_oauth.go +++ b/api/auth/validate_oauth.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 auth diff --git a/api/build/token.go b/api/build/token.go index cd91a2126..7365a72d4 100644 --- a/api/build/token.go +++ b/api/build/token.go @@ -81,9 +81,6 @@ func GetBuildToken(c *gin.Context) { "user": cl.Subject, }).Infof("generating build token for build %s/%d", r.GetFullName(), b.GetNumber()) - fmt.Println("EASTON THIS IS THE BUILD WE COMPARE STATUS TO") - fmt.Println(b.GetStatus()) - fmt.Println(b.GetNumber()) // if build is not in a pending state, then a build token should not be needed - conflict if !strings.EqualFold(b.GetStatus(), constants.StatusPending) { retErr := fmt.Errorf("unable to mint build token: build is not in pending state") diff --git a/api/queue/queue.go b/api/queue/queue.go index 10946e99c..78d4c170b 100644 --- a/api/queue/queue.go +++ b/api/queue/queue.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 queue diff --git a/database/build/list_pending_running_repo.go b/database/build/list_pending_running_repo.go index 73bfd0e62..3c82ca419 100644 --- a/database/build/list_pending_running_repo.go +++ b/database/build/list_pending_running_repo.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/database/resource.go b/database/resource.go index 353f25513..35f7c7a97 100644 --- a/database/resource.go +++ b/database/resource.go @@ -1,6 +1,4 @@ -// Copyright (c) 2023 Target Brands, Ine. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. +// SPDX-License-Identifier: Apache-2.0 package database diff --git a/database/validate.go b/database/validate.go index 18f07cb84..4c519d081 100644 --- a/database/validate.go +++ b/database/validate.go @@ -1,6 +1,4 @@ -// Copyright (c) 2023 Target Brands, Ine. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. +// SPDX-License-Identifier: Apache-2.0 package database diff --git a/database/validate_test.go b/database/validate_test.go index 60761e215..36b60fb98 100644 --- a/database/validate_test.go +++ b/database/validate_test.go @@ -1,6 +1,4 @@ -// Copyright (c) 2023 Target Brands, Ine. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. +// SPDX-License-Identifier: Apache-2.0 package database diff --git a/router/middleware/signing_test.go b/router/middleware/signing_test.go index 908c0908b..3f152a3c9 100644 --- a/router/middleware/signing_test.go +++ b/router/middleware/signing_test.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 middleware diff --git a/router/queue.go b/router/queue.go index d3b311b13..ca408ffe3 100644 --- a/router/queue.go +++ b/router/queue.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 router From b1c41988995c2ab157a724dd8fbb361bc019fbd1 Mon Sep 17 00:00:00 2001 From: ecrupper Date: Tue, 10 Oct 2023 09:17:02 -0500 Subject: [PATCH 6/8] linter overlord --- api/build/cancel.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/api/build/cancel.go b/api/build/cancel.go index 375787555..e8cd19264 100644 --- a/api/build/cancel.go +++ b/api/build/cancel.go @@ -169,9 +169,8 @@ func CancelBuild(c *gin.Context) { return } - c.JSON(resp.StatusCode, b) - b.SetError(fmt.Sprintf("build was canceled by %s", user.GetName())) + b, err = database.FromContext(c).UpdateBuild(ctx, b) if err != nil { retErr := fmt.Errorf("unable to update status for build %s: %w", entry, err) @@ -180,6 +179,8 @@ func CancelBuild(c *gin.Context) { return } + c.JSON(resp.StatusCode, b) + return } } From 4dc8711e3334aecc3e821aef1b3a2fbc690629d8 Mon Sep 17 00:00:00 2001 From: ecrupper Date: Thu, 12 Oct 2023 09:57:36 -0500 Subject: [PATCH 7/8] address feedback --- api/build/auto_cancel.go | 9 +++++---- api/webhook/post.go | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/api/build/auto_cancel.go b/api/build/auto_cancel.go index 5af31042e..8609d48fd 100644 --- a/api/build/auto_cancel.go +++ b/api/build/auto_cancel.go @@ -16,11 +16,12 @@ import ( "github.com/go-vela/server/internal/token" "github.com/go-vela/types/constants" "github.com/go-vela/types/library" + "github.com/go-vela/types/pipeline" ) // AutoCancel is a helper function that checks to see if any pending or running // builds for the repo can be replaced by the current build. -func AutoCancel(c *gin.Context, b *library.Build, rBs []*library.Build, r *library.Repo, pending, running bool) error { +func AutoCancel(c *gin.Context, b *library.Build, rBs []*library.Build, r *library.Repo, cancelOpts *pipeline.CancelOptions) error { // iterate through pending and running builds for _, rB := range rBs { // if build is the current build, continue @@ -28,7 +29,7 @@ func AutoCancel(c *gin.Context, b *library.Build, rBs []*library.Build, r *libra continue } - // ensure criteria is met before auto canceling + // ensure criteria is met before auto canceling (push to same branch, or pull with same action from same head_ref) if (strings.EqualFold(rB.GetEvent(), constants.EventPush) && strings.EqualFold(b.GetEvent(), constants.EventPush) && strings.EqualFold(b.GetBranch(), rB.GetBranch())) || @@ -36,7 +37,7 @@ func AutoCancel(c *gin.Context, b *library.Build, rBs []*library.Build, r *libra strings.EqualFold(b.GetEventAction(), rB.GetEventAction()) && strings.EqualFold(b.GetHeadRef(), rB.GetHeadRef())) { switch { - case strings.EqualFold(rB.GetStatus(), constants.StatusPending) && pending: + case strings.EqualFold(rB.GetStatus(), constants.StatusPending) && cancelOpts.Pending: // pending build will be handled gracefully by worker once pulled off queue rB.SetStatus(constants.StatusCanceled) @@ -44,7 +45,7 @@ func AutoCancel(c *gin.Context, b *library.Build, rBs []*library.Build, r *libra if err != nil { return err } - case strings.EqualFold(rB.GetStatus(), constants.StatusRunning) && running: + case strings.EqualFold(rB.GetStatus(), constants.StatusRunning) && cancelOpts.Running: // call cancelRunning routine for builds already running on worker err := cancelRunning(c, rB, r) if err != nil { diff --git a/api/webhook/post.go b/api/webhook/post.go index a5e731f0d..c7d18eada 100644 --- a/api/webhook/post.go +++ b/api/webhook/post.go @@ -688,7 +688,7 @@ func PostWebhook(c *gin.Context) { } // call auto cancel routine - err = build.AutoCancel(c, b, rBs, repo, p.Metadata.AutoCancel.Pending, p.Metadata.AutoCancel.Running) + err = build.AutoCancel(c, b, rBs, repo, p.Metadata.AutoCancel) if err != nil { logrus.Errorf("unable to cancel running build: %v", err) } From 9e08ad7b7ef8dbf7439d52aec189ded3179a1c3b Mon Sep 17 00:00:00 2001 From: ecrupper Date: Fri, 13 Oct 2023 14:25:37 -0500 Subject: [PATCH 8/8] publish before auto cancel and continue upon failure to cancel --- api/build/auto_cancel.go | 72 +++++++++++++++++++--------------------- api/webhook/post.go | 37 ++++++++++++--------- 2 files changed, 57 insertions(+), 52 deletions(-) diff --git a/api/build/auto_cancel.go b/api/build/auto_cancel.go index 8609d48fd..975306f8c 100644 --- a/api/build/auto_cancel.go +++ b/api/build/auto_cancel.go @@ -21,51 +21,49 @@ import ( // AutoCancel is a helper function that checks to see if any pending or running // builds for the repo can be replaced by the current build. -func AutoCancel(c *gin.Context, b *library.Build, rBs []*library.Build, r *library.Repo, cancelOpts *pipeline.CancelOptions) error { - // iterate through pending and running builds - for _, rB := range rBs { - // if build is the current build, continue - if rB.GetID() == b.GetID() { - continue - } - - // ensure criteria is met before auto canceling (push to same branch, or pull with same action from same head_ref) - if (strings.EqualFold(rB.GetEvent(), constants.EventPush) && - strings.EqualFold(b.GetEvent(), constants.EventPush) && - strings.EqualFold(b.GetBranch(), rB.GetBranch())) || - (strings.EqualFold(rB.GetEvent(), constants.EventPull) && - strings.EqualFold(b.GetEventAction(), rB.GetEventAction()) && - strings.EqualFold(b.GetHeadRef(), rB.GetHeadRef())) { - switch { - case strings.EqualFold(rB.GetStatus(), constants.StatusPending) && cancelOpts.Pending: - // pending build will be handled gracefully by worker once pulled off queue - rB.SetStatus(constants.StatusCanceled) - - _, err := database.FromContext(c).UpdateBuild(c, rB) - if err != nil { - return err - } - case strings.EqualFold(rB.GetStatus(), constants.StatusRunning) && cancelOpts.Running: - // call cancelRunning routine for builds already running on worker - err := cancelRunning(c, rB, r) - if err != nil { - return err - } - default: - continue - } +func AutoCancel(c *gin.Context, b *library.Build, rB *library.Build, r *library.Repo, cancelOpts *pipeline.CancelOptions) (bool, error) { + // if build is the current build, continue + if rB.GetID() == b.GetID() { + return false, nil + } - // set error message that references current build - rB.SetError(fmt.Sprintf("build was auto canceled in favor of build %d", b.GetNumber())) + // ensure criteria is met before auto canceling (push to same branch, or pull with same action from same head_ref) + if (strings.EqualFold(rB.GetEvent(), constants.EventPush) && + strings.EqualFold(b.GetEvent(), constants.EventPush) && + strings.EqualFold(b.GetBranch(), rB.GetBranch())) || + (strings.EqualFold(rB.GetEvent(), constants.EventPull) && + strings.EqualFold(b.GetEventAction(), rB.GetEventAction()) && + strings.EqualFold(b.GetHeadRef(), rB.GetHeadRef())) { + switch { + case strings.EqualFold(rB.GetStatus(), constants.StatusPending) && cancelOpts.Pending: + // pending build will be handled gracefully by worker once pulled off queue + rB.SetStatus(constants.StatusCanceled) _, err := database.FromContext(c).UpdateBuild(c, rB) if err != nil { - return err + return false, err } + case strings.EqualFold(rB.GetStatus(), constants.StatusRunning) && cancelOpts.Running: + // call cancelRunning routine for builds already running on worker + err := cancelRunning(c, rB, r) + if err != nil { + return false, err + } + default: + return false, nil + } + + // set error message that references current build + rB.SetError(fmt.Sprintf("build was auto canceled in favor of build %d", b.GetNumber())) + + _, err := database.FromContext(c).UpdateBuild(c, rB) + if err != nil { + // if this call fails, we still canceled the build, so return true + return true, err } } - return nil + return true, nil } // cancelRunning is a helper function that determines the executor currently running a build and sends an API call diff --git a/api/webhook/post.go b/api/webhook/post.go index c7d18eada..aa4449194 100644 --- a/api/webhook/post.go +++ b/api/webhook/post.go @@ -667,6 +667,17 @@ func PostWebhook(c *gin.Context) { logrus.Errorf("unable to set commit status for %s/%d: %v", repo.GetFullName(), b.GetNumber(), err) } + // publish the build to the queue + go build.PublishToQueue( + ctx, + queue.FromGinContext(c), + database.FromContext(c), + p, + b, + repo, + u, + ) + // if anything is provided in the auto_cancel metadata, then we start with true runAutoCancel := p.Metadata.AutoCancel.Running || p.Metadata.AutoCancel.Pending || p.Metadata.AutoCancel.DefaultBranch @@ -687,23 +698,19 @@ func PostWebhook(c *gin.Context) { logrus.Errorf("unable to fetch pending and running builds for %s: %v", repo.GetFullName(), err) } - // call auto cancel routine - err = build.AutoCancel(c, b, rBs, repo, p.Metadata.AutoCancel) - if err != nil { - logrus.Errorf("unable to cancel running build: %v", err) + for _, rB := range rBs { + // call auto cancel routine + canceled, err := build.AutoCancel(c, b, rB, repo, p.Metadata.AutoCancel) + if err != nil { + // continue cancel loop if error, but log based on type of error + if canceled { + logrus.Errorf("unable to update canceled build error message: %v", err) + } else { + logrus.Errorf("unable to cancel running build: %v", err) + } + } } } - - // publish the build to the queue - go build.PublishToQueue( - ctx, - queue.FromGinContext(c), - database.FromContext(c), - p, - b, - repo, - u, - ) } // handleRepositoryEvent is a helper function that processes repository events from the SCM and updates