WIP: rebase fork onto upstream/main (v1.103.0) #15
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user