End up writing this because of commands such as gclouds that emits output to stderr but exit with 0 (?????)
// ExecAndWait executes the given opts command and wait for its result.
func ExecAndWait(ctx context.Context, container *dagger.Container, description string, opts dagger.ContainerExecOpts) (*dagger.Container, error) {
opts.Args = append([]string{"/bin/sh", "-c"}, strings.Join(opts.Args, " ")+" 2>/tmp/fail.log || echo \"exit-code:$?\" >> /tmp/fail.log")
container = container.Exec(opts)
content, err := container.File("/tmp/fail.log").Contents(ctx)
if err != nil {
return container, fmt.Errorf("%v: error returned: %v content: %v", description, err, content)
}
exitCode, errLog := parseFailLog(content)
if exitCode != 0 {
return container, fmt.Errorf("%v: command failed: %v", description, fmt.Sprintf("%v", errLog))
}
return container, nil
}
// This is for some weird cases like gcloud. gcloud CLI returns output to stderr,
// though the exit code itself is 0 (means no error)
func parseFailLog(content string) (int, string) {
var (
failLog = make([]string, 0)
// if there is no exit-code recorded in the given content, we consider that as non-error
// because we only write the exit-code line when there is an error.
exitCode = 0
)
scanner := bufio.NewScanner(strings.NewReader(content))
for scanner.Scan() {
txt := scanner.Text()
if strings.Index(txt, "exit-code:") < 0 {
failLog = append(failLog, txt)
} else {
c, err := strconv.Atoi(strings.Split(txt, ":")[1])
if err != nil {
log.Fatalf("exit code value is not int: %v", err)
}
exitCode = c
break
}
}
return exitCode, strings.Join(failLog, "\n")
}