4c3ed5ab32
Move tailscaled's in-tree reactive users from of IPN bus Notify.NetMap updates to the narrower Notify.SelfChange signal introduced earlier in this series. Consumers that need additional state (peers, DNS config, etc.) fetch it on demand via the LocalAPI. It is a step toward the larger goal of not fanning Notify.NetMap out to every bus watcher on Linux/non-GUI hosts. A future change stops sending Notify.NetMap entirely on Linux and non-GUI platforms. (eventually once macOS/iOS/Windows migrate to the upcoming new Notify APIs, we'll remove ipn.Notify.NetMap entirely) Updates #12542 Change-Id: I51ea9d86bdca1909d6ac0e7d5bd3934a3a4e8516 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
192 lines
4.9 KiB
Go
192 lines
4.9 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build !plan9
|
|
|
|
package state
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"net/netip"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"tailscale.com/ipn"
|
|
"tailscale.com/ipn/store"
|
|
klc "tailscale.com/kube/localclient"
|
|
"tailscale.com/tailcfg"
|
|
"tailscale.com/types/logger"
|
|
)
|
|
|
|
func TestSetInitialStateKeys(t *testing.T) {
|
|
var (
|
|
podUID = []byte("test-pod-uid")
|
|
expectedCapVer = fmt.Appendf(nil, "%d", tailcfg.CurrentCapabilityVersion)
|
|
)
|
|
for name, tc := range map[string]struct {
|
|
initial map[ipn.StateKey][]byte
|
|
expected map[ipn.StateKey][]byte
|
|
}{
|
|
"empty_initial": {
|
|
initial: map[ipn.StateKey][]byte{},
|
|
expected: map[ipn.StateKey][]byte{
|
|
keyPodUID: podUID,
|
|
keyCapVer: expectedCapVer,
|
|
},
|
|
},
|
|
"existing_pod_uid_and_capver": {
|
|
initial: map[ipn.StateKey][]byte{
|
|
keyPodUID: podUID,
|
|
keyCapVer: expectedCapVer,
|
|
},
|
|
expected: map[ipn.StateKey][]byte{
|
|
keyPodUID: podUID,
|
|
keyCapVer: expectedCapVer,
|
|
},
|
|
},
|
|
"all_keys_preexisting": {
|
|
initial: map[ipn.StateKey][]byte{
|
|
keyPodUID: podUID,
|
|
keyCapVer: expectedCapVer,
|
|
keyDeviceID: []byte("existing-device-id"),
|
|
keyDeviceFQDN: []byte("existing-device-fqdn"),
|
|
keyDeviceIPs: []byte(`["1.2.3.4"]`),
|
|
},
|
|
expected: map[ipn.StateKey][]byte{
|
|
keyPodUID: podUID,
|
|
keyCapVer: expectedCapVer,
|
|
keyDeviceID: []byte("existing-device-id"),
|
|
keyDeviceFQDN: []byte("existing-device-fqdn"),
|
|
keyDeviceIPs: []byte(`["1.2.3.4"]`),
|
|
},
|
|
},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
store, err := store.New(logger.Discard, "mem:")
|
|
if err != nil {
|
|
t.Fatalf("error creating in-memory store: %v", err)
|
|
}
|
|
|
|
for key, value := range tc.initial {
|
|
if err := store.WriteState(key, value); err != nil {
|
|
t.Fatalf("error writing initial state key %q: %v", key, err)
|
|
}
|
|
}
|
|
|
|
if err := SetInitialKeys(store, string(podUID)); err != nil {
|
|
t.Fatalf("setInitialStateKeys failed: %v", err)
|
|
}
|
|
|
|
actual := make(map[ipn.StateKey][]byte)
|
|
for expectedKey, expectedValue := range tc.expected {
|
|
actualValue, err := store.ReadState(expectedKey)
|
|
if err != nil {
|
|
t.Errorf("error reading state key %q: %v", expectedKey, err)
|
|
continue
|
|
}
|
|
|
|
actual[expectedKey] = actualValue
|
|
if !bytes.Equal(actualValue, expectedValue) {
|
|
t.Errorf("state key %q mismatch: expected %q, got %q", expectedKey, expectedValue, actualValue)
|
|
}
|
|
}
|
|
if diff := cmp.Diff(actual, tc.expected); diff != "" {
|
|
t.Errorf("state keys mismatch (-got +want):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestKeepStateKeysUpdated(t *testing.T) {
|
|
store := fakeStore{
|
|
writeChan: make(chan string),
|
|
}
|
|
|
|
errs := make(chan error)
|
|
notifyChan := make(chan ipn.Notify)
|
|
lc := &klc.FakeLocalClient{
|
|
FakeIPNBusWatcher: klc.FakeIPNBusWatcher{
|
|
NotifyChan: notifyChan,
|
|
},
|
|
}
|
|
|
|
go func() {
|
|
err := KeepKeysUpdated(t.Context(), store, lc)
|
|
if err != nil {
|
|
errs <- fmt.Errorf("keepStateKeysUpdated returned with error: %w", err)
|
|
}
|
|
}()
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
notify ipn.Notify
|
|
expected []string
|
|
}{
|
|
{
|
|
name: "initial_not_authed",
|
|
notify: ipn.Notify{},
|
|
expected: nil,
|
|
},
|
|
{
|
|
name: "authed",
|
|
notify: ipn.Notify{
|
|
SelfChange: &tailcfg.Node{
|
|
StableID: "TESTCTRL00000001",
|
|
Name: "test-node.test.ts.net",
|
|
Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32"), netip.MustParsePrefix("fd7a:115c:a1e0:ab12:4843:cd96:0:1/128")},
|
|
},
|
|
},
|
|
expected: []string{
|
|
fmt.Sprintf("%s=%s", keyDeviceID, "TESTCTRL00000001"),
|
|
fmt.Sprintf("%s=%s", keyDeviceFQDN, "test-node.test.ts.net"),
|
|
fmt.Sprintf("%s=%s", keyDeviceIPs, `["100.64.0.1","fd7a:115c:a1e0:ab12:4843:cd96:0:1"]`),
|
|
},
|
|
},
|
|
{
|
|
name: "updated_fields",
|
|
notify: ipn.Notify{
|
|
SelfChange: &tailcfg.Node{
|
|
StableID: "TESTCTRL00000001",
|
|
Name: "updated.test.ts.net",
|
|
Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.250/32")},
|
|
},
|
|
},
|
|
expected: []string{
|
|
fmt.Sprintf("%s=%s", keyDeviceFQDN, "updated.test.ts.net"),
|
|
fmt.Sprintf("%s=%s", keyDeviceIPs, `["100.64.0.250"]`),
|
|
},
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
notifyChan <- tc.notify
|
|
for _, expected := range tc.expected {
|
|
select {
|
|
case got := <-store.writeChan:
|
|
if got != expected {
|
|
t.Errorf("expected %q, got %q", expected, got)
|
|
}
|
|
case err := <-errs:
|
|
t.Fatalf("unexpected error: %v", err)
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatalf("timed out waiting for expected write %q", expected)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type fakeStore struct {
|
|
writeChan chan string
|
|
}
|
|
|
|
func (f fakeStore) ReadState(key ipn.StateKey) ([]byte, error) {
|
|
return nil, fmt.Errorf("ReadState not implemented")
|
|
}
|
|
|
|
func (f fakeStore) WriteState(key ipn.StateKey, value []byte) error {
|
|
f.writeChan <- fmt.Sprintf("%s=%s", key, value)
|
|
return nil
|
|
}
|