Skip to content

Commit 7a1bf39

Browse files
committed
Improve resilience of oc start-build log streaming
* Add retry when attempting to stream build logs. * Increase server-side build wait timeout to 30s. Fixes bug 1575990
1 parent 9fd7944 commit 7a1bf39

File tree

3 files changed

+137
-26
lines changed

3 files changed

+137
-26
lines changed

pkg/build/registry/buildlog/rest.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ type REST struct {
3838
getSimpleLogsFn func(podNamespace, podName string, logOpts *kapi.PodLogOptions) (runtime.Object, error)
3939
}
4040

41-
const defaultTimeout time.Duration = 10 * time.Second
41+
const defaultTimeout time.Duration = 30 * time.Second
4242

4343
// NewREST creates a new REST for BuildLog
4444
// Takes build registry and pod client to get necessary attributes to assemble

pkg/oc/cli/cmd/startbuild.go

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ import (
4242
cmdutil "github.com/openshift/origin/pkg/cmd/util"
4343
"github.com/openshift/origin/pkg/git"
4444
"github.com/openshift/origin/pkg/oc/cli/util/clientcmd"
45+
ocerrors "github.com/openshift/origin/pkg/oc/errors"
4546
utilenv "github.com/openshift/origin/pkg/oc/util/env"
46-
oerrors "github.com/openshift/origin/pkg/util/errors"
4747
)
4848

4949
var (
@@ -161,9 +161,10 @@ type StartBuildOptions struct {
161161
GitRepository string
162162
GitPostReceive string
163163

164-
Mapper meta.RESTMapper
165-
BuildClient buildclient.BuildInterface
166-
ClientConfig *restclient.Config
164+
Mapper meta.RESTMapper
165+
BuildClient buildclient.BuildInterface
166+
BuildLogClient buildclientinternalmanual.BuildLogInterface
167+
ClientConfig *restclient.Config
167168

168169
AsBinary bool
169170
ShortOutput bool
@@ -287,6 +288,8 @@ func (o *StartBuildOptions) Complete(f *clientcmd.Factory, cmd *cobra.Command, c
287288
o.Namespace = namespace
288289
o.Name = name
289290

291+
o.BuildLogClient = buildclientinternalmanual.NewBuildLogClient(o.BuildClient.RESTClient(), o.Namespace)
292+
290293
// Handle environment variables
291294
cmdutil.WarnAboutCommaSeparation(o.ErrOut, o.Env, "--env")
292295
env, _, err := utilenv.ParseEnv(o.Env, o.In)
@@ -431,27 +434,9 @@ func (o *StartBuildOptions) Run() error {
431434

432435
// Stream the logs from the build
433436
if o.Follow {
434-
opts := buildapi.BuildLogOptions{
435-
Follow: true,
436-
NoWait: false,
437-
}
438-
logClient := buildclientinternalmanual.NewBuildLogClient(o.BuildClient.RESTClient(), o.Namespace)
439-
for {
440-
rd, err := logClient.Logs(newBuild.Name, opts).Stream()
441-
if err != nil {
442-
// retry the connection to build logs when we hit the timeout.
443-
if oerrors.IsTimeoutErr(err) {
444-
fmt.Fprintf(o.ErrOut, "timed out getting logs, retrying\n")
445-
continue
446-
}
447-
fmt.Fprintf(o.ErrOut, "error getting logs (%v), waiting for build to complete\n", err)
448-
break
449-
}
450-
defer rd.Close()
451-
if _, err = io.Copy(o.Out, rd); err != nil {
452-
fmt.Fprintf(o.ErrOut, "error streaming logs (%v), waiting for build to complete\n", err)
453-
}
454-
break
437+
err = o.streamBuildLogs(newBuild)
438+
if err != nil {
439+
fmt.Fprintf(o.ErrOut, "Failed to stream the build logs - to view the logs, run oc logs build/%s\nError: %v\n", newBuild.Name, err)
455440
}
456441
}
457442

@@ -462,6 +447,32 @@ func (o *StartBuildOptions) Run() error {
462447
return nil
463448
}
464449

450+
func (o *StartBuildOptions) streamBuildLogs(build *buildapi.Build) error {
451+
opts := buildapi.BuildLogOptions{
452+
Follow: true,
453+
NoWait: false,
454+
}
455+
var err error
456+
for {
457+
rd, logErr := o.BuildLogClient.Logs(build.Name, opts).Stream()
458+
if logErr != nil {
459+
err = ocerrors.NewError("unable to stream the build logs").WithCause(logErr)
460+
glog.V(4).Infof("Error: %v", err)
461+
if o.WaitForComplete {
462+
continue
463+
}
464+
break
465+
}
466+
defer rd.Close()
467+
if _, streamErr := io.Copy(o.Out, rd); streamErr != nil {
468+
err = ocerrors.NewError("unable to stream the build logs").WithCause(streamErr)
469+
glog.V(4).Infof("Error: %v", err)
470+
}
471+
break
472+
}
473+
return err
474+
}
475+
465476
// RunListBuildWebHooks prints the webhooks for the provided build config.
466477
func (o *StartBuildOptions) RunListBuildWebHooks() error {
467478
generic, github := false, false

pkg/oc/cli/cmd/startbuild_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,18 @@ import (
1313
"strings"
1414
"testing"
1515

16+
kerrors "k8s.io/apimachinery/pkg/api/errors"
17+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
18+
"k8s.io/apimachinery/pkg/runtime/schema"
1619
restclient "k8s.io/client-go/rest"
20+
restfake "k8s.io/client-go/rest/fake"
1721
kclientcmd "k8s.io/client-go/tools/clientcmd"
1822
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
1923
"k8s.io/kubernetes/pkg/api/legacyscheme"
2024

2125
buildapi "github.com/openshift/origin/pkg/build/apis/build"
26+
buildclient "github.com/openshift/origin/pkg/build/client/internalversion"
27+
"github.com/openshift/origin/pkg/oauth/generated/clientset/scheme"
2228
)
2329

2430
type FakeClientConfig struct {
@@ -282,3 +288,97 @@ func TestHttpBinary(t *testing.T) {
282288
}
283289
}
284290
}
291+
292+
type logTestCase struct {
293+
RequestErr error
294+
IOErr error
295+
ExpectedLogMsg string
296+
ExpectedErrMsg string
297+
}
298+
299+
type failReader struct {
300+
Err error
301+
}
302+
303+
func (r *failReader) Read(p []byte) (n int, err error) {
304+
return 0, r.Err
305+
}
306+
307+
func TestStreamBuildLogs(t *testing.T) {
308+
cases := []logTestCase{
309+
{
310+
ExpectedLogMsg: "hello",
311+
},
312+
{
313+
RequestErr: errors.New("simulated failure"),
314+
ExpectedErrMsg: "unable to stream the build logs",
315+
},
316+
{
317+
RequestErr: &kerrors.StatusError{
318+
ErrStatus: metav1.Status{
319+
Reason: metav1.StatusReasonTimeout,
320+
Message: "timeout",
321+
},
322+
},
323+
ExpectedErrMsg: "unable to stream the build logs",
324+
},
325+
{
326+
IOErr: errors.New("failed to read"),
327+
ExpectedErrMsg: "unable to stream the build logs",
328+
},
329+
}
330+
331+
for _, tc := range cases {
332+
out := &bytes.Buffer{}
333+
build := &buildapi.Build{
334+
ObjectMeta: metav1.ObjectMeta{
335+
Name: "test-build",
336+
Namespace: "test-namespace",
337+
},
338+
}
339+
// Set up dummy RESTClient to handle requests
340+
fakeREST := &restfake.RESTClient{
341+
NegotiatedSerializer: scheme.Codecs,
342+
GroupVersion: schema.GroupVersion{Group: "build.openshift.io", Version: "v1"},
343+
Client: restfake.CreateHTTPClient(func(*http.Request) (*http.Response, error) {
344+
if tc.RequestErr != nil {
345+
return nil, tc.RequestErr
346+
}
347+
var body io.Reader
348+
if tc.IOErr != nil {
349+
body = &failReader{
350+
Err: tc.IOErr,
351+
}
352+
} else {
353+
body = bytes.NewBufferString(tc.ExpectedLogMsg)
354+
}
355+
return &http.Response{
356+
StatusCode: http.StatusOK,
357+
Body: ioutil.NopCloser(body),
358+
}, nil
359+
}),
360+
}
361+
362+
o := &StartBuildOptions{
363+
Out: out,
364+
ErrOut: out,
365+
BuildLogClient: buildclient.NewBuildLogClient(fakeREST, build.Namespace),
366+
}
367+
368+
err := o.streamBuildLogs(build)
369+
if tc.RequestErr == nil && tc.IOErr == nil {
370+
if err != nil {
371+
t.Errorf("received unexpected error streaming build logs: %v", err)
372+
}
373+
if out.String() != tc.ExpectedLogMsg {
374+
t.Errorf("expected log \"%s\", got \"%s\"", tc.ExpectedLogMsg, out.String())
375+
}
376+
} else {
377+
if err == nil {
378+
t.Errorf("no error was received, expected error message: %s", tc.ExpectedErrMsg)
379+
} else if !strings.Contains(err.Error(), tc.ExpectedErrMsg) {
380+
t.Errorf("expected error message \"%s\", got \"%s\"", tc.ExpectedErrMsg, err)
381+
}
382+
}
383+
}
384+
}

0 commit comments

Comments
 (0)