mirror of
https://github.com/tailscale/tailscale.git
synced 2026-10-23 03:37:19 +09:00
This is the output of the new misc/bumpdeps tool (#21325) run with --exclude-newer-than-days=7, which asks proxy.golang.org for the newest version of every direct dependency, ignoring releases younger than a week in favor of the newest older one, and runs a single go get. gvisor tracks its "go" branch, wireguard-go its "tailscale" branch, and golang-x-crypto its "main" branch (the proxy's @latest for it is a stray v0.91.0 tag from 2024 that predates our acme fork changes). Indirect deps only moved as far as MVS pulled them. The week-long cooldown held back gvisor, the gokrazy modules, chromedp/cdproto, and hashicorp/raft-boltdb/v2, whose only newer versions are days old; they'll come along next time. Several upstream changes needed small fixes: nfpm's PrepareForPackager takes a modification time now (a zero time keeps the old behavior of using the source file's mtime), esbuild's ServeOptions.Port became an int while ServeResult.Host became a Hosts slice, client-go's EventRecorder.Eventf is now recognized by vet as a printf wrapper (so the k8s-operator calls that passed a preformatted message switch to Event), google/nftables v0.3.0 reads back the kernel's NF_NAT_RANGE_PROTO_SPECIFIED flag into a new expr.NAT.Specified field (so the port map DNAT rule now sets it too or findRule never matches the rule it just added), and staticcheck v0.8.1 knows encoding/json/v2's embed tag option, so the two SA5008 suppressions for it are gone. Two tests assumed old library behavior. client-go's fake clientset now replays existing objects when a watch starts, as a real apiserver does, so the k8s-proxy config test must tolerate the loader ignoring that no-op event before the real reload arrives. fyne.io/systray moved its dbusmenu object path and answers the first GetLayout with depth 1, so the systray test now finds the menu via the item's Menu property and polls until the submenu entries appear. Then make tidy, make updatedeps, and make kube-generate-all (the controller-gen bump to v0.22.0 changes doc strings, stops listing top-level metadata as required, and crd-ref-docs now marks optional fields). Updates #8043 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com> Change-Id: I3f9a2c6e8b1d4705a9e2c7b8d1f4e6a0c2b5d8e3
254 lines
6.7 KiB
Go
254 lines
6.7 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package config
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"go.uber.org/zap"
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/watch"
|
|
"k8s.io/client-go/kubernetes/fake"
|
|
ktesting "k8s.io/client-go/testing"
|
|
"tailscale.com/kube/k8s-proxy/conf"
|
|
"tailscale.com/kube/kubetypes"
|
|
)
|
|
|
|
func TestWatchConfig(t *testing.T) {
|
|
type phase struct {
|
|
config string
|
|
cancel bool
|
|
expectedConf *conf.ConfigV1Alpha1
|
|
expectedErr string
|
|
}
|
|
|
|
// Same set of behaviour tests for each config source.
|
|
for _, env := range []string{"file", "kube"} {
|
|
t.Run(env, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
initialConfig string
|
|
phases []phase
|
|
}{
|
|
{
|
|
name: "no_config",
|
|
phases: []phase{{
|
|
expectedErr: "error loading initial config",
|
|
}},
|
|
},
|
|
{
|
|
name: "valid_config",
|
|
initialConfig: `{"version": "v1alpha1", "authKey": "abc123"}`,
|
|
phases: []phase{{
|
|
expectedConf: &conf.ConfigV1Alpha1{
|
|
AuthKey: new("abc123"),
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
name: "can_cancel",
|
|
initialConfig: `{"version": "v1alpha1", "authKey": "abc123"}`,
|
|
phases: []phase{
|
|
{
|
|
expectedConf: &conf.ConfigV1Alpha1{
|
|
AuthKey: new("abc123"),
|
|
},
|
|
},
|
|
{
|
|
cancel: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "can_reload",
|
|
initialConfig: `{"version": "v1alpha1", "authKey": "abc123"}`,
|
|
phases: []phase{
|
|
{
|
|
expectedConf: &conf.ConfigV1Alpha1{
|
|
AuthKey: new("abc123"),
|
|
},
|
|
},
|
|
{
|
|
config: `{"version": "v1alpha1", "authKey": "def456"}`,
|
|
expectedConf: &conf.ConfigV1Alpha1{
|
|
AuthKey: new("def456"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "ignores_events_with_no_changes",
|
|
initialConfig: `{"version": "v1alpha1", "authKey": "abc123"}`,
|
|
phases: []phase{
|
|
{
|
|
expectedConf: &conf.ConfigV1Alpha1{
|
|
AuthKey: new("abc123"),
|
|
},
|
|
},
|
|
{
|
|
config: `{"version": "v1alpha1", "authKey": "abc123"}`,
|
|
},
|
|
},
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
root := t.TempDir()
|
|
cl := fake.NewClientset()
|
|
|
|
var cfgPath string
|
|
var writeFile func(*testing.T, string)
|
|
if env == "file" {
|
|
cfgPath = filepath.Join(root, kubetypes.KubeAPIServerConfigFile)
|
|
writeFile = func(t *testing.T, content string) {
|
|
if err := os.WriteFile(cfgPath, []byte(content), 0o644); err != nil {
|
|
t.Fatalf("error writing config file %q: %v", cfgPath, err)
|
|
}
|
|
}
|
|
} else {
|
|
cfgPath = "kube:default/config-secret"
|
|
writeFile = func(t *testing.T, content string) {
|
|
s := secretFrom(content)
|
|
mustCreateOrUpdate(t, cl, s)
|
|
}
|
|
}
|
|
configChan := make(chan *conf.Config)
|
|
loader := NewConfigLoader(zap.Must(zap.NewDevelopment()).Sugar(), cl.CoreV1(), configChan)
|
|
loader.cfgIgnored = make(chan struct{})
|
|
errs := make(chan error)
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
defer cancel()
|
|
|
|
writeFile(t, tc.initialConfig)
|
|
go func() {
|
|
errs <- loader.WatchConfig(ctx, cfgPath)
|
|
}()
|
|
|
|
for i, p := range tc.phases {
|
|
if p.config != "" {
|
|
writeFile(t, p.config)
|
|
}
|
|
if p.cancel {
|
|
cancel()
|
|
}
|
|
|
|
timeout := time.After(5 * time.Second)
|
|
waitPhase:
|
|
for {
|
|
select {
|
|
case cfg := <-configChan:
|
|
if diff := cmp.Diff(*p.expectedConf, cfg.Parsed); diff != "" {
|
|
t.Errorf("unexpected config (-want +got):\n%s", diff)
|
|
}
|
|
case err := <-errs:
|
|
if p.cancel {
|
|
if err != nil {
|
|
t.Fatalf("unexpected error after cancel: %v", err)
|
|
}
|
|
} else if p.expectedErr == "" {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
} else if !strings.Contains(err.Error(), p.expectedErr) {
|
|
t.Fatalf("expected error to contain %q, got %q", p.expectedErr, err.Error())
|
|
}
|
|
case <-loader.cfgIgnored:
|
|
if p.expectedConf != nil {
|
|
// A watch started without a resource version
|
|
// replays the existing Secret as an Added
|
|
// event, which the loader rightly ignores as
|
|
// a no-op. Keep waiting for the real reload.
|
|
continue
|
|
}
|
|
case <-timeout:
|
|
t.Fatalf("timed out waiting for expected event in phase: %d", i)
|
|
}
|
|
break waitPhase
|
|
}
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWatchConfigSecret_Rewatches(t *testing.T) {
|
|
cl := fake.NewClientset()
|
|
var watchCount int
|
|
var watcher *watch.RaceFreeFakeWatcher
|
|
expected := []string{
|
|
`{"version": "v1alpha1", "authKey": "abc123"}`,
|
|
`{"version": "v1alpha1", "authKey": "def456"}`,
|
|
`{"version": "v1alpha1", "authKey": "ghi789"}`,
|
|
}
|
|
cl.PrependWatchReactor("secrets", func(action ktesting.Action) (handled bool, ret watch.Interface, err error) {
|
|
watcher = watch.NewRaceFreeFake()
|
|
watcher.Add(secretFrom(expected[watchCount]))
|
|
if action.GetVerb() == "watch" && action.GetResource().Resource == "secrets" {
|
|
watchCount++
|
|
}
|
|
return true, watcher, nil
|
|
})
|
|
|
|
configChan := make(chan *conf.Config)
|
|
loader := NewConfigLoader(zap.Must(zap.NewDevelopment()).Sugar(), cl.CoreV1(), configChan)
|
|
|
|
mustCreateOrUpdate(t, cl, secretFrom(expected[0]))
|
|
|
|
errs := make(chan error)
|
|
go func() {
|
|
errs <- loader.watchConfigSecretChanges(t.Context(), "default", "config-secret")
|
|
}()
|
|
|
|
for i := range 2 {
|
|
select {
|
|
case cfg := <-configChan:
|
|
if exp := expected[i]; cfg.Parsed.AuthKey == nil || !strings.Contains(exp, *cfg.Parsed.AuthKey) {
|
|
t.Fatalf("expected config to have authKey %q, got: %v", exp, cfg.Parsed.AuthKey)
|
|
}
|
|
if i == 0 {
|
|
watcher.Stop()
|
|
}
|
|
case err := <-errs:
|
|
t.Fatalf("unexpected error: %v", err)
|
|
case <-loader.cfgIgnored:
|
|
t.Fatalf("expected config to be reloaded, but got ignored signal")
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatalf("timed out waiting for expected event")
|
|
}
|
|
}
|
|
|
|
if watchCount != 2 {
|
|
t.Fatalf("expected 2 watch API calls, got %d", watchCount)
|
|
}
|
|
}
|
|
|
|
func secretFrom(content string) *corev1.Secret {
|
|
return &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "config-secret",
|
|
},
|
|
Data: map[string][]byte{
|
|
kubetypes.KubeAPIServerConfigFile: []byte(content),
|
|
},
|
|
}
|
|
}
|
|
|
|
func mustCreateOrUpdate(t *testing.T, cl *fake.Clientset, s *corev1.Secret) {
|
|
t.Helper()
|
|
if _, err := cl.CoreV1().Secrets("default").Create(t.Context(), s, metav1.CreateOptions{}); err != nil {
|
|
if _, updateErr := cl.CoreV1().Secrets("default").Update(t.Context(), s, metav1.UpdateOptions{}); updateErr != nil {
|
|
t.Fatalf("error writing config Secret %q: %v", s.Name, updateErr)
|
|
}
|
|
}
|
|
}
|