Skip to content
This repository has been archived by the owner on Sep 5, 2019. It is now read-only.

Update build execution time #335

Merged
merged 16 commits into from
Sep 11, 2018
3 changes: 2 additions & 1 deletion pkg/apis/build/v1alpha1/build_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ type BuildSpec struct {
NodeSelector map[string]string `json:"nodeSelector,omitempty"`

// Time after which the build times out. Defaults to 10 minutes.
// Go's ParseDuration documentation for expected format: https://golang.org/pkg/time/#ParseDuration
// Specified build timeout should be less than 24h.
// Refer Go's ParseDuration documentation for expected format: https://golang.org/pkg/time/#ParseDuration
Timeout string `json:"timeout,omitempty"`
}

Expand Down
14 changes: 11 additions & 3 deletions pkg/builder/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,24 +75,32 @@ func IsDone(status *v1alpha1.BuildStatus) bool {
return false
}

// IsTimedOut returns true if the build's status indicates that build is timedout.
// IsTimeout returns true if the build's execution time is greater than
// specified build spec timeout.
func IsTimeout(status *v1alpha1.BuildStatus, buildTimeout string) bool {
var timeout time.Duration
var defaultTimeout = 10 * time.Minute
var err error

if status == nil {
return false
}

if buildTimeout == "" {
timeout = 10 * time.Minute
// Set default timeout to 10 minute if build timeout is not set
timeout = defaultTimeout
} else {
timeout, err = time.ParseDuration(buildTimeout)
if err != nil {
return false
}
}
return time.Since(status.CreationTime.Time).Seconds() > timeout.Seconds()

// If build has not started timeout, startTime should be zero.
if status.StartTime.Time.IsZero() {
return false
}
return time.Since(status.StartTime.Time).Seconds() > timeout.Seconds()
}

// ErrorMessage returns the error message from the status.
Expand Down
5 changes: 3 additions & 2 deletions pkg/controller/build/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,8 @@ func (c *Controller) syncHandler(key string) error {
Reason: "BuildTimeout",
Message: timeoutMsg,
})

// update build completed time
build.Status.CompletionTime = metav1.Now()
c.recorder.Eventf(build, corev1.EventTypeWarning, "BuildTimeout", timeoutMsg)

if _, err := c.updateStatus(build); err != nil {
Expand All @@ -303,7 +304,7 @@ func (c *Controller) syncHandler(key string) error {
}

c.logger.Errorf("Timeout: %v", timeoutMsg)
return fmt.Errorf("Build %q timed out after %s", build.Name, build.Spec.Timeout)
return nil
} else {
// if not timed out then wait async
if err := c.waitForOperationAsync(build, op); err != nil {
Expand Down
6 changes: 3 additions & 3 deletions pkg/controller/build/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ func TestBasicFlows(t *testing.T) {
t.Errorf("error fetching build: %v", err)
}
// Update status to current time
first.Status.CreationTime = metav1.Now()
first.Status.StartTime = metav1.Now()

if builder.IsDone(&first.Status) {
t.Errorf("First IsDone(%d); wanted not done, got done.", idx)
Expand Down Expand Up @@ -282,8 +282,8 @@ func TestTimeoutFlows(t *testing.T) {
f.updateIndex(i, []*v1alpha1.Build{first})

// Run a second iteration of the syncHandler.
if err := c.syncHandler(getKey(build, t)); err == nil {
t.Errorf("Expect syncing build to error with timeout")
if err := c.syncHandler(getKey(build, t)); err != nil {
t.Errorf("Unexpected error while syncing build: %v", err)
}

expectedTimeoutMsg := "Warning BuildTimeout Build \"test\" failed to finish within \"1s\""
Expand Down
6 changes: 2 additions & 4 deletions pkg/webhook/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,13 +243,11 @@ func validateTimeout(timeout string) error {
if timeout == "" {
return nil
} else {
timeout, err := time.ParseDuration(timeout)
timeoutDuration, err := time.ParseDuration(timeout)
if err != nil {
return validationError("InvalidTimeFormat", "invalid build timeout %q err %#v. Refer https://golang.org/pkg/time/#ParseDuration for time format documentation", timeout, err)
}

// time out should not greater than 24 hours
if timeout.Hours() > maxTimeout.Hours() {
if timeoutDuration > maxTimeout {
return validationError("InvalidTimeout", "build timeout exceeded 24h")
}
}
Expand Down
15 changes: 0 additions & 15 deletions test/bad-nodeselector/build-with-bad-selector.yaml

This file was deleted.

2 changes: 1 addition & 1 deletion test/build-with-timeout/build-with-high-timeout.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ metadata:
labels:
expect: succeeded
spec:
timeout: 500s
timeout: "500s"
steps:
- name: step1
image: ubuntu
Expand Down
2 changes: 1 addition & 1 deletion test/build-with-timeout/build-with-low-timeout.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ metadata:
labels:
expect: failed
spec:
timeout: 50s
timeout: "50s"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These quotes aren't necessary right? I don't feel strongly about removing them, I just want to make sure this all works if a user doesn't explicitly quote the string.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quotes are not necessary. When I changed the type into string I might have gone on a spree to add quotes everywhere :D I will follow up with a PR to address all your comments.

steps:
- name: step1
image: ubuntu
Expand Down
9 changes: 8 additions & 1 deletion test/e2e/e2e.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,14 @@ type buildClient struct {
}

func (c *buildClient) watchBuild(name string) (*v1alpha1.Build, error) {
w, err := c.builds.Watch(metav1.SingleObject(metav1.ObjectMeta{Name: name}))
ls := metav1.SingleObject(metav1.ObjectMeta{Name: name})
// TODO: Update watchBuild function to take this as parameter depending on test requirements

// Set build timeout to 120 seconds. This will trigger watch timeout error
var timeout int64 = 120
ls.TimeoutSeconds = &timeout

w, err := c.builds.Watch(ls)
if err != nil {
return nil, err
}
Expand Down
89 changes: 89 additions & 0 deletions test/e2e/simple_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"log"
"os"
"testing"
"time"

corev1 "k8s.io/api/core/v1"
kuberrors "k8s.io/apimachinery/pkg/api/errors"
Expand Down Expand Up @@ -82,6 +83,7 @@ func TestSimpleBuild(t *testing.T) {
Name: buildName,
},
Spec: v1alpha1.BuildSpec{
Timeout: "40s",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use an actual time.Duration for this field?

Copy link
Contributor Author

@shashwathi shashwathi Sep 10, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thats a great idea. I have updated the PR with this change.

Steps: []corev1.Container{{
Image: "busybox",
Args: []string{"echo", "simple"},
Expand Down Expand Up @@ -120,3 +122,90 @@ func TestFailingBuild(t *testing.T) {
t.Fatalf("watchBuild did not return expected error: %v", err)
}
}

func TestBuildLowTimeout(t *testing.T) {
clients := setup(t)

buildName := "build-low-timeout"
buildTimeout := "50s"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: buildTimeout = 50*time.Second then Timeout: buildTimeout.String() means you don't have to parse and handle the error down on line 169. Up to you, it's also fine like this.

if _, err := clients.buildClient.builds.Create(&v1alpha1.Build{
ObjectMeta: metav1.ObjectMeta{
Namespace: buildTestNamespace,
Name: buildName,
},
Spec: v1alpha1.BuildSpec{
Timeout: buildTimeout,
Steps: []corev1.Container{{
Name: "lowtimeoutstep",
Image: "ubuntu",
Command: []string{"/bin/bash"},
Args: []string{"-c", "sleep 2000"},
}},
},
}); err != nil {
t.Fatalf("Error creating build: %v", err)
}

b, err := clients.buildClient.watchBuild(buildName)
if err == nil {
t.Error("watchBuild did not return expected BuildTimeout error")
}

if &b.Status == nil {
t.Fatalf("wanted build status to be set; got nil")
}

successCondition := b.Status.GetCondition(v1alpha1.BuildSucceeded)

if successCondition == nil {
t.Fatalf("wanted build status to be set; got %q", b.Status)
}

// verify reason for build failure is timeout
if successCondition.Reason != "BuildTimeout" {
t.Fatalf("wanted BuildTimeout; got %q", successCondition.Reason)
}
buildDuration := b.Status.CompletionTime.Time.Sub(b.Status.StartTime.Time).Seconds()
lowerEnd, err := time.ParseDuration(buildTimeout)
if err != nil {
t.Errorf("Error parsing build duration: %v", err)
}
higherEnd := 90 * time.Second // build timeout + 30 sec poll time + 10 sec
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this value is intended to be based on buildTimeout, maybe it should explicitly be based on it?

higherEnd := buildTimeout + 30*time.Second + 10*time.Second

That way if we change buildTimeout above we won't also have to remember to update this value.


if !(buildDuration >= lowerEnd.Seconds() && buildDuration < higherEnd.Seconds()) {
t.Fatalf("Expected the build duration to be within range %.2fs to %.2fs; but got build duration: %f, start time: %q and completed time: %q \n",
lowerEnd.Seconds(),
higherEnd.Seconds(),
buildDuration,
b.Status.StartTime.Time,
b.Status.CompletionTime.Time,
)
}
}

// TestPendingBuild tests that a build with non existent node selector will remain in pending
// state until watch timeout.
func TestPendingBuild(t *testing.T) {
clients := setup(t)

buildName := "pending-build"
if _, err := clients.buildClient.builds.Create(&v1alpha1.Build{
ObjectMeta: metav1.ObjectMeta{
Namespace: buildTestNamespace,
Name: buildName,
},
Spec: v1alpha1.BuildSpec{
NodeSelector: map[string]string{"disk": "fake-ssd"},
Steps: []corev1.Container{{
Image: "busybox",
Args: []string{"false"}, // fails.
}},
},
}); err != nil {
t.Fatalf("Error creating build: %v", err)
}

if _, err := clients.buildClient.watchBuild(buildName); err == nil {
t.Fatalf("watchBuild did not return watch timeout error")
}
}