WIP: rebase fork onto upstream/main (v1.103.0) #15

Closed
codinget wants to merge 670 commits from webnet into save/webnet-2026-07-29
2 changed files with 130 additions and 15 deletions
Showing only changes of commit b6713e9bc8 - Show all commits
+64 -15
View File
@@ -52,18 +52,15 @@ See: https://tailscale.com/s/k8s-auth-proxy
}
// kubeconfigPath returns the path to the kubeconfig file for the current user.
func kubeconfigPath() (string, error) {
func kubeconfigPath() string {
if kubeconfig := os.Getenv("KUBECONFIG"); kubeconfig != "" {
if version.IsSandboxedMacOS() {
return "", errors.New("cannot read $KUBECONFIG on GUI builds of the macOS client: this requires the open-source tailscaled distribution")
}
var out string
for _, out = range filepath.SplitList(kubeconfig) {
if info, err := os.Stat(out); !os.IsNotExist(err) && !info.IsDir() {
break
}
}
return out, nil
return out
}
var dir string
@@ -77,7 +74,64 @@ func kubeconfigPath() (string, error) {
} else {
dir = homedir.HomeDir()
}
return filepath.Join(dir, ".kube", "config"), nil
return filepath.Join(dir, ".kube", "config")
}
// checkKubeconfigWritable returns nil if the kubeconfig at path can be written,
// or an error explaining why it can't. A not-yet-created file or .kube
// directory is fine as long as the nearest existing ancestor is writable.
//
// On sandboxed macOS builds, kubeconfigPath resolves path to the user's real
// ~/.kube/config, which we can only write via the home-relative-path
// entitlement. If that write would fail (e.g. because $KUBECONFIG points
// somewhere the sandbox can't reach), we want to surface a clear error pointing
// at the open-source tailscaled distribution rather than silently writing a
// config the user's kubectl will never read into the sandbox container.
func checkKubeconfigWritable(path string) error {
for try := path; ; try = filepath.Dir(try) {
if _, err := os.Stat(try); err == nil {
if err := isWritable(try); err != nil {
return kubeconfigAccessErr(path, err)
}
return nil
} else if !os.IsNotExist(err) {
return kubeconfigAccessErr(path, err)
}
if parent := filepath.Dir(try); parent == try {
return nil // reached the filesystem root
}
}
}
// isWritable reports whether path can be opened or created for writing. For a
// directory it probes by creating and removing a temporary file.
func isWritable(path string) error {
fi, err := os.Stat(path)
if err != nil {
return err
}
if fi.IsDir() {
f, err := os.CreateTemp(path, ".tailscale-kubeconfig-*")
if err != nil {
return err
}
f.Close()
return os.Remove(f.Name())
}
f, err := os.OpenFile(path, os.O_WRONLY, 0)
if err != nil {
return err
}
return f.Close()
}
// kubeconfigAccessErr wraps err with context about path, adding macOS sandbox
// guidance when the process is sandboxed.
func kubeconfigAccessErr(path string, err error) error {
if version.IsSandboxedMacOS() {
return fmt.Errorf("cannot write kubeconfig at %q: %w; GUI builds of the macOS client run in a sandbox and can only access files under your home directory, use the open-source tailscaled distribution for other locations", path, err)
}
return fmt.Errorf("cannot write kubeconfig at %q: %w", path, err)
}
func runConfigureKubeconfig(ctx context.Context, args []string) error {
@@ -106,8 +160,8 @@ func runConfigureKubeconfig(ctx context.Context, args []string) error {
return err
}
targetFQDN = strings.TrimSuffix(targetFQDN, ".")
var kubeconfig string
if kubeconfig, err = kubeconfigPath(); err != nil {
kubeconfig := kubeconfigPath()
if err := checkKubeconfigWritable(kubeconfig); err != nil {
return err
}
scheme := "https://"
@@ -215,13 +269,8 @@ func setKubeconfigForPeer(scheme, fqdn, filePath string) error {
if !os.IsNotExist(err) {
return err
}
if err := os.Mkdir(dir, 0755); err != nil {
if version.IsSandboxedMacOS() && errors.Is(err, os.ErrPermission) {
// macOS sandboxing prevents us from creating the .kube directory
// in the home directory.
return errors.New("unable to create .kube directory in home directory, please create it manually (e.g. mkdir ~/.kube")
}
return err
if err := os.MkdirAll(dir, 0755); err != nil {
return kubeconfigAccessErr(filePath, err)
}
}
b, err := os.ReadFile(filePath)
+66
View File
@@ -7,6 +7,9 @@ package cli
import (
"bytes"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
@@ -247,6 +250,69 @@ users:
}
}
func TestCheckKubeconfigWritable(t *testing.T) {
t.Run("nonexistent-file-in-writable-dir", func(t *testing.T) {
dir := t.TempDir()
if err := checkKubeconfigWritable(filepath.Join(dir, "config")); err != nil {
t.Errorf("unexpected error: %v", err)
}
})
t.Run("nonexistent-file-and-dir-in-writable-parent", func(t *testing.T) {
dir := t.TempDir()
// The .kube directory does not exist yet, but its parent does and is
// writable, so this should be fine.
if err := checkKubeconfigWritable(filepath.Join(dir, ".kube", "config")); err != nil {
t.Errorf("unexpected error: %v", err)
}
})
t.Run("existing-writable-file", func(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config")
if err := os.WriteFile(path, []byte("apiVersion: v1\nkind: Config\n"), 0600); err != nil {
t.Fatal(err)
}
if err := checkKubeconfigWritable(path); err != nil {
t.Errorf("unexpected error: %v", err)
}
})
t.Run("unwritable-existing-file", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("file mode permissions are not enforced the same way on Windows")
}
if os.Getuid() == 0 {
t.Skip("root bypasses file permission checks")
}
dir := t.TempDir()
path := filepath.Join(dir, "config")
if err := os.WriteFile(path, []byte("x"), 0400); err != nil {
t.Fatal(err)
}
if err := checkKubeconfigWritable(path); err == nil {
t.Error("expected error for read-only file, got nil")
}
})
t.Run("unwritable-dir", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("directory mode permissions are not enforced the same way on Windows")
}
if os.Getuid() == 0 {
t.Skip("root bypasses directory permission checks")
}
dir := t.TempDir()
sub := filepath.Join(dir, "ro")
if err := os.Mkdir(sub, 0500); err != nil {
t.Fatal(err)
}
if err := checkKubeconfigWritable(filepath.Join(sub, "config")); err == nil {
t.Error("expected error for unwritable dir, got nil")
}
})
}
func TestGetInputs(t *testing.T) {
for _, arg := range []string{
"foo.tail-scale.ts.net",