#How to grab the actual err msg with ExitCode

1 messages · Page 1 of 1 (latest)

subtle flower
#

Hello team.
I'm calling ExitCode to validate the execution result of the last command, but the error message seems not quite useful.

2022/11/08 15:04:08 build error: unit test execution: input:1: container.from.withMountedDirectory.withWorkdir.exec.exitCode process "/_shim xxx test ./..." did not complete successfully: exit code: 1

In this case, I expect something like xxx: command not found. What would be the best way to grab the entire stderr output of the last cmd run by Exec?

#

I actually tried this, but got only the following:

updatedContainer := container.Exec(opts)
resultCode, err := updatedContainer.ExitCode(ctx)
if err != nil || resultCode != 0 {
    updatedContainer.Stderr().Export(ctx, "stderr.log")
    contents, _ := ioutil.ReadFile("stderr.log")
    if contents != nil {
        fmt.Printf("XXXX: %s\n", string(contents))
    }

    return updatedContainer, fmt.Errorf("%v: %v", description, cond.IfNilError(err, fmt.Sprintf("non-zero result(%v)", resultCode)))
}
return updatedContainer, nil

Result: XXXX: ---

#

It feels like only the first line of the entire output was grabbed, but I'm not sure

subtle flower
#

I tested again with this:

container = container.Exec(opts)
resultCode, err := container.ExitCode(ctx)
if err != nil {
    if resultCode == 0 {
        return container, fmt.Errorf("%v: %v", description, fmt.Sprintf("no such command error: %v", err))
    }

    return container, fmt.Errorf("%v: %v", description, err)
}

return container, nil

Interestingly, the resultCode value was always zero, even though still I see this error message that the actual exit code of the previous command is nonzero.

2022/11/08 16:48:47 build error: unit test execution: no such command error: input:1: container.from.withMountedDirectory.withWorkdir.exec.exitCode process "/_shim cat test" did not complete successfully: exit code: 1
spring bloom
#

Unfortunately, there's no current way to fetch the stderr if the command fails through the API 😅 until that issue is resolved

#

you can make some hacks to make it work like trapping the exit code through Exec and then returning that as an output but it's not very friendly

subtle flower
#

Anyway, good to know! At least I know you guys are discussing the issue. Thank you for the suggestion!

lost finch
#

I’m also doing something similar

spring bloom
subtle flower
#

Thank you for the hack and I agree that this is becoming important because basically I can't see why the command failed without applying the workaround.

subtle flower
#

I'm not sure even that workaround works. I tried the following:

func ExecAndWait(ctx context.Context, container *dagger.Container, description string, opts dagger.ContainerExecOpts) (*dagger.Container, error) {
    opts.Args = append(append([]string{"/bin/sh", "-c"}, opts.Args...), ">/dev/null", "2>/tmp/stderr.log", "||true")
    container = container.Exec(opts)

    content, err := container.File("/tmp/stderr.log").Contents(ctx)
    if err != nil {
        return container, fmt.Errorf("%v: error returned: %v content: %v", description, err, content)
    }
    if len(content) > 0 {
        return container, fmt.Errorf("%v: stderr returned: %v", description, fmt.Sprintf("%v", content))
    }

    return container, nil

}

In the terminal prompt, gx test x >/dev/null 2>/tmp/stderr.log ||true exits with 0 as expected. But in the compiled dagger runtime, /_shim /bin/sh -c gx test x >/dev/null 2>/tmp/stderr.log ||true exits with 127, that is no such command like gx. At this point, I suspect that there is no way that I can force my command exit with 0 (even with ||true). Maybe is this because of the /_shim thing?

spring bloom
#

no such command like gx makes me think that the gx binary effectively doesn't exist in the container?

#

FWIW this works:

    stderr, err := c.Container().From("alpine").
        Exec(dagger.ContainerExecOpts{
            Args: []string{"sh", "-c", "notfound || true"},
        }).ExitCode(ctx)
    if err != nil {
        panic(err)
    }

    fmt.Printf("%#v", stderr)
#

that forces the execution to return a 0 status code even though the command doesn't exit

#

@subtle flower can you try sending all the arguments of the "sh -c" in a single slice element? I think that's your issue

#

since the -c flag of sh expects the command to be in a single argument string

subtle flower
#

Thank you so much! Updated as follows and it worked - I could successfully grab the error message of the failed cmd

// 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)
    }
    if len(content) > 0 {
        return container, fmt.Errorf("%v: command failed: %v", description, fmt.Sprintf("%v", content))
    }

    return container, nil
}
subtle flower
#

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")
}
coarse forge
#

While not as robust as the hack/workaround mentioned above (thanks for that, gave me the clue to do this)!

This quick and simple hack in the Python SDK was at least able to get me to debug the error for gcloud issues:

.exec(["/bin/bash", "-c", "gcloud auth list>>/tmp/gcloud.log && cat /tmp/gcloud.log"])

past fjord
#

I think I have the same issue, I try to access to the error message without success 😢
I have did this:

if _, err := container.ExitCode(ctx); err == nil {
    return nil
}

detail, err := container.Stderr().Contents(ctx)
if err != nil {
    return err
}

fmt.Println("markdown error:\n", detail)

But the output is:

markdown lint: input:1: container.from.withMountedDirectory.withWorkdir.exec.stderr.contents process "/_shim /home/nonroot/entrypoint.sh README.md **/*.md" did not complete successfully: exit code: 1

regal night
#

the only ugly trick is to make sure your command always succeeds, here is an example:

#
func main() {
    ctx := context.Background()
    client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stdout))
    if err != nil {
        panic(err)
    }

    alpine := client.Container().From("alpine:3.16")
    c := alpine.Exec(dagger.ContainerExecOpts{
        Args: []string{"sh", "-c", "cat /not-exist 2> /errors || true"},
    })
    content, err := c.File("/errors").Contents(ctx)
    if err != nil {
        // file does not exist, the command exited properly
    }
    fmt.Printf("Error messages: %q\n", content)
}
#

And same code with Stderr instead of using a file:

func main() {
    ctx := context.Background()
    client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stdout))
    if err != nil {
        panic(err)
    }

    alpine := client.Container().From("alpine:3.16")
    c := alpine.Exec(dagger.ContainerExecOpts{
        Args: []string{"sh", "-c", "cat /not-exist || true"},
    })
    content, err := c.Stderr().Contents(ctx)
    if err != nil {
        // file does not exist, the command exited properly
    }
    fmt.Printf("Error messages: %q\n", content)
}

(same idea but without a temp file)

past fjord
#

@regal night I see the principle, but I can't apply it. I saw that markdown lint offers to generate a file with errors. That's what I tried to do:

args := []string{"--output", "/tmp/errors", "--quiet"}
if cfg.json {
    args = append(args, "--json")
}
container = container.Exec(dagger.ContainerExecOpts{
    Args: append(args, append(cfg.files, `|| true`)...),
})

content, err := container.File("/tmp/errors").Contents(ctx)
if err != nil {
    return fmt.Errorf("error returned: %v content: %v", err, content)
}
if len(content) > 0 {
    return fmt.Errorf("command failed: %v", fmt.Sprintf("%v", content))
}

When I execute the code, I have my errors:

error returned: input:1: container.from.withMountedDirectory.withWorkdir.exec.file process "/_shim /home/nonroot/entrypoint.sh --output /tmp/errors --quiet README.md **/*.md || true" did not complete successfully: exit code: 1
But when I try to do the same directly in an container it works:

docker run -ti --rm -v $(pwd):/src -w /src --entrypoint sh tmknom/markdownlint
/src $ ^C
/src $ /home/nonroot/entrypoint.sh --output /tmp/errors --quiet README.md **/*.md || true
/src $ echo $?
0
/src $ head -n 5 /tmp/errors
golang/README.md:19 MD012/no-multiple-blanks Multiple consecutive blank lines [Expected: 1; Actual: 2]
golang/README.md:23:81 MD013/line-length Line length [Expected: 80; Actual: 120]
golang/README.md:28:1 MD033/no-inline-html Inline HTML [Element: details]
golang/README.md:28:10 MD033/no-inline-html Inline HTML [Element: summary]
golang/README.md:29:1 MD033/no-inline-html Inline HTML [Element: p]
/src $ exit
spring bloom
#

@past fjord you can't use || true in your exec opt because that's a shell construct and not part of the exec args of the entrypoint.

This should be the equivalent in dagger of what you're trying to achieve:

func main() {
    ctx := context.Background()
    c, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
    if err != nil {
        panic(err)
    }

    contents, err := c.Container().From("tmknom/markdownlint").
        WithMountedDirectory("/src", c.Host().Workdir()).
        WithWorkdir("/src").
        Exec(dagger.ContainerExecOpts{
            Args: []string{"sh", "-c", "/home/nonroot/entrypoint.sh --output /tmp/errors --quiet readme.md || true"},
        }).
        File("/tmp/errors").Contents(ctx)
    if err != nil {
        panic(err)
    }
    fmt.Println(contents)
}
past fjord
#

@spring bloom thx for the work around

spring bloom
#

🚀 happy to help!