Repository: grafana/tanka
Pull request #1078 (2024-06-26 09:20:44)

fix: Do not hide diff error details (kubectl < 1.18.0)
Resolves https://github.com/grafana/tanka/issues/532

From what I can see this is the only place that actually does a typecheck for exec.ExitError. Because of that, wrapping the error should not be an issue.

github-actions[bot]: [PR Preview Action](https://github.com/rossjrw/pr-preview-action) v1.4.7
:---:
:rocket: Deployed preview to https://grafana.github.io/tanka/pr-preview/pr-1078/
on branch [`gh-pages`](https://github.com/grafana/tanka/tree/gh-pages) at 2024-06-26 09:21 UTC
<!-- Sticky Pull Request Commentpr-preview -->

Patch:
diff --git a/pkg/kubernetes/client/diff.go b/pkg/kubernetes/client/diff.go
index 9b6e23e7..43e40a1b 100644
--- a/pkg/kubernetes/client/diff.go
+++ b/pkg/kubernetes/client/diff.go
@@ -2,8 +2,8 @@ package client
 
 import (
 	"bytes"
+	"fmt"
 	"os"
-	"os/exec"
 	"regexp"
 	"strings"
 
@@ -12,8 +12,15 @@ import (
 	"github.com/grafana/tanka/pkg/kubernetes/manifest"
 )
 
+// Provides just the parts of `*exec.ExitError` that we need, so we can swap for a fake in the tests
+type exitError interface {
+	error
+	ExitCode() int
+}
+
 // DiffServerSide takes the desired state and computes the differences server-side, returning them in `diff(1)` format
 func (k Kubectl) DiffServerSide(data manifest.List) (*string, error) {
+	fmt.Println(data)
 	return k.diff(data, true)
 }
 
@@ -78,7 +85,7 @@ func (k Kubectl) diff(data manifest.List, serverSide bool) (*string, error) {
 // 0: no error, no differences
 // 1: error OR differences found
 func parseDiffErr(err error, stderr string, version *semver.Version) error {
-	exitErr, ok := err.(*exec.ExitError)
+	exitErr, ok := err.(exitError)
 	if !ok {
 		// this error is not kubectl related
 		return err
@@ -91,7 +98,7 @@ func parseDiffErr(err error, stderr string, version *semver.Version) error {
 
 	// before 1.18 "exit status 1" meant error as well ... so we need to check stderr
 	if version.LessThan(semver.MustParse("1.18.0")) && stderr != "" {
-		return err
+		return fmt.Errorf("diff failed: %w (%s)", err, stderr)
 	}
 
 	// differences found is not an error


Test patch:
diff --git a/pkg/kubernetes/client/diff_test.go b/pkg/kubernetes/client/diff_test.go
new file mode 100644
index 00000000..38ae744c
--- /dev/null
+++ b/pkg/kubernetes/client/diff_test.go
@@ -0,0 +1,70 @@
+package client
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/Masterminds/semver"
+	"github.com/stretchr/testify/require"
+)
+
+func TestParseDiffError(t *testing.T) {
+	tests := map[string]struct {
+		err         error
+		stderr      string
+		version     *semver.Version
+		expectedErr error
+	}{
+		// If this is not an exit error, then we just pass it through:
+		"no-exiterr": {
+			err:         fmt.Errorf("something else"),
+			stderr:      "error-details",
+			version:     semver.MustParse("1.17.0"),
+			expectedErr: fmt.Errorf("something else"),
+		},
+		// If kubectl returns with an exit code other than 1 its an indicator
+		// that it is not an internal error and so we return it as is:
+		"return-internal-as-is": {
+			err: &dummyExitError{
+				exitCode: 123,
+			},
+			stderr:      "error-details",
+			version:     semver.MustParse("1.17.0"),
+			expectedErr: fmt.Errorf("ExitError"),
+		},
+		// If kubectl is is < 1.18.0, then the error should contain then stderr
+		// content:
+		"lt-1.18.0-contains-stderr": {
+			err: &dummyExitError{
+				exitCode: 1,
+			},
+			stderr:      "error-details",
+			version:     semver.MustParse("1.17.0"),
+			expectedErr: fmt.Errorf("diff failed: ExitError (error-details)"),
+		},
+	}
+
+	for testName, test := range tests {
+		t.Run(testName, func(t *testing.T) {
+			err := parseDiffErr(test.err, test.stderr, test.version)
+			if test.expectedErr == nil {
+				require.NoError(t, err)
+				return
+			}
+			require.Error(t, err)
+			require.Equal(t, test.expectedErr.Error(), err.Error())
+		})
+	}
+}
+
+type dummyExitError struct {
+	exitCode int
+}
+
+func (e *dummyExitError) Error() string {
+	return "ExitError"
+}
+
+func (e *dummyExitError) ExitCode() int {
+	return e.exitCode
+}