Skip to content

Commit 0e046cc

Browse files
authored
fix(k8s-client): fix error classification for failed kubeconfig ingestion (NVIDIA#1743)
Signed-off-by: Tjark Gunnar Rasche <trasche@nvidia.com>
1 parent 335a9ef commit 0e046cc

9 files changed

Lines changed: 175 additions & 20 deletions

File tree

pkg/cli/validate.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ func deployAgentForValidation(ctx context.Context, cfg *validateAgentConfig) (*s
158158
// Ensure namespace exists before deploying the agent Job.
159159
clientset, _, err := k8sclient.GetKubeClient()
160160
if err != nil {
161-
return nil, "", errors.Wrap(errors.ErrCodeInternal, "failed to create kubernetes client", err)
161+
return nil, "", errors.PropagateOrWrap(err, errors.ErrCodeInternal, "failed to create kubernetes client")
162162
}
163163
ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: cfg.namespace}}
164164
if _, nsErr := clientset.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{}); nsErr != nil {

pkg/k8s/client/client.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,11 @@ func GetKubeClient() (Interface, *rest.Config, error) {
151151
// Returns:
152152
// - *kubernetes.Clientset: The Kubernetes client
153153
// - *rest.Config: The rest configuration used to create the client
154-
// - error: Any error encountered during client creation
154+
// - error: ErrCodeInvalidRequest when a file-derived kubeconfig (the explicit
155+
// path, KUBECONFIG, or auto-discovered ~/.kube/config) cannot be loaded or
156+
// used to construct a client. These caller-input failures are deterministic
157+
// and non-retryable. ErrCodeInternal is returned when in-cluster config
158+
// discovery or in-cluster client construction fails.
155159
//
156160
// Example with custom kubeconfig:
157161
//
@@ -175,14 +179,19 @@ func BuildKubeClient(kubeconfig string) (*kubernetes.Clientset, *rest.Config, er
175179
} else {
176180
config, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
177181
if err != nil {
178-
return nil, nil, errors.WrapWithContext(errors.ErrCodeInternal, "failed to build kube config", err, map[string]interface{}{
182+
return nil, nil, errors.WrapWithContext(errors.ErrCodeInvalidRequest, "failed to build kube config", err, map[string]interface{}{
179183
"kubeconfig": kubeconfig,
180184
})
181185
}
182186
}
183187

184188
client, err := kubernetes.NewForConfig(config)
185189
if err != nil {
190+
if kubeconfig != "" {
191+
return nil, nil, errors.WrapWithContext(errors.ErrCodeInvalidRequest,
192+
"failed to create kubernetes client from kubeconfig", err,
193+
map[string]interface{}{"kubeconfig": kubeconfig})
194+
}
186195
return nil, nil, errors.Wrap(errors.ErrCodeInternal, "failed to create kubernetes client", err)
187196
}
188197

pkg/k8s/client/client_test.go

Lines changed: 89 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,32 @@
1515
package client
1616

1717
import (
18+
stderrors "errors"
1819
"os"
1920
"path/filepath"
2021
"strings"
2122
"sync"
2223
"testing"
24+
25+
"github.com/NVIDIA/aicr/pkg/errors"
2326
)
2427

28+
func assertKubeconfigErrorContext(t *testing.T, err error, wantKubeconfig string) {
29+
t.Helper()
30+
31+
var structuredErr *errors.StructuredError
32+
if !stderrors.As(err, &structuredErr) {
33+
t.Fatalf("error = %v, want *errors.StructuredError", err)
34+
}
35+
gotKubeconfig, ok := structuredErr.Context["kubeconfig"].(string)
36+
if !ok {
37+
t.Fatalf("error context kubeconfig = %v, want string", structuredErr.Context["kubeconfig"])
38+
}
39+
if gotKubeconfig != wantKubeconfig {
40+
t.Errorf("error context kubeconfig = %q, want %q", gotKubeconfig, wantKubeconfig)
41+
}
42+
}
43+
2544
// TestBuildKubeClient_PathResolution tests the kubeconfig path resolution logic
2645
// without attempting to connect to a cluster.
2746
func TestBuildKubeClient_PathResolution(t *testing.T) {
@@ -30,24 +49,30 @@ func TestBuildKubeClient_PathResolution(t *testing.T) {
3049
t.Setenv("KUBECONFIG", os.Getenv("KUBECONFIG"))
3150

3251
tests := []struct {
33-
name string
34-
kubeconfigArg string
35-
kubeconfigEnv string
36-
wantErr bool
37-
errorContains string
52+
name string
53+
kubeconfigArg string
54+
kubeconfigEnv string
55+
wantErr bool
56+
errorContains string
57+
wantCode errors.ErrorCode
58+
wantKubeconfig string
3859
}{
3960
{
40-
name: "explicit invalid path",
41-
kubeconfigArg: "/nonexistent/path/to/kubeconfig",
42-
wantErr: true,
43-
errorContains: "failed to build kube config",
61+
name: "explicit invalid path",
62+
kubeconfigArg: " /nonexistent/path/to/kubeconfig ",
63+
wantErr: true,
64+
errorContains: "failed to build kube config",
65+
wantCode: errors.ErrCodeInvalidRequest,
66+
wantKubeconfig: "/nonexistent/path/to/kubeconfig",
4467
},
4568
{
46-
name: "env var with invalid path",
47-
kubeconfigArg: "",
48-
kubeconfigEnv: "/nonexistent/env/kubeconfig",
49-
wantErr: true,
50-
errorContains: "failed to build kube config",
69+
name: "env var with invalid path",
70+
kubeconfigArg: "",
71+
kubeconfigEnv: "/nonexistent/env/kubeconfig",
72+
wantErr: true,
73+
errorContains: "failed to build kube config",
74+
wantCode: errors.ErrCodeInvalidRequest,
75+
wantKubeconfig: "/nonexistent/env/kubeconfig",
5176
},
5277
}
5378

@@ -72,6 +97,12 @@ func TestBuildKubeClient_PathResolution(t *testing.T) {
7297
t.Errorf("BuildKubeClient() error = %v, want error containing %q", err, tt.errorContains)
7398
}
7499
}
100+
if err != nil && tt.wantCode != "" && !stderrors.Is(err, errors.New(tt.wantCode, "")) {
101+
t.Errorf("BuildKubeClient() error = %v, want code %s", err, tt.wantCode)
102+
}
103+
if err != nil && tt.wantKubeconfig != "" {
104+
assertKubeconfigErrorContext(t, err, tt.wantKubeconfig)
105+
}
75106
})
76107
}
77108
}
@@ -117,6 +148,50 @@ func TestBuildKubeClient_ExplicitPath(t *testing.T) {
117148
if !strings.Contains(err.Error(), "failed to build kube config") {
118149
t.Errorf("BuildKubeClient() error = %v, want error containing 'failed to build kube config'", err)
119150
}
151+
if !stderrors.Is(err, errors.New(errors.ErrCodeInvalidRequest, "")) {
152+
t.Errorf("BuildKubeClient() error = %v, want ErrCodeInvalidRequest", err)
153+
}
154+
assertKubeconfigErrorContext(t, err, invalidConfig)
155+
}
156+
157+
// TestBuildKubeClient_InvalidClientConfigReturnsInvalidRequest verifies that a
158+
// kubeconfig which parses successfully but cannot initialize a Kubernetes
159+
// client is still classified as caller input rather than an internal failure.
160+
func TestBuildKubeClient_InvalidClientConfigReturnsInvalidRequest(t *testing.T) {
161+
kubeconfig := filepath.Join(t.TempDir(), "invalid-client-config")
162+
content := `apiVersion: v1
163+
kind: Config
164+
clusters:
165+
- name: test
166+
cluster:
167+
server: https://127.0.0.1
168+
certificate-authority-data: bm90IGEgcGVtIGNlcnRpZmljYXRl
169+
contexts:
170+
- name: test
171+
context:
172+
cluster: test
173+
user: test
174+
current-context: test
175+
users:
176+
- name: test
177+
user:
178+
token: test
179+
`
180+
if err := os.WriteFile(kubeconfig, []byte(content), 0o600); err != nil {
181+
t.Fatalf("failed to write test kubeconfig: %v", err)
182+
}
183+
184+
_, _, err := BuildKubeClient(kubeconfig)
185+
if err == nil {
186+
t.Fatal("BuildKubeClient() error = nil, want invalid request")
187+
}
188+
if !stderrors.Is(err, errors.New(errors.ErrCodeInvalidRequest, "")) {
189+
t.Errorf("BuildKubeClient() error = %v, want ErrCodeInvalidRequest", err)
190+
}
191+
if !strings.Contains(err.Error(), "failed to create kubernetes client from kubeconfig") {
192+
t.Errorf("BuildKubeClient() error = %v, want client construction failure", err)
193+
}
194+
assertKubeconfigErrorContext(t, err, kubeconfig)
120195
}
121196

122197
// TestGetKubeClient_Singleton tests that GetKubeClient returns the same instance.

pkg/serializer/configmap.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ func (w *ConfigMapWriter) Serialize(ctx context.Context, snapshot any) error {
8383
// as reader.go's ConfigMap read path.
8484
k8sClient, config, err := client.GetKubeClientWithConfig(w.kubeconfig)
8585
if err != nil {
86-
return errors.Wrap(errors.ErrCodeInternal, "failed to get kubernetes client", err)
86+
return errors.PropagateOrWrap(err, errors.ErrCodeInternal, "failed to get kubernetes client")
8787
}
8888

8989
// Log authentication context for audit

pkg/serializer/configmap_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,12 @@
1515
package serializer
1616

1717
import (
18+
stderrors "errors"
19+
"os"
20+
"path/filepath"
1821
"testing"
1922

23+
"github.com/NVIDIA/aicr/pkg/errors"
2024
"github.com/NVIDIA/aicr/pkg/k8s/pod"
2125
)
2226

@@ -191,3 +195,36 @@ func TestNewConfigMapWriter_PreservesFormatCoercion(t *testing.T) {
191195
t.Errorf("kubeconfig = %q, want empty (default discovery)", writer.kubeconfig)
192196
}
193197
}
198+
199+
func TestConfigMapWriterSerializePreservesKubeconfigErrorCode(t *testing.T) {
200+
kubeconfig := writeInvalidKubeconfig(t)
201+
writer := NewConfigMapWriterWithKubeconfig("default", "test", kubeconfig, FormatYAML)
202+
203+
err := writer.Serialize(t.Context(), map[string]string{"key": "value"})
204+
assertOutermostErrorCode(t, err, errors.ErrCodeInvalidRequest)
205+
}
206+
207+
func writeInvalidKubeconfig(t *testing.T) string {
208+
t.Helper()
209+
210+
kubeconfig := filepath.Join(t.TempDir(), "invalid-kubeconfig")
211+
if err := os.WriteFile(kubeconfig, []byte("invalid yaml content"), 0o600); err != nil {
212+
t.Fatalf("failed to write invalid kubeconfig: %v", err)
213+
}
214+
return kubeconfig
215+
}
216+
217+
func assertOutermostErrorCode(t *testing.T, err error, want errors.ErrorCode) {
218+
t.Helper()
219+
if err == nil {
220+
t.Fatalf("error = nil, want code %s", want)
221+
}
222+
223+
var structuredErr *errors.StructuredError
224+
if !stderrors.As(err, &structuredErr) {
225+
t.Fatalf("error = %v, want *errors.StructuredError", err)
226+
}
227+
if structuredErr.Code != want {
228+
t.Errorf("error code = %s, want %s", structuredErr.Code, want)
229+
}
230+
}

pkg/serializer/reader.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -506,7 +506,7 @@ func fromConfigMapWithKubeconfigContext[T any](ctx context.Context, namespace, n
506506
k8sClient, _, err = client.GetKubeClient()
507507
}
508508
if err != nil {
509-
return nil, errors.Wrap(errors.ErrCodeInternal, "failed to get kubernetes client", err)
509+
return nil, errors.PropagateOrWrap(err, errors.ErrCodeInternal, "failed to get kubernetes client")
510510
}
511511

512512
readCtx, cancel := context.WithTimeout(ctx, defaults.ConfigMapWriteTimeout)

pkg/serializer/reader_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,17 @@ func TestNewReader(t *testing.T) {
185185
})
186186
}
187187

188+
func TestFromConfigMapPreservesKubeconfigErrorCode(t *testing.T) {
189+
kubeconfig := writeInvalidKubeconfig(t)
190+
191+
_, err := FromFileWithKubeconfigContext[testConfig](
192+
t.Context(),
193+
"cm://default/test",
194+
kubeconfig,
195+
)
196+
assertOutermostErrorCode(t, err, errors.ErrCodeInvalidRequest)
197+
}
198+
188199
func TestReader_DeserializeJSON(t *testing.T) {
189200
t.Run("valid json object", func(t *testing.T) {
190201
jsonData := `{"name":"test","value":123}`

pkg/snapshotter/agent.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ func getKubeClient(kubeconfig string) (k8sclient.Interface, error) {
296296
clientset, _, err = k8sclient.GetKubeClient()
297297
}
298298
if err != nil {
299-
return nil, errors.Wrap(errors.ErrCodeInternal, "failed to create Kubernetes client", err)
299+
return nil, errors.PropagateOrWrap(err, errors.ErrCodeInternal, "failed to create Kubernetes client")
300300
}
301301
return clientset, nil
302302
}

pkg/snapshotter/agent_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@
1515
package snapshotter
1616

1717
import (
18+
stderrors "errors"
1819
"os"
20+
"path/filepath"
1921
"testing"
2022

23+
"github.com/NVIDIA/aicr/pkg/errors"
2124
corev1 "k8s.io/api/core/v1"
2225
)
2326

@@ -65,6 +68,26 @@ func TestAgentConfig_Defaults(t *testing.T) {
6568
}
6669
}
6770

71+
func TestGetKubeClientPreservesKubeconfigErrorCode(t *testing.T) {
72+
kubeconfig := filepath.Join(t.TempDir(), "invalid-kubeconfig")
73+
if err := os.WriteFile(kubeconfig, []byte("invalid yaml content"), 0o600); err != nil {
74+
t.Fatalf("failed to write invalid kubeconfig: %v", err)
75+
}
76+
77+
_, err := getKubeClient(kubeconfig)
78+
if err == nil {
79+
t.Fatal("getKubeClient() error = nil, want ErrCodeInvalidRequest")
80+
}
81+
82+
var structuredErr *errors.StructuredError
83+
if !stderrors.As(err, &structuredErr) {
84+
t.Fatalf("getKubeClient() error = %v, want *errors.StructuredError", err)
85+
}
86+
if structuredErr.Code != errors.ErrCodeInvalidRequest {
87+
t.Errorf("getKubeClient() error code = %s, want %s", structuredErr.Code, errors.ErrCodeInvalidRequest)
88+
}
89+
}
90+
6891
func TestParseNodeSelectors(t *testing.T) {
6992
tests := []struct {
7093
name string

0 commit comments

Comments
 (0)