Skip to content

Commit d6927ff

Browse files
committed
unit tests for auth resolution + local.yaml invariants
pkg/k8s/client_test.go: - URLResolution_TrailingSlashNormalized: target with a trailing slash still matches. - URLResolution_MultipleMatches_ErrorListsContexts: the ambiguity error names the conflicting contexts + 'disambiguate'. - ResolveKubeconfigAuth_NoteOn{Active,NonActive}Match: the 'Using kubeconfig context <name>' note is printed on a single match (os.Stderr captured). pkg/functions/function_unit_test.go: - LocalAuth_LocalYAMLMode0600 + TightensPreexisting0644: credential file is 0600, incl. re-tightening a pre-existing 0644 file. - LocalAuth_NotInFuncYAML: credentials never serialize into func.yaml; the cluster URL does.
1 parent 7ea4500 commit d6927ff

2 files changed

Lines changed: 187 additions & 0 deletions

File tree

pkg/functions/function_unit_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"os"
66
"path/filepath"
77
"reflect"
8+
"runtime"
89
"strings"
910
"testing"
1011

@@ -459,3 +460,95 @@ func TestLocalAuth_MultipleEntries(t *testing.T) {
459460
t.Fatal("cluster-b auth not found or wrong")
460461
}
461462
}
463+
464+
// TestLocalAuth_LocalYAMLMode0600 ensures the credential file is written 0600.
465+
func TestLocalAuth_LocalYAMLMode0600(t *testing.T) {
466+
if runtime.GOOS == "windows" {
467+
t.Skip("file mode bits are not meaningful on Windows")
468+
}
469+
root := FromTempDirectory(t)
470+
f, err := fn.New().Init(fn.Function{Runtime: "go", Root: root})
471+
if err != nil {
472+
t.Fatal(err)
473+
}
474+
f.Local.SetAuth("https://cluster.example.com:6443", fn.ClusterTLS{}, fn.UserAuth{Token: "secret"})
475+
if err := f.Write(); err != nil {
476+
t.Fatal(err)
477+
}
478+
info, err := os.Stat(filepath.Join(root, ".func", "local.yaml"))
479+
if err != nil {
480+
t.Fatal(err)
481+
}
482+
if perm := info.Mode().Perm(); perm != 0o600 {
483+
t.Fatalf("local.yaml mode = %o, want 0600", perm)
484+
}
485+
}
486+
487+
// TestLocalAuth_TightensPreexisting0644 ensures a pre-existing, loosened
488+
// local.yaml is re-tightened to 0600 on every Write -- unconditionally, even
489+
// when the rewrite carries no credentials (os.WriteFile does not change the
490+
// mode of an already-existing file). local.yaml is private machine-local
491+
// state and must never be left group/world-readable.
492+
func TestLocalAuth_TightensPreexisting0644(t *testing.T) {
493+
if runtime.GOOS == "windows" {
494+
t.Skip("file mode bits are not meaningful on Windows")
495+
}
496+
root := FromTempDirectory(t)
497+
f, err := fn.New().Init(fn.Function{Runtime: "go", Root: root})
498+
if err != nil {
499+
t.Fatal(err)
500+
}
501+
// Write a local.yaml, then loosen it as an older func (or a stray umask)
502+
// would have.
503+
if err := f.Write(); err != nil {
504+
t.Fatal(err)
505+
}
506+
localPath := filepath.Join(root, ".func", "local.yaml")
507+
if err := os.Chmod(localPath, 0o644); err != nil {
508+
t.Fatal(err)
509+
}
510+
// Reload and write again WITHOUT any credentials -> must still be
511+
// re-tightened to 0600. The invariant holds regardless of auth content.
512+
// (A non-auth change is needed to force a write, since Write() no-ops when
513+
// the function is unmodified.)
514+
if f, err = fn.NewFunction(root); err != nil {
515+
t.Fatal(err)
516+
}
517+
f.Local.Remote = true
518+
if err := f.Write(); err != nil {
519+
t.Fatal(err)
520+
}
521+
info, err := os.Stat(localPath)
522+
if err != nil {
523+
t.Fatal(err)
524+
}
525+
if perm := info.Mode().Perm(); perm != 0o600 {
526+
t.Fatalf("pre-existing local.yaml not tightened: mode = %o, want 0600", perm)
527+
}
528+
}
529+
530+
// TestLocalAuth_NotInFuncYAML ensures credentials never leak into the
531+
// source-controlled func.yaml, while the (non-secret) cluster URL is recorded.
532+
func TestLocalAuth_NotInFuncYAML(t *testing.T) {
533+
root := FromTempDirectory(t)
534+
f := fn.Function{Runtime: "go", Root: root,
535+
Deploy: fn.DeploySpec{Cluster: "https://cluster.example.com:6443"}}
536+
f, err := fn.New().Init(f)
537+
if err != nil {
538+
t.Fatal(err)
539+
}
540+
f.Local.SetAuth("https://cluster.example.com:6443", fn.ClusterTLS{}, fn.UserAuth{Token: "super-secret-token"})
541+
if err := f.Write(); err != nil {
542+
t.Fatal(err)
543+
}
544+
raw, err := os.ReadFile(filepath.Join(root, "func.yaml"))
545+
if err != nil {
546+
t.Fatal(err)
547+
}
548+
if strings.Contains(string(raw), "super-secret-token") {
549+
t.Fatalf("func.yaml leaked the stored token:\n%s", raw)
550+
}
551+
if !strings.Contains(string(raw), "https://cluster.example.com:6443") {
552+
t.Fatalf("func.yaml should record the (non-secret) cluster URL:\n%s", raw)
553+
}
554+
}

pkg/k8s/client_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package k8s
22

33
import (
44
"encoding/base64"
5+
"io"
56
"os"
67
"path/filepath"
78
"testing"
@@ -513,6 +514,99 @@ func TestBuildClientConfig_URLResolution_NoMatch(t *testing.T) {
513514
}
514515
}
515516

517+
func TestBuildClientConfig_URLResolution_TrailingSlashNormalized(t *testing.T) {
518+
writeTestKubeconfig(t, multiContextKubeconfig())
519+
520+
// target has a trailing slash; the kubeconfig server does not -> must match.
521+
cc, err := BuildClientConfig("https://cluster-a.example.com:6443/", "", "", fn.Local{})
522+
if err != nil {
523+
t.Fatalf("trailing-slash target failed to resolve: %v", err)
524+
}
525+
cfg, err := cc.ClientConfig()
526+
if err != nil {
527+
t.Fatal(err)
528+
}
529+
if cfg.BearerToken != "token-a" {
530+
t.Fatalf("expected token-a via trailing-slash match, got %q", cfg.BearerToken)
531+
}
532+
}
533+
534+
func TestBuildClientConfig_URLResolution_MultipleMatches_ErrorListsContexts(t *testing.T) {
535+
writeTestKubeconfig(t, clientcmdapi.Config{
536+
CurrentContext: "ctx-1",
537+
Contexts: map[string]*clientcmdapi.Context{
538+
"ctx-1": {Cluster: "cluster-x", AuthInfo: "user-1"},
539+
"ctx-2": {Cluster: "cluster-y", AuthInfo: "user-2"},
540+
"ctx-3": {Cluster: "cluster-z", AuthInfo: "user-3"},
541+
},
542+
Clusters: map[string]*clientcmdapi.Cluster{
543+
"cluster-x": {Server: "https://active.example.com:6443"},
544+
"cluster-y": {Server: "https://shared.example.com:6443"},
545+
"cluster-z": {Server: "https://shared.example.com:6443"},
546+
},
547+
AuthInfos: map[string]*clientcmdapi.AuthInfo{
548+
"user-1": {Token: "tok-1"},
549+
"user-2": {Token: "tok-2"},
550+
"user-3": {Token: "tok-3"},
551+
},
552+
})
553+
554+
_, err := BuildClientConfig("https://shared.example.com:6443", "", "", fn.Local{})
555+
if err == nil {
556+
t.Fatal("expected error for multiple matching contexts")
557+
}
558+
// The improved error names the ambiguous contexts and how to disambiguate.
559+
for _, want := range []string{"ctx-2", "ctx-3", "disambiguate"} {
560+
if !contains(err.Error(), want) {
561+
t.Fatalf("multi-match error missing %q: %v", want, err)
562+
}
563+
}
564+
}
565+
566+
func TestResolveKubeconfigAuth_NoteOnActiveMatch(t *testing.T) {
567+
writeTestKubeconfig(t, multiContextKubeconfig())
568+
out := captureStderr(t, func() {
569+
if _, err := BuildClientConfig("https://cluster-a.example.com:6443", "", "", fn.Local{}); err != nil {
570+
t.Fatal(err)
571+
}
572+
})
573+
if !contains(out, `Using kubeconfig context "active-ctx"`) {
574+
t.Fatalf("expected active-context note, got %q", out)
575+
}
576+
}
577+
578+
func TestResolveKubeconfigAuth_NoteOnNonActiveMatch(t *testing.T) {
579+
writeTestKubeconfig(t, multiContextKubeconfig())
580+
out := captureStderr(t, func() {
581+
if _, err := BuildClientConfig("https://cluster-b.example.com:6443", "", "", fn.Local{}); err != nil {
582+
t.Fatal(err)
583+
}
584+
})
585+
if !contains(out, `Using kubeconfig context "other-ctx"`) {
586+
t.Fatalf("expected non-active-context note, got %q", out)
587+
}
588+
}
589+
590+
// captureStderr redirects os.Stderr for the duration of f and returns what was
591+
// written (resolveKubeconfigAuth prints its selection note to os.Stderr).
592+
func captureStderr(t *testing.T, f func()) string {
593+
t.Helper()
594+
old := os.Stderr
595+
r, w, err := os.Pipe()
596+
if err != nil {
597+
t.Fatal(err)
598+
}
599+
os.Stderr = w
600+
defer func() { os.Stderr = old }()
601+
f()
602+
_ = w.Close()
603+
b, err := io.ReadAll(r)
604+
if err != nil {
605+
t.Fatal(err)
606+
}
607+
return string(b)
608+
}
609+
516610
// --- YAML round-trip tests for []byte fields ---
517611

518612
func TestAuthEntry_YAMLRoundTrip(t *testing.T) {

0 commit comments

Comments
 (0)