From 6cf99b1dcfcdc336dc8be518f868d021c5a6e4a7 Mon Sep 17 00:00:00 2001 From: Tournaris Pavlos-Petros Date: Tue, 23 Jul 2019 22:36:09 +0300 Subject: [PATCH 1/4] Introduce Cancelot --- .gitignore | 1 + cancelot/Dockerfile | 15 ++++++ cancelot/README.md | 45 ++++++++++++++++ cancelot/cancelot/cancelpreviousbuild.go | 65 ++++++++++++++++++++++++ cancelot/cancelot/cloudbuild.go | 50 ++++++++++++++++++ cancelot/cloudbuild.yaml | 6 +++ cancelot/main.go | 33 ++++++++++++ 7 files changed, 215 insertions(+) create mode 100644 .gitignore create mode 100644 cancelot/Dockerfile create mode 100644 cancelot/README.md create mode 100644 cancelot/cancelot/cancelpreviousbuild.go create mode 100644 cancelot/cancelot/cloudbuild.go create mode 100644 cancelot/cloudbuild.yaml create mode 100644 cancelot/main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..485dee64b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea diff --git a/cancelot/Dockerfile b/cancelot/Dockerfile new file mode 100644 index 000000000..a2ab0fca9 --- /dev/null +++ b/cancelot/Dockerfile @@ -0,0 +1,15 @@ +FROM gcr.io/cloud-builders/go AS build-env + +ADD ./ /go/src/ + +ENV GOBIN=/go/bin +RUN go get -d -v ./... +RUN go install /go/src/main.go + +FROM alpine:latest + +RUN apk add --no-cache ca-certificates + +COPY --from=build-env /go/bin/main /go/bin/main + +ENTRYPOINT [ "/go/bin/main" ] diff --git a/cancelot/README.md b/cancelot/README.md new file mode 100644 index 000000000..b2d692ca1 --- /dev/null +++ b/cancelot/README.md @@ -0,0 +1,45 @@ +# Cancelot + +Cancelot (thank you Twitter for the name :D) allows you to cancel previous builds running for the same branch, +when you are using VCS triggered builds. + +# Purpose + +Cancelot was built because there is no out of the box solution by CloudBuild, in order to cancel a previous running +build upon a new commit in the same branch. This can save a lot of build minutes that would be otherwise billed to the +account. + +# Deploying Cancelot + +* Make any changes you need +* Navigate to Cancelot's folder and execute the following: `gcloud builds submit . --config=cloudbuild.yaml` +* Enjoy + +## Using Cancelot + +Add the builder as the first step in your project's `cloudbuild.yaml`: + +```yaml +steps: +- name: 'gcr.io/$PROJECT_ID/cancelot' + args: [ + '--current_build_id', '$BUILD_ID', + '--branch_name', '$BRANCH_NAME' + ] +``` + +Cancelot will be invoked when your build starts and will try to find any running jobs that match the following filter: + +``` +build_id != "[CURRENT_BUILD_ID]" AND +source.repo_source.branch_name = "[BRANCH_NAME]" AND +status = "WORKING" AND +start_time<"[CURRENT_BUILD_START_TIME]" +``` + +After successfully fetching the list with the running builds that match the defined criteria, it loops and cancels +each one. + +## Inspiration + +Cancelot is heavily inspired by `slackbot` from CloudBuilders community diff --git a/cancelot/cancelot/cancelpreviousbuild.go b/cancelot/cancelot/cancelpreviousbuild.go new file mode 100644 index 000000000..f180a90f5 --- /dev/null +++ b/cancelot/cancelot/cancelpreviousbuild.go @@ -0,0 +1,65 @@ +package cancelot + +import ( + "context" + "fmt" + "google.golang.org/api/cloudbuild/v1" + "log" +) + +// Checks for previous running builds on the same branch, in order to cancel them +func CancelPreviousBuild(ctx context.Context, currentBuildId string, branchName string) { + svc := gcbClient(ctx) + project, err := getProject() + if err != nil { + log.Fatalf("Failed to get project: %v", err) + } + + log.Printf("Going to fetch current build details for: %s", currentBuildId) + + currentBuildResponse, currentBuildError := svc.Projects.Builds.Get(project, currentBuildId).Do() + if currentBuildError != nil { + log.Fatalf("Failed to get build details from Cloud Build. Will retry in one minute.") + } + + log.Printf("Going to check ongoing jobs for branch: %s", branchName) + + onGoingJobFilter := fmt.Sprintf(` + build_id != "%s" AND + source.repo_source.branch_name = "%s" AND + status = "WORKING" AND + start_time<"%s"`, + currentBuildId, + branchName, + currentBuildResponse.StartTime) + + log.Printf("Builds filter created: %s", onGoingJobFilter) + + onGoingBuildsResponse, onGoingBuildsError := svc.Projects.Builds.List(project).Filter(onGoingJobFilter).Do() + + if onGoingBuildsError != nil { + log.Fatalf("Failed to get builds from Cloud Build. Will retry in one minute.") + } + + onGoingBuilds := onGoingBuildsResponse.Builds + numOfOnGoingBuilds := len(onGoingBuilds) + + log.Printf("Ongoing builds for %s has size of: %d", branchName, numOfOnGoingBuilds) + + if numOfOnGoingBuilds == 0 { + return + } + + for _, build := range onGoingBuilds { + log.Printf("Going to cancel build with id: %s", build.Id) + + cancelBuildCall := svc.Projects.Builds.Cancel(project, build.Id, &cloudbuild.CancelBuildRequest{}) + buildCancel, err := cancelBuildCall.Do() + + if err != nil { + log.Fatalf("Failed to cancel build with id:%s", build.Id) + } else { + log.Printf("Cancelled build with id:%s", buildCancel.Id) + } + } +} diff --git a/cancelot/cancelot/cloudbuild.go b/cancelot/cancelot/cloudbuild.go new file mode 100644 index 000000000..7acf28ffe --- /dev/null +++ b/cancelot/cancelot/cloudbuild.go @@ -0,0 +1,50 @@ +package cancelot + +import ( + "bytes" + "context" + "log" + "os/exec" + "strings" + + "cloud.google.com/go/compute/metadata" + "golang.org/x/oauth2/google" + cloudbuild "google.golang.org/api/cloudbuild/v1" +) + +// getProject gets the project ID. +func getProject() (string, error) { + // Test if we're running on GCE. + if metadata.OnGCE() { + // Use the GCE Metadata service. + projectID, err := metadata.ProjectID() + if err != nil { + log.Printf("Failed to get project ID from instance metadata") + return "", err + } + return projectID, nil + } + // Shell out to gcloud. + cmd := exec.Command("gcloud", "config", "get-value", "project") + var out bytes.Buffer + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + log.Printf("Failed to shell out to gcloud: %+v", err) + return "", err + } + projectID := strings.TrimSuffix(out.String(), "\n") + return projectID, nil +} + +func gcbClient(ctx context.Context) *cloudbuild.Service { + client, err := google.DefaultClient(ctx, cloudbuild.CloudPlatformScope) + if err != nil { + log.Fatalf("Caught error creating client: %v", err) + } + svc, err := cloudbuild.New(client) + if err != nil { + log.Fatalf("Caught error creating service: %v", err) + } + return svc +} diff --git a/cancelot/cloudbuild.yaml b/cancelot/cloudbuild.yaml new file mode 100644 index 000000000..0d7bae7fc --- /dev/null +++ b/cancelot/cloudbuild.yaml @@ -0,0 +1,6 @@ +steps: +- name: 'gcr.io/cloud-builders/docker' + args: [ 'build', '-t', 'gcr.io/$PROJECT_ID/cancelot', '.' ] +images: +- 'gcr.io/$PROJECT_ID/cancelot' +tags: ['cloud-builders-community'] diff --git a/cancelot/main.go b/cancelot/main.go new file mode 100644 index 000000000..68542ee95 --- /dev/null +++ b/cancelot/main.go @@ -0,0 +1,33 @@ +// Post build status results to Slack. + +package main + +import ( + "context" + "flag" + "log" + + "./cancelot" +) + +var ( + currentBuildId = flag.String("current_build_id", "", "The current build id, in order to be excluded") + branchName = flag.String("branch_name", "", "BranchName to cancel previous ongoing jobs on") +) + +func main() { + log.Print("Starting cancelot") + flag.Parse() + ctx := context.Background() + + if *currentBuildId == "" { + log.Fatalf("CurrentBuildId must be provided.") + } + + if *branchName == "" { + log.Fatalf("BranchName must be provided.") + } + + cancelot.CancelPreviousBuild(ctx, *currentBuildId, *branchName) + return +} From 38d0df6f7c7f0f7ee023645009af8b2ba1ba6c4c Mon Sep 17 00:00:00 2001 From: pavlospt Date: Fri, 26 Jul 2019 13:53:47 +0300 Subject: [PATCH 2/4] Remove not needed `else` case --- cancelot/cancelot/cancelpreviousbuild.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cancelot/cancelot/cancelpreviousbuild.go b/cancelot/cancelot/cancelpreviousbuild.go index f180a90f5..01f7f642f 100644 --- a/cancelot/cancelot/cancelpreviousbuild.go +++ b/cancelot/cancelot/cancelpreviousbuild.go @@ -54,12 +54,12 @@ func CancelPreviousBuild(ctx context.Context, currentBuildId string, branchName log.Printf("Going to cancel build with id: %s", build.Id) cancelBuildCall := svc.Projects.Builds.Cancel(project, build.Id, &cloudbuild.CancelBuildRequest{}) - buildCancel, err := cancelBuildCall.Do() + buildCancelResponse, buildCancelError := cancelBuildCall.Do() - if err != nil { - log.Fatalf("Failed to cancel build with id:%s", build.Id) - } else { - log.Printf("Cancelled build with id:%s", buildCancel.Id) + if buildCancelError != nil { + log.Fatalf("Failed to cancel build with id:%s - %v", build.Id, buildCancelError) } + + log.Printf("Cancelled build with id:%s", buildCancelResponse.Id) } } From 18de62fb7550c87c693c496c503e5a878a85f2dc Mon Sep 17 00:00:00 2001 From: Tournaris Pavlos-Petros Date: Tue, 10 Sep 2019 22:27:40 +0300 Subject: [PATCH 3/4] Remove .gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .gitignore diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 485dee64b..000000000 --- a/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.idea From 9de46e349498053ca7d4ce8fc677e0aa8e42aeef Mon Sep 17 00:00:00 2001 From: pavlospt Date: Wed, 11 Sep 2019 09:32:09 +0300 Subject: [PATCH 4/4] Add simple test & README instructions for its execution --- cancelot/README.md | 8 ++++++++ cancelot/test/cloudbuild.yaml | 8 ++++++++ 2 files changed, 16 insertions(+) create mode 100644 cancelot/test/cloudbuild.yaml diff --git a/cancelot/README.md b/cancelot/README.md index b2d692ca1..9dab342d8 100644 --- a/cancelot/README.md +++ b/cancelot/README.md @@ -40,6 +40,14 @@ start_time<"[CURRENT_BUILD_START_TIME]" After successfully fetching the list with the running builds that match the defined criteria, it loops and cancels each one. +### Contributing + +After making any changes to Cancelot, please navigate to `test` folder & deploy the `cloudbuild.yaml`, like this: + +```bash +gcloud builds submit . --config=cloudbuild.yaml --substitutions=BRANCH_NAME="test" +``` + ## Inspiration Cancelot is heavily inspired by `slackbot` from CloudBuilders community diff --git a/cancelot/test/cloudbuild.yaml b/cancelot/test/cloudbuild.yaml new file mode 100644 index 000000000..04ebe5eb0 --- /dev/null +++ b/cancelot/test/cloudbuild.yaml @@ -0,0 +1,8 @@ +steps: +- name: 'gcr.io/$PROJECT_ID/cancelot' + args: [ + '--current_build_id', '$BUILD_ID', + '--branch_name', "$BRANCH_NAME" + ] +- name: 'ubuntu' + args: ['echo', 'done']