Skip to content

Commit 7c43fba

Browse files
vdemeestertekton-robot
authored andcommitted
entrypoint: in case of step command failure, write postfile
The entrypoint package wraps the step commands and execute them. This allows use to use pods containers with some order. In a step, the entrypoint binary will wait for the file of the previous step to be present to execute the actual command. Before this change, if a command failed (`exit 1` or something), entrypoint would not write a file, and thus the whole pod would be stuck running (all the next step would wait forever). This fixes that by always writing the post-file — and making the *waiter* a bit smarter : - it will now look for a `{postfile}.err` to detect if the previous step failed or not. - if the previous steps failed, it will fail too without executing the step commands. Signed-off-by: Vincent Demeester <vdemeest@redhat.com>
1 parent 77ad395 commit 7c43fba

File tree

4 files changed

+271
-31
lines changed

4 files changed

+271
-31
lines changed

cmd/entrypoint/main.go

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package main
1818

1919
import (
2020
"flag"
21+
"fmt"
2122
"log"
2223
"os"
2324
"os/exec"
@@ -36,15 +37,34 @@ var (
3637
func main() {
3738
flag.Parse()
3839

39-
entrypoint.Entrypointer{
40+
e := entrypoint.Entrypointer{
4041
Entrypoint: *ep,
4142
WaitFile: *waitFile,
4243
PostFile: *postFile,
4344
Args: flag.Args(),
4445
Waiter: &RealWaiter{},
4546
Runner: &RealRunner{},
4647
PostWriter: &RealPostWriter{},
47-
}.Go()
48+
}
49+
if err := e.Go(); err != nil {
50+
switch err.(type) {
51+
case skipError:
52+
os.Exit(0)
53+
case *exec.ExitError:
54+
// Copied from https://stackoverflow.com/questions/10385551/get-exit-code-go
55+
// This works on both Unix and Windows. Although
56+
// package syscall is generally platform dependent,
57+
// WaitStatus is defined for both Unix and Windows and
58+
// in both cases has an ExitStatus() method with the
59+
// same signature.
60+
if status, ok := err.(*exec.ExitError).Sys().(syscall.WaitStatus); ok {
61+
os.Exit(status.ExitStatus())
62+
}
63+
log.Fatalf("Error executing command (ExitError): %v", err)
64+
default:
65+
log.Fatalf("Error executing command: %v", err)
66+
}
67+
}
4868
}
4969

5070
// TODO(jasonhall): Test that original exit code is propagated and that
@@ -55,15 +75,20 @@ type RealWaiter struct{ waitFile string }
5575

5676
var _ entrypoint.Waiter = (*RealWaiter)(nil)
5777

58-
func (*RealWaiter) Wait(file string) {
78+
func (*RealWaiter) Wait(file string) error {
5979
if file == "" {
60-
return
80+
return nil
6181
}
6282
for ; ; time.Sleep(time.Second) {
83+
// Watch for the post file
6384
if _, err := os.Stat(file); err == nil {
64-
return
85+
return nil
6586
} else if !os.IsNotExist(err) {
66-
log.Fatalf("Waiting for %q: %v", file, err)
87+
return fmt.Errorf("Waiting for %q: %v", file, err)
88+
}
89+
// Watch for the post error file
90+
if _, err := os.Stat(file + ".err"); err == nil {
91+
return skipError("error file present, bail and skip the step")
6792
}
6893
}
6994
}
@@ -73,9 +98,9 @@ type RealRunner struct{}
7398

7499
var _ entrypoint.Runner = (*RealRunner)(nil)
75100

76-
func (*RealRunner) Run(args ...string) {
101+
func (*RealRunner) Run(args ...string) error {
77102
if len(args) == 0 {
78-
return
103+
return nil
79104
}
80105
name, args := args[0], args[1:]
81106

@@ -84,20 +109,9 @@ func (*RealRunner) Run(args ...string) {
84109
cmd.Stderr = os.Stderr
85110

86111
if err := cmd.Run(); err != nil {
87-
if exiterr, ok := err.(*exec.ExitError); ok {
88-
// Copied from https://stackoverflow.com/questions/10385551/get-exit-code-go
89-
// This works on both Unix and Windows. Although
90-
// package syscall is generally platform dependent,
91-
// WaitStatus is defined for both Unix and Windows and
92-
// in both cases has an ExitStatus() method with the
93-
// same signature.
94-
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
95-
os.Exit(status.ExitStatus())
96-
}
97-
log.Fatalf("Error executing command (ExitError): %v", err)
98-
}
99-
log.Fatalf("Error executing command: %v", err)
112+
return err
100113
}
114+
return nil
101115
}
102116

103117
// RealPostWriter actually writes files.
@@ -113,3 +127,9 @@ func (*RealPostWriter) Write(file string) {
113127
log.Fatalf("Creating %q: %v", file, err)
114128
}
115129
}
130+
131+
type skipError string
132+
133+
func (e skipError) Error() string {
134+
return string(e)
135+
}

pkg/entrypoint/entrypointer.go

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ limitations under the License.
1616

1717
package entrypoint
1818

19+
import (
20+
"fmt"
21+
)
22+
1923
// Entrypointer holds fields for running commands with redirected
2024
// entrypoints.
2125
type Entrypointer struct {
@@ -41,12 +45,12 @@ type Entrypointer struct {
4145
// Waiter encapsulates waiting for files to exist.
4246
type Waiter interface {
4347
// Wait blocks until the specified file exists.
44-
Wait(file string)
48+
Wait(file string) error
4549
}
4650

4751
// Runner encapsulates running commands.
4852
type Runner interface {
49-
Run(args ...string)
53+
Run(args ...string) error
5054
}
5155

5256
// PostWriter encapsulates writing a file when complete.
@@ -57,17 +61,33 @@ type PostWriter interface {
5761

5862
// Go optionally waits for a file, runs the command, and writes a
5963
// post file.
60-
func (e Entrypointer) Go() {
64+
func (e Entrypointer) Go() error {
6165
if e.WaitFile != "" {
62-
e.Waiter.Wait(e.WaitFile)
66+
if err := e.Waiter.Wait(e.WaitFile); err != nil {
67+
// An error happened while waiting, so we bail
68+
// *but* we write postfile to make next steps bail too
69+
e.WritePostFile(e.PostFile, err)
70+
return err
71+
}
6372
}
6473

6574
if e.Entrypoint != "" {
6675
e.Args = append([]string{e.Entrypoint}, e.Args...)
6776
}
68-
e.Runner.Run(e.Args...)
6977

70-
if e.PostFile != "" {
71-
e.PostWriter.Write(e.PostFile)
78+
err := e.Runner.Run(e.Args...)
79+
80+
// Write the post file *no matter what*
81+
e.WritePostFile(e.PostFile, err)
82+
83+
return err
84+
}
85+
86+
func (e Entrypointer) WritePostFile(postFile string, err error) {
87+
if err != nil && postFile != "" {
88+
postFile = fmt.Sprintf("%s.err", postFile)
89+
}
90+
if postFile != "" {
91+
e.PostWriter.Write(postFile)
7292
}
7393
}

pkg/entrypoint/entrypoint_test.go renamed to pkg/entrypoint/entrypointer_test.go

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,80 @@ limitations under the License.
1717
package entrypoint
1818

1919
import (
20+
"fmt"
2021
"reflect"
2122
"testing"
23+
24+
"github.com/google/go-cmp/cmp"
2225
)
2326

27+
func TestEntrypointerFailures(t *testing.T) {
28+
for _, c := range []struct {
29+
desc, waitFile, postFile string
30+
waiter Waiter
31+
runner Runner
32+
expectedError string
33+
}{{
34+
desc: "failing runner with no postFile",
35+
runner: &fakeErrorRunner{},
36+
expectedError: "runner failed",
37+
}, {
38+
desc: "failing runner with postFile",
39+
runner: &fakeErrorRunner{},
40+
expectedError: "runner failed",
41+
postFile: "foo",
42+
}, {
43+
desc: "failing waiter with no postFile",
44+
waitFile: "foo",
45+
waiter: &fakeErrorWaiter{},
46+
expectedError: "waiter failed",
47+
}, {
48+
desc: "failing waiter with postFile",
49+
waitFile: "foo",
50+
waiter: &fakeErrorWaiter{},
51+
expectedError: "waiter failed",
52+
postFile: "bar",
53+
}} {
54+
t.Run(c.desc, func(t *testing.T) {
55+
fw := c.waiter
56+
if fw == nil {
57+
fw = &fakeWaiter{}
58+
}
59+
fr := c.runner
60+
if fr == nil {
61+
fr = &fakeRunner{}
62+
}
63+
fpw := &fakePostWriter{}
64+
err := Entrypointer{
65+
Entrypoint: "echo",
66+
WaitFile: c.waitFile,
67+
PostFile: c.postFile,
68+
Args: []string{"some", "args"},
69+
Waiter: fw,
70+
Runner: fr,
71+
PostWriter: fpw,
72+
}.Go()
73+
if err == nil {
74+
t.Fatalf("Entrpointer didn't fail")
75+
}
76+
if d := cmp.Diff(c.expectedError, err.Error()); d != "" {
77+
t.Errorf("Entrypointer error diff -want, +got: %v", d)
78+
}
79+
80+
if c.postFile != "" {
81+
if fpw.wrote == nil {
82+
t.Error("Wanted post file written, got nil")
83+
} else if *fpw.wrote != c.postFile+".err" {
84+
t.Errorf("Wrote post file %q, want %q", *fpw.wrote, c.postFile)
85+
}
86+
}
87+
if c.postFile == "" && fpw.wrote != nil {
88+
t.Errorf("Wrote post file when not required")
89+
}
90+
})
91+
}
92+
}
93+
2494
func TestEntrypointer(t *testing.T) {
2595
for _, c := range []struct {
2696
desc, entrypoint, waitFile, postFile string
@@ -50,7 +120,7 @@ func TestEntrypointer(t *testing.T) {
50120
}} {
51121
t.Run(c.desc, func(t *testing.T) {
52122
fw, fr, fpw := &fakeWaiter{}, &fakeRunner{}, &fakePostWriter{}
53-
Entrypointer{
123+
err := Entrypointer{
54124
Entrypoint: c.entrypoint,
55125
WaitFile: c.waitFile,
56126
PostFile: c.postFile,
@@ -59,6 +129,9 @@ func TestEntrypointer(t *testing.T) {
59129
Runner: fr,
60130
PostWriter: fpw,
61131
}.Go()
132+
if err != nil {
133+
t.Fatalf("Entrypointer failed: %v", err)
134+
}
62135

63136
if c.waitFile != "" {
64137
if fw.waited == nil {
@@ -102,12 +175,32 @@ func TestEntrypointer(t *testing.T) {
102175

103176
type fakeWaiter struct{ waited *string }
104177

105-
func (f *fakeWaiter) Wait(file string) { f.waited = &file }
178+
func (f *fakeWaiter) Wait(file string) error {
179+
f.waited = &file
180+
return nil
181+
}
106182

107183
type fakeRunner struct{ args *[]string }
108184

109-
func (f *fakeRunner) Run(args ...string) { f.args = &args }
185+
func (f *fakeRunner) Run(args ...string) error {
186+
f.args = &args
187+
return nil
188+
}
110189

111190
type fakePostWriter struct{ wrote *string }
112191

113192
func (f *fakePostWriter) Write(file string) { f.wrote = &file }
193+
194+
type fakeErrorWaiter struct{ waited *string }
195+
196+
func (f *fakeErrorWaiter) Wait(file string) error {
197+
f.waited = &file
198+
return fmt.Errorf("waiter failed")
199+
}
200+
201+
type fakeErrorRunner struct{ args *[]string }
202+
203+
func (f *fakeErrorRunner) Run(args ...string) error {
204+
f.args = &args
205+
return fmt.Errorf("runner failed")
206+
}

0 commit comments

Comments
 (0)