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']