mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-29 23:31:28 +01:00 
			
		
		
		
	Previously, the operator checked the ProxyGroup status fields for information on how many of the proxies had successfully authed. Use their state Secrets instead as a more reliable source of truth. containerboot has written device_fqdn and device_ips keys to the state Secret since inception, and pod_uid since 1.78.0, so there's no need to use the API for that data. Read it from the state Secret for consistency. However, to ensure we don't read data from a previous run of containerboot, make sure we reset containerboot's state keys on startup. One other knock-on effect of that is ProxyGroups can briefly be marked not Ready while a Pod is restarting. Introduce a new ProxyGroupAvailable condition to more accurately reflect when downstream controllers can implement flows that rely on a ProxyGroup having at least 1 proxy Pod running. Fixes #16327 Change-Id: I026c18e9d23e87109a471a87b8e4fb6271716a66 Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
		
			
				
	
	
		
			321 lines
		
	
	
		
			9.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			321 lines
		
	
	
		
			9.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright (c) Tailscale Inc & AUTHORS
 | |
| // SPDX-License-Identifier: BSD-3-Clause
 | |
| 
 | |
| //go:build linux
 | |
| 
 | |
| package main
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"testing"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/google/go-cmp/cmp"
 | |
| 	"tailscale.com/ipn"
 | |
| 	"tailscale.com/kube/egressservices"
 | |
| 	"tailscale.com/kube/ingressservices"
 | |
| 	"tailscale.com/kube/kubeapi"
 | |
| 	"tailscale.com/kube/kubeclient"
 | |
| 	"tailscale.com/kube/kubetypes"
 | |
| 	"tailscale.com/tailcfg"
 | |
| )
 | |
| 
 | |
| func TestSetupKube(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		name    string
 | |
| 		cfg     *settings
 | |
| 		wantErr bool
 | |
| 		wantCfg *settings
 | |
| 		kc      *kubeClient
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "TS_AUTHKEY set, state Secret exists",
 | |
| 			cfg: &settings{
 | |
| 				AuthKey:    "foo",
 | |
| 				KubeSecret: "foo",
 | |
| 			},
 | |
| 			kc: &kubeClient{stateSecret: "foo", Client: &kubeclient.FakeClient{
 | |
| 				CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) {
 | |
| 					return false, false, nil
 | |
| 				},
 | |
| 				GetSecretImpl: func(context.Context, string) (*kubeapi.Secret, error) {
 | |
| 					return nil, nil
 | |
| 				},
 | |
| 			}},
 | |
| 			wantCfg: &settings{
 | |
| 				AuthKey:    "foo",
 | |
| 				KubeSecret: "foo",
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "TS_AUTHKEY set, state Secret does not exist, we have permissions to create it",
 | |
| 			cfg: &settings{
 | |
| 				AuthKey:    "foo",
 | |
| 				KubeSecret: "foo",
 | |
| 			},
 | |
| 			kc: &kubeClient{stateSecret: "foo", Client: &kubeclient.FakeClient{
 | |
| 				CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) {
 | |
| 					return false, true, nil
 | |
| 				},
 | |
| 				GetSecretImpl: func(context.Context, string) (*kubeapi.Secret, error) {
 | |
| 					return nil, &kubeapi.Status{Code: 404}
 | |
| 				},
 | |
| 			}},
 | |
| 			wantCfg: &settings{
 | |
| 				AuthKey:    "foo",
 | |
| 				KubeSecret: "foo",
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "TS_AUTHKEY set, state Secret does not exist, we do not have permissions to create it",
 | |
| 			cfg: &settings{
 | |
| 				AuthKey:    "foo",
 | |
| 				KubeSecret: "foo",
 | |
| 			},
 | |
| 			kc: &kubeClient{stateSecret: "foo", Client: &kubeclient.FakeClient{
 | |
| 				CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) {
 | |
| 					return false, false, nil
 | |
| 				},
 | |
| 				GetSecretImpl: func(context.Context, string) (*kubeapi.Secret, error) {
 | |
| 					return nil, &kubeapi.Status{Code: 404}
 | |
| 				},
 | |
| 			}},
 | |
| 			wantCfg: &settings{
 | |
| 				AuthKey:    "foo",
 | |
| 				KubeSecret: "foo",
 | |
| 			},
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "TS_AUTHKEY set, we encounter a non-404 error when trying to retrieve the state Secret",
 | |
| 			cfg: &settings{
 | |
| 				AuthKey:    "foo",
 | |
| 				KubeSecret: "foo",
 | |
| 			},
 | |
| 			kc: &kubeClient{stateSecret: "foo", Client: &kubeclient.FakeClient{
 | |
| 				CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) {
 | |
| 					return false, false, nil
 | |
| 				},
 | |
| 				GetSecretImpl: func(context.Context, string) (*kubeapi.Secret, error) {
 | |
| 					return nil, &kubeapi.Status{Code: 403}
 | |
| 				},
 | |
| 			}},
 | |
| 			wantCfg: &settings{
 | |
| 				AuthKey:    "foo",
 | |
| 				KubeSecret: "foo",
 | |
| 			},
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "TS_AUTHKEY set, we encounter a non-404 error when trying to check Secret permissions",
 | |
| 			cfg: &settings{
 | |
| 				AuthKey:    "foo",
 | |
| 				KubeSecret: "foo",
 | |
| 			},
 | |
| 			wantCfg: &settings{
 | |
| 				AuthKey:    "foo",
 | |
| 				KubeSecret: "foo",
 | |
| 			},
 | |
| 			kc: &kubeClient{stateSecret: "foo", Client: &kubeclient.FakeClient{
 | |
| 				CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) {
 | |
| 					return false, false, errors.New("broken")
 | |
| 				},
 | |
| 			}},
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 		{
 | |
| 			// Interactive login using URL in Pod logs
 | |
| 			name: "TS_AUTHKEY not set, state Secret does not exist, we have permissions to create it",
 | |
| 			cfg: &settings{
 | |
| 				KubeSecret: "foo",
 | |
| 			},
 | |
| 			wantCfg: &settings{
 | |
| 				KubeSecret: "foo",
 | |
| 			},
 | |
| 			kc: &kubeClient{stateSecret: "foo", Client: &kubeclient.FakeClient{
 | |
| 				CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) {
 | |
| 					return false, true, nil
 | |
| 				},
 | |
| 				GetSecretImpl: func(context.Context, string) (*kubeapi.Secret, error) {
 | |
| 					return nil, &kubeapi.Status{Code: 404}
 | |
| 				},
 | |
| 			}},
 | |
| 		},
 | |
| 		{
 | |
| 			// Interactive login using URL in Pod logs
 | |
| 			name: "TS_AUTHKEY not set, state Secret exists, but does not contain auth key",
 | |
| 			cfg: &settings{
 | |
| 				KubeSecret: "foo",
 | |
| 			},
 | |
| 			wantCfg: &settings{
 | |
| 				KubeSecret: "foo",
 | |
| 			},
 | |
| 			kc: &kubeClient{stateSecret: "foo", Client: &kubeclient.FakeClient{
 | |
| 				CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) {
 | |
| 					return false, false, nil
 | |
| 				},
 | |
| 				GetSecretImpl: func(context.Context, string) (*kubeapi.Secret, error) {
 | |
| 					return &kubeapi.Secret{}, nil
 | |
| 				},
 | |
| 			}},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "TS_AUTHKEY not set, state Secret contains auth key, we do not have RBAC to patch it",
 | |
| 			cfg: &settings{
 | |
| 				KubeSecret: "foo",
 | |
| 			},
 | |
| 			kc: &kubeClient{stateSecret: "foo", Client: &kubeclient.FakeClient{
 | |
| 				CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) {
 | |
| 					return false, false, nil
 | |
| 				},
 | |
| 				GetSecretImpl: func(context.Context, string) (*kubeapi.Secret, error) {
 | |
| 					return &kubeapi.Secret{Data: map[string][]byte{"authkey": []byte("foo")}}, nil
 | |
| 				},
 | |
| 			}},
 | |
| 			wantCfg: &settings{
 | |
| 				KubeSecret: "foo",
 | |
| 			},
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "TS_AUTHKEY not set, state Secret contains auth key, we have RBAC to patch it",
 | |
| 			cfg: &settings{
 | |
| 				KubeSecret: "foo",
 | |
| 			},
 | |
| 			kc: &kubeClient{stateSecret: "foo", Client: &kubeclient.FakeClient{
 | |
| 				CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) {
 | |
| 					return true, false, nil
 | |
| 				},
 | |
| 				GetSecretImpl: func(context.Context, string) (*kubeapi.Secret, error) {
 | |
| 					return &kubeapi.Secret{Data: map[string][]byte{"authkey": []byte("foo")}}, nil
 | |
| 				},
 | |
| 			}},
 | |
| 			wantCfg: &settings{
 | |
| 				KubeSecret:         "foo",
 | |
| 				AuthKey:            "foo",
 | |
| 				KubernetesCanPatch: true,
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		kc := tt.kc
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			if err := tt.cfg.setupKube(context.Background(), kc); (err != nil) != tt.wantErr {
 | |
| 				t.Errorf("settings.setupKube() error = %v, wantErr %v", err, tt.wantErr)
 | |
| 			}
 | |
| 			if diff := cmp.Diff(*tt.cfg, *tt.wantCfg); diff != "" {
 | |
| 				t.Errorf("unexpected contents of settings after running settings.setupKube()\n(-got +want):\n%s", diff)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestWaitForConsistentState(t *testing.T) {
 | |
| 	data := map[string][]byte{
 | |
| 		// Missing _current-profile.
 | |
| 		string(ipn.KnownProfilesStateKey): []byte(""),
 | |
| 		string(ipn.MachineKeyStateKey):    []byte(""),
 | |
| 		"profile-foo":                     []byte(""),
 | |
| 	}
 | |
| 	kc := &kubeClient{
 | |
| 		Client: &kubeclient.FakeClient{
 | |
| 			GetSecretImpl: func(context.Context, string) (*kubeapi.Secret, error) {
 | |
| 				return &kubeapi.Secret{
 | |
| 					Data: data,
 | |
| 				}, nil
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
 | |
| 	defer cancel()
 | |
| 	if err := kc.waitForConsistentState(ctx); err != context.DeadlineExceeded {
 | |
| 		t.Fatalf("expected DeadlineExceeded, got %v", err)
 | |
| 	}
 | |
| 
 | |
| 	ctx, cancel = context.WithTimeout(context.Background(), time.Second)
 | |
| 	defer cancel()
 | |
| 	data[string(ipn.CurrentProfileStateKey)] = []byte("")
 | |
| 	if err := kc.waitForConsistentState(ctx); err != nil {
 | |
| 		t.Fatalf("expected nil, got %v", err)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestResetContainerbootState(t *testing.T) {
 | |
| 	capver := fmt.Appendf(nil, "%d", tailcfg.CurrentCapabilityVersion)
 | |
| 	for name, tc := range map[string]struct {
 | |
| 		podUID   string
 | |
| 		initial  map[string][]byte
 | |
| 		expected map[string][]byte
 | |
| 	}{
 | |
| 		"empty_initial": {
 | |
| 			podUID:  "1234",
 | |
| 			initial: map[string][]byte{},
 | |
| 			expected: map[string][]byte{
 | |
| 				kubetypes.KeyCapVer: capver,
 | |
| 				kubetypes.KeyPodUID: []byte("1234"),
 | |
| 			},
 | |
| 		},
 | |
| 		"empty_initial_no_pod_uid": {
 | |
| 			initial: map[string][]byte{},
 | |
| 			expected: map[string][]byte{
 | |
| 				kubetypes.KeyCapVer: capver,
 | |
| 			},
 | |
| 		},
 | |
| 		"only_relevant_keys_updated": {
 | |
| 			podUID: "1234",
 | |
| 			initial: map[string][]byte{
 | |
| 				kubetypes.KeyCapVer:              []byte("1"),
 | |
| 				kubetypes.KeyPodUID:              []byte("5678"),
 | |
| 				kubetypes.KeyDeviceID:            []byte("device-id"),
 | |
| 				kubetypes.KeyDeviceFQDN:          []byte("device-fqdn"),
 | |
| 				kubetypes.KeyDeviceIPs:           []byte(`["192.0.2.1"]`),
 | |
| 				kubetypes.KeyHTTPSEndpoint:       []byte("https://example.com"),
 | |
| 				egressservices.KeyEgressServices: []byte("egress-services"),
 | |
| 				ingressservices.IngressConfigKey: []byte("ingress-config"),
 | |
| 				"_current-profile":               []byte("current-profile"),
 | |
| 				"_machinekey":                    []byte("machine-key"),
 | |
| 				"_profiles":                      []byte("profiles"),
 | |
| 				"_serve_e0ce":                    []byte("serve-e0ce"),
 | |
| 				"profile-e0ce":                   []byte("profile-e0ce"),
 | |
| 			},
 | |
| 			expected: map[string][]byte{
 | |
| 				kubetypes.KeyCapVer: capver,
 | |
| 				kubetypes.KeyPodUID: []byte("1234"),
 | |
| 				// Cleared keys.
 | |
| 				kubetypes.KeyDeviceID:            nil,
 | |
| 				kubetypes.KeyDeviceFQDN:          nil,
 | |
| 				kubetypes.KeyDeviceIPs:           nil,
 | |
| 				kubetypes.KeyHTTPSEndpoint:       nil,
 | |
| 				egressservices.KeyEgressServices: nil,
 | |
| 				ingressservices.IngressConfigKey: nil,
 | |
| 				// Tailscaled keys not included in patch.
 | |
| 			},
 | |
| 		},
 | |
| 	} {
 | |
| 		t.Run(name, func(t *testing.T) {
 | |
| 			var actual map[string][]byte
 | |
| 			kc := &kubeClient{stateSecret: "foo", Client: &kubeclient.FakeClient{
 | |
| 				GetSecretImpl: func(context.Context, string) (*kubeapi.Secret, error) {
 | |
| 					return &kubeapi.Secret{
 | |
| 						Data: tc.initial,
 | |
| 					}, nil
 | |
| 				},
 | |
| 				StrategicMergePatchSecretImpl: func(ctx context.Context, name string, secret *kubeapi.Secret, _ string) error {
 | |
| 					actual = secret.Data
 | |
| 					return nil
 | |
| 				},
 | |
| 			}}
 | |
| 			if err := kc.resetContainerbootState(context.Background(), tc.podUID); err != nil {
 | |
| 				t.Fatalf("resetContainerbootState() error = %v", err)
 | |
| 			}
 | |
| 			if diff := cmp.Diff(tc.expected, actual); diff != "" {
 | |
| 				t.Errorf("resetContainerbootState() mismatch (-want +got):\n%s", diff)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 |