summaryrefslogtreecommitdiffhomepage
path: root/cmd
diff options
context:
space:
mode:
authorMaisem Ali <maisem@tailscale.com>2023-02-08 15:25:00 -0800
committerMaisem Ali <maisem@tailscale.com>2023-02-14 06:06:42 +0000
commitc01c84ea8ede0304ee078405faa7a99dfcb4facd (patch)
treee0baa9803bc527cc7f73ad27c9e52a9f86b7b435 /cmd
parent181a3da51377ef8b6bbb443c68a68fb2d87fd7ee (diff)
downloadtailscale-c01c84ea8ede0304ee078405faa7a99dfcb4facd.tar.xz
tailscale-c01c84ea8ede0304ee078405faa7a99dfcb4facd.zip
cmd/tailscale/cli: add "configure kubeconfig" subcommand
It takes in a node hostname and configures the local kubeconfig file to point to that. Updates #7220 Signed-off-by: Maisem Ali <maisem@tailscale.com>
Diffstat (limited to 'cmd')
-rw-r--r--cmd/tailscale/cli/configure-kube.go152
-rw-r--r--cmd/tailscale/cli/configure-kube_test.go196
-rw-r--r--cmd/tailscale/cli/configure-synology.go6
-rw-r--r--cmd/tailscale/cli/configure.go8
-rw-r--r--cmd/tailscale/depaware.txt3
5 files changed, 361 insertions, 4 deletions
diff --git a/cmd/tailscale/cli/configure-kube.go b/cmd/tailscale/cli/configure-kube.go
new file mode 100644
index 000000000..65ff85e14
--- /dev/null
+++ b/cmd/tailscale/cli/configure-kube.go
@@ -0,0 +1,152 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+//go:build !ts_omit_kube
+
+package cli
+
+import (
+ "context"
+ "errors"
+ "flag"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/peterbourgon/ff/v3/ffcli"
+ "golang.org/x/exp/slices"
+ "k8s.io/client-go/util/homedir"
+ "sigs.k8s.io/yaml"
+)
+
+func init() {
+ configureCmd.Subcommands = append(configureCmd.Subcommands, configureKubeconfigCmd)
+}
+
+var configureKubeconfigCmd = &ffcli.Command{
+ Name: "kubeconfig",
+ ShortHelp: "Configure kubeconfig to use Tailscale",
+ ShortUsage: "kubeconfig <hostname-or-fqdn>",
+ LongHelp: strings.TrimSpace(`
+Run this command to configure your kubeconfig to use Tailscale for authentication to a Kubernetes cluster.
+
+The hostname argument should be set to the Tailscale hostname of the peer running as an auth proxy in the cluster.
+`),
+ FlagSet: (func() *flag.FlagSet {
+ fs := newFlagSet("kubeconfig")
+ return fs
+ })(),
+ Exec: runConfigureKubeconfig,
+}
+
+func runConfigureKubeconfig(ctx context.Context, args []string) error {
+ if len(args) != 1 {
+ return errors.New("unknown arguments")
+ }
+ hostOrFQDN := args[0]
+
+ st, err := localClient.Status(ctx)
+ if err != nil {
+ return err
+ }
+ if st.BackendState != "Running" {
+ return errors.New("Tailscale is not running")
+ }
+ targetFQDN, ok := nodeDNSNameFromArg(st, hostOrFQDN)
+ if !ok {
+ return fmt.Errorf("no peer found with hostname %q", hostOrFQDN)
+ }
+ targetFQDN = strings.TrimSuffix(targetFQDN, ".")
+ confPath := filepath.Join(homedir.HomeDir(), ".kube", "config")
+ if err := setKubeconfigForPeer(targetFQDN, confPath); err != nil {
+ return err
+ }
+ printf("kubeconfig configured for %q\n", hostOrFQDN)
+ return nil
+}
+
+// appendOrSetNamed finds a map with a "name" key matching name in dst, and
+// replaces it with val. If no such map is found, val is appended to dst.
+func appendOrSetNamed(dst []any, name string, val map[string]any) []any {
+ if got := slices.IndexFunc(dst, func(m any) bool {
+ if m, ok := m.(map[string]any); ok {
+ return m["name"] == name
+ }
+ return false
+ }); got != -1 {
+ dst[got] = val
+ } else {
+ dst = append(dst, val)
+ }
+ return dst
+}
+
+var errInvalidKubeconfig = errors.New("invalid kubeconfig")
+
+func updateKubeconfig(cfgYaml []byte, fqdn string) ([]byte, error) {
+ var cfg map[string]any
+ if len(cfgYaml) > 0 {
+ if err := yaml.Unmarshal(cfgYaml, &cfg); err != nil {
+ return nil, errInvalidKubeconfig
+ }
+ }
+ if cfg == nil {
+ cfg = map[string]any{
+ "apiVersion": "v1",
+ "kind": "Config",
+ }
+ } else if cfg["apiVersion"] != "v1" || cfg["kind"] != "Config" {
+ return nil, errInvalidKubeconfig
+ }
+
+ var clusters []any
+ if cm, ok := cfg["clusters"]; ok {
+ clusters = cm.([]any)
+ }
+ cfg["clusters"] = appendOrSetNamed(clusters, fqdn, map[string]any{
+ "name": fqdn,
+ "cluster": map[string]string{
+ "server": "https://" + fqdn,
+ },
+ })
+
+ var users []any
+ if um, ok := cfg["users"]; ok {
+ users = um.([]any)
+ }
+ cfg["users"] = appendOrSetNamed(users, "tailscale-auth", map[string]any{
+ // We just need one of these, and can reuse it for all clusters.
+ "name": "tailscale-auth",
+ "user": map[string]string{
+ // We do not use the token, but if we do not set anything here
+ // kubectl will prompt for a username and password.
+ "token": "unused",
+ },
+ })
+
+ var contexts []any
+ if cm, ok := cfg["contexts"]; ok {
+ contexts = cm.([]any)
+ }
+ cfg["contexts"] = appendOrSetNamed(contexts, fqdn, map[string]any{
+ "name": fqdn,
+ "context": map[string]string{
+ "cluster": fqdn,
+ "user": "tailscale-auth",
+ },
+ })
+ cfg["current-context"] = fqdn
+ return yaml.Marshal(cfg)
+}
+
+func setKubeconfigForPeer(fqdn, filePath string) error {
+ b, err := os.ReadFile(filePath)
+ if err != nil && !os.IsNotExist(err) {
+ return err
+ }
+ b, err = updateKubeconfig(b, fqdn)
+ if err != nil {
+ return err
+ }
+ return os.WriteFile(filePath, b, 0600)
+}
diff --git a/cmd/tailscale/cli/configure-kube_test.go b/cmd/tailscale/cli/configure-kube_test.go
new file mode 100644
index 000000000..0f326cd64
--- /dev/null
+++ b/cmd/tailscale/cli/configure-kube_test.go
@@ -0,0 +1,196 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+//go:build !ts_omit_kube
+
+package cli
+
+import (
+ "bytes"
+ "strings"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+)
+
+func TestKubeconfig(t *testing.T) {
+ const fqdn = "foo.tail-scale.ts.net"
+ tests := []struct {
+ name string
+ in string
+ want string
+ wantErr error
+ }{
+ {
+ name: "invalid-yaml",
+ in: `apiVersion: v1
+kind: ,asdf`,
+ wantErr: errInvalidKubeconfig,
+ },
+ {
+ name: "invalid-cfg",
+ in: `apiVersion: v1
+kind: Pod`,
+ wantErr: errInvalidKubeconfig,
+ },
+ {
+ name: "empty",
+ in: "",
+ want: `apiVersion: v1
+clusters:
+- cluster:
+ server: https://foo.tail-scale.ts.net
+ name: foo.tail-scale.ts.net
+contexts:
+- context:
+ cluster: foo.tail-scale.ts.net
+ user: tailscale-auth
+ name: foo.tail-scale.ts.net
+current-context: foo.tail-scale.ts.net
+kind: Config
+users:
+- name: tailscale-auth
+ user:
+ token: unused`,
+ },
+ {
+ name: "already-configured",
+ in: `apiVersion: v1
+clusters:
+- cluster:
+ server: https://foo.tail-scale.ts.net
+ name: foo.tail-scale.ts.net
+contexts:
+- context:
+ cluster: foo.tail-scale.ts.net
+ user: tailscale-auth
+ name: foo.tail-scale.ts.net
+kind: Config
+current-context: foo.tail-scale.ts.net
+users:
+- name: tailscale-auth
+ user:
+ token: unused`,
+ want: `apiVersion: v1
+clusters:
+- cluster:
+ server: https://foo.tail-scale.ts.net
+ name: foo.tail-scale.ts.net
+contexts:
+- context:
+ cluster: foo.tail-scale.ts.net
+ user: tailscale-auth
+ name: foo.tail-scale.ts.net
+current-context: foo.tail-scale.ts.net
+kind: Config
+users:
+- name: tailscale-auth
+ user:
+ token: unused`,
+ },
+ {
+ name: "other-cluster",
+ in: `apiVersion: v1
+clusters:
+- cluster:
+ server: https://192.168.1.1:8443
+ name: some-cluster
+contexts:
+- context:
+ cluster: some-cluster
+ user: some-auth
+ name: some-cluster
+kind: Config
+current-context: some-cluster
+users:
+- name: some-auth
+ user:
+ token: asdfasdf`,
+ want: `apiVersion: v1
+clusters:
+- cluster:
+ server: https://192.168.1.1:8443
+ name: some-cluster
+- cluster:
+ server: https://foo.tail-scale.ts.net
+ name: foo.tail-scale.ts.net
+contexts:
+- context:
+ cluster: some-cluster
+ user: some-auth
+ name: some-cluster
+- context:
+ cluster: foo.tail-scale.ts.net
+ user: tailscale-auth
+ name: foo.tail-scale.ts.net
+current-context: foo.tail-scale.ts.net
+kind: Config
+users:
+- name: some-auth
+ user:
+ token: asdfasdf
+- name: tailscale-auth
+ user:
+ token: unused`,
+ },
+ {
+ name: "already-using-tailscale",
+ in: `apiVersion: v1
+clusters:
+- cluster:
+ server: https://bar.tail-scale.ts.net
+ name: bar.tail-scale.ts.net
+contexts:
+- context:
+ cluster: bar.tail-scale.ts.net
+ user: tailscale-auth
+ name: bar.tail-scale.ts.net
+kind: Config
+current-context: bar.tail-scale.ts.net
+users:
+- name: tailscale-auth
+ user:
+ token: unused`,
+ want: `apiVersion: v1
+clusters:
+- cluster:
+ server: https://bar.tail-scale.ts.net
+ name: bar.tail-scale.ts.net
+- cluster:
+ server: https://foo.tail-scale.ts.net
+ name: foo.tail-scale.ts.net
+contexts:
+- context:
+ cluster: bar.tail-scale.ts.net
+ user: tailscale-auth
+ name: bar.tail-scale.ts.net
+- context:
+ cluster: foo.tail-scale.ts.net
+ user: tailscale-auth
+ name: foo.tail-scale.ts.net
+current-context: foo.tail-scale.ts.net
+kind: Config
+users:
+- name: tailscale-auth
+ user:
+ token: unused`,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := updateKubeconfig([]byte(tt.in), fqdn)
+ if err != nil {
+ if err != tt.wantErr {
+ t.Fatalf("updateKubeconfig() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ return
+ } else if tt.wantErr != nil {
+ t.Fatalf("updateKubeconfig() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ got = bytes.TrimSpace(got)
+ want := []byte(strings.TrimSpace(tt.want))
+ if d := cmp.Diff(want, got); d != "" {
+ t.Errorf("Kubeconfig() mismatch (-want +got):\n%s", d)
+ }
+ })
+ }
+}
diff --git a/cmd/tailscale/cli/configure-synology.go b/cmd/tailscale/cli/configure-synology.go
index 3c3ed8e14..86c739f2e 100644
--- a/cmd/tailscale/cli/configure-synology.go
+++ b/cmd/tailscale/cli/configure-synology.go
@@ -18,9 +18,9 @@ import (
"tailscale.com/version/distro"
)
-// configureHostCmd is the "tailscale configure-host" command which
-// was once used to configure Synology devices, but is now a redirect
-// to "tailscale configure synology".
+// configureHostCmd is the "tailscale configure-host" command which was once
+// used to configure Synology devices, but is now a compatibility alias to
+// "tailscale configure synology".
var configureHostCmd = &ffcli.Command{
Name: "configure-host",
Exec: runConfigureSynology,
diff --git a/cmd/tailscale/cli/configure.go b/cmd/tailscale/cli/configure.go
index f8aaad449..a40039081 100644
--- a/cmd/tailscale/cli/configure.go
+++ b/cmd/tailscale/cli/configure.go
@@ -7,13 +7,19 @@ import (
"context"
"flag"
"runtime"
+ "strings"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/version/distro"
)
var configureCmd = &ffcli.Command{
- Name: "configure",
+ Name: "configure",
+ ShortHelp: "Configure the host to enable more Tailscale features",
+ LongHelp: strings.TrimSpace(`
+The 'configure' command is intended to provide a way to configure different
+services on the host to enable more Tailscale features.
+`),
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("configure")
return fs
diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt
index 98c5c4b00..a926d9eae 100644
--- a/cmd/tailscale/depaware.txt
+++ b/cmd/tailscale/depaware.txt
@@ -40,9 +40,12 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
💣 go4.org/mem from tailscale.com/derp+
go4.org/netipx from tailscale.com/wgengine/filter
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
+ gopkg.in/yaml.v2 from sigs.k8s.io/yaml
+ k8s.io/client-go/util/homedir from tailscale.com/cmd/tailscale/cli
nhooyr.io/websocket from tailscale.com/derp/derphttp+
nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
+ sigs.k8s.io/yaml from tailscale.com/cmd/tailscale/cli
software.sslmate.com/src/go-pkcs12 from tailscale.com/cmd/tailscale/cli
software.sslmate.com/src/go-pkcs12/internal/rc2 from software.sslmate.com/src/go-pkcs12
tailscale.com from tailscale.com/version