Skip to content

Commit fb34de0

Browse files
committed
Capture stdout and stderr even if cmd.Run fails
In its current form, if `cmd.Run` fails, then the function immediately exits with no reason given for the termination. This makes debugging scripts hard to understand as no indication is given for the failure, other than a failure occured. By trapping the error returned from `cmd.Run`, we first process the buffers for both `stdout` and `stderr` and set these to the `dxr`. Additionally we capture a truncated `stderr` from the command error which can help point to where the command failed, setting this as the Fatal error message returned as an event.
1 parent a905f7e commit fb34de0

File tree

2 files changed

+79
-15
lines changed

2 files changed

+79
-15
lines changed

fn.go

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package main
33
import (
44
"bytes"
55
"context"
6+
"fmt"
7+
"os/exec"
68
"strings"
79

810
"github.com/crossplane-contrib/function-shell/input/v1alpha1"
@@ -84,7 +86,7 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1beta1.RunFunctionRequ
8486
shellCmd = in.ShellCommandField
8587
}
8688

87-
var shellEnvVars = make(map[string]string)
89+
shellEnvVars := make(map[string]string)
8890
for _, envVar := range in.ShellEnvVars {
8991
if envVar.ValueRef != "" {
9092
envValue, err := fromValueRef(req, envVar.ValueRef)
@@ -107,7 +109,7 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1beta1.RunFunctionRequ
107109
}
108110

109111
var exportCmds string
110-
//exportCmds = "export PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin;"
112+
// exportCmds = "export PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin;"
111113
for k, v := range shellEnvVars {
112114
exportCmds = exportCmds + "export " + k + "=\"" + v + "\";"
113115
}
@@ -118,25 +120,30 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1beta1.RunFunctionRequ
118120
cmd := shell.Commandf(exportCmds + shellCmd)
119121
cmd.Stdout = &stdout
120122
cmd.Stderr = &stderr
121-
err = cmd.Run()
122-
if err != nil {
123-
response.Fatal(rsp, errors.Wrapf(err, "shellCmd %s for %s failed", shellCmd, oxr.Resource.GetKind()))
124-
return rsp, nil
125-
}
123+
124+
cmderr := cmd.Run()
126125
out := strings.TrimSpace(stdout.String())
127126
err = dxr.Resource.SetValue(stdoutField, out)
128127
if err != nil {
129128
response.Fatal(rsp, errors.Wrapf(err, "cannot set field %s to %s for %s", stdoutField, out, oxr.Resource.GetKind()))
130129
return rsp, nil
131130
}
132-
err = dxr.Resource.SetValue(stderrField, strings.TrimSpace(stderr.String()))
131+
132+
serr := strings.TrimSpace(stderr.String())
133+
err = dxr.Resource.SetValue(stderrField, serr)
133134
if err != nil {
134-
response.Fatal(rsp, errors.Wrapf(err, "cannot set field %s to %s for %s", stderrField, out, oxr.Resource.GetKind()))
135-
return rsp, nil
135+
response.Fatal(rsp, errors.Wrapf(err, "cannot set field %s to %s for %s", stderrField, serr, oxr.Resource.GetKind()))
136136
}
137137
if err := response.SetDesiredCompositeResource(rsp, dxr); err != nil {
138138
response.Fatal(rsp, errors.Wrapf(err, "cannot set desired composite resources from %T", req))
139-
return rsp, nil
139+
}
140+
141+
if cmderr != nil {
142+
if exiterr, ok := cmderr.(*exec.ExitError); ok {
143+
msg := fmt.Sprintf("shellCmd %q for %q failed with %s", shellCmd, oxr.Resource.GetKind(), exiterr.Stderr)
144+
response.Fatal(rsp, errors.Wrap(cmderr, msg))
145+
return rsp, nil
146+
}
140147
}
141148

142149
return rsp, nil

fn_test.go

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import (
1616
)
1717

1818
func TestRunFunction(t *testing.T) {
19-
2019
type args struct {
2120
ctx context.Context
2221
req *fnv1beta1.RunFunctionRequest
@@ -119,6 +118,48 @@ func TestRunFunction(t *testing.T) {
119118
},
120119
},
121120
},
121+
"ResponseIsErrorIfInvalidShellCommand": {
122+
reason: "The function should write to the specified stderr when the shell command is invalid",
123+
args: args{
124+
req: &fnv1beta1.RunFunctionRequest{
125+
Meta: &fnv1beta1.RequestMeta{Tag: "hello"},
126+
Input: resource.MustStructJSON(`{
127+
"apiVersion": "template.fn.crossplane.io/v1alpha1",
128+
"kind": "Parameters",
129+
"shellCommand": "set -euo pìpefail",
130+
"stdoutField": "status.atFunction.shell.stdout",
131+
"stderrField": "status.atFunction.shell.stderr"
132+
}`),
133+
},
134+
},
135+
want: want{
136+
rsp: &fnv1beta1.RunFunctionResponse{
137+
Meta: &fnv1beta1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)},
138+
Desired: &fnv1beta1.State{
139+
Composite: &fnv1beta1.Resource{
140+
Resource: resource.MustStructJSON(`{
141+
"apiVersion": "",
142+
"kind": "",
143+
"status": {
144+
"atFunction": {
145+
"shell": {
146+
"stdout": "",
147+
"stderr": "/bin/sh: 1: set: Illegal option -o pìpefail"
148+
}
149+
}
150+
}
151+
}`),
152+
},
153+
},
154+
Results: []*fnv1beta1.Result{
155+
{
156+
Severity: fnv1beta1.Severity_SEVERITY_FATAL,
157+
Message: "shellCmd \"set -euo pìpefail\" for \"\" failed with : exit status 2",
158+
},
159+
},
160+
},
161+
},
162+
},
122163
"ResponseIsErrorWhenShellCommandNotFound": {
123164
reason: "The Function should write to the specified stderr field when the shellCommand is not found",
124165
args: args{
@@ -128,18 +169,34 @@ func TestRunFunction(t *testing.T) {
128169
"apiVersion": "template.fn.crossplane.io/v1alpha1",
129170
"kind": "Parameters",
130171
"shellCommand": "unkown-shell-command",
131-
"stdoutField": "spec.atFunction.shell.stdout",
132-
"stderrField": "spec.atFunction.shell.stderr"
172+
"stdoutField": "status.atFunction.shell.stdout",
173+
"stderrField": "status.atFunction.shell.stderr"
133174
}`),
134175
},
135176
},
136177
want: want{
137178
rsp: &fnv1beta1.RunFunctionResponse{
138179
Meta: &fnv1beta1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)},
180+
Desired: &fnv1beta1.State{
181+
Composite: &fnv1beta1.Resource{
182+
Resource: resource.MustStructJSON(`{
183+
"apiVersion": "",
184+
"kind": "",
185+
"status": {
186+
"atFunction": {
187+
"shell": {
188+
"stdout": "",
189+
"stderr": "/bin/sh: 1: unkown-shell-command: not found"
190+
}
191+
}
192+
}
193+
}`),
194+
},
195+
},
139196
Results: []*fnv1beta1.Result{
140197
{
141198
Severity: fnv1beta1.Severity_SEVERITY_FATAL,
142-
Message: "shellCmd unkown-shell-command for failed: exit status 127",
199+
Message: "shellCmd \"unkown-shell-command\" for \"\" failed with : exit status 127",
143200
},
144201
},
145202
},

0 commit comments

Comments
 (0)