mirror of
https://github.com/tailscale/tailscale.git
synced 2025-04-02 22:35:59 +00:00
230 lines
5.9 KiB
Go
230 lines
5.9 KiB
Go
![]() |
// Copyright (c) Tailscale Inc & AUTHORS
|
||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||
|
|
||
|
//go:build linux
|
||
|
|
||
|
package main
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"testing"
|
||
|
"time"
|
||
|
|
||
|
"tailscale.com/ipn"
|
||
|
"tailscale.com/tailcfg"
|
||
|
)
|
||
|
|
||
|
// TestEnsureCertLoops tests that the certManager correctly starts and stops
|
||
|
// update loops for certs when the serve config changes. It tracks goroutine
|
||
|
// count and uses that as a validator that the expected number of cert loops are
|
||
|
// running.
|
||
|
func TestEnsureCertLoops(t *testing.T) {
|
||
|
tests := []struct {
|
||
|
name string
|
||
|
initialConfig *ipn.ServeConfig
|
||
|
updatedConfig *ipn.ServeConfig
|
||
|
initialGoroutines int64 // after initial serve config is applied
|
||
|
updatedGoroutines int64 // after updated serve config is applied
|
||
|
wantErr bool
|
||
|
}{
|
||
|
{
|
||
|
name: "empty_serve_config",
|
||
|
initialConfig: &ipn.ServeConfig{},
|
||
|
initialGoroutines: 0,
|
||
|
},
|
||
|
{
|
||
|
name: "nil_serve_config",
|
||
|
initialConfig: nil,
|
||
|
initialGoroutines: 0,
|
||
|
wantErr: true,
|
||
|
},
|
||
|
{
|
||
|
name: "empty_to_one_service",
|
||
|
initialConfig: &ipn.ServeConfig{},
|
||
|
updatedConfig: &ipn.ServeConfig{
|
||
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
||
|
"svc:my-app": {
|
||
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||
|
"my-app.tailnetxyz.ts.net:443": {},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
initialGoroutines: 0,
|
||
|
updatedGoroutines: 1,
|
||
|
},
|
||
|
{
|
||
|
name: "single_service",
|
||
|
initialConfig: &ipn.ServeConfig{
|
||
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
||
|
"svc:my-app": {
|
||
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||
|
"my-app.tailnetxyz.ts.net:443": {},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
initialGoroutines: 1,
|
||
|
},
|
||
|
{
|
||
|
name: "multiple_services",
|
||
|
initialConfig: &ipn.ServeConfig{
|
||
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
||
|
"svc:my-app": {
|
||
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||
|
"my-app.tailnetxyz.ts.net:443": {},
|
||
|
},
|
||
|
},
|
||
|
"svc:my-other-app": {
|
||
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||
|
"my-other-app.tailnetxyz.ts.net:443": {},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
initialGoroutines: 2, // one loop per domain across all services
|
||
|
},
|
||
|
{
|
||
|
name: "ignore_non_https_ports",
|
||
|
initialConfig: &ipn.ServeConfig{
|
||
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
||
|
"svc:my-app": {
|
||
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||
|
"my-app.tailnetxyz.ts.net:443": {},
|
||
|
"my-app.tailnetxyz.ts.net:80": {},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
initialGoroutines: 1, // only one loop for the 443 endpoint
|
||
|
},
|
||
|
{
|
||
|
name: "remove_domain",
|
||
|
initialConfig: &ipn.ServeConfig{
|
||
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
||
|
"svc:my-app": {
|
||
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||
|
"my-app.tailnetxyz.ts.net:443": {},
|
||
|
},
|
||
|
},
|
||
|
"svc:my-other-app": {
|
||
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||
|
"my-other-app.tailnetxyz.ts.net:443": {},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
updatedConfig: &ipn.ServeConfig{
|
||
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
||
|
"svc:my-app": {
|
||
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||
|
"my-app.tailnetxyz.ts.net:443": {},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
initialGoroutines: 2, // initially two loops (one per service)
|
||
|
updatedGoroutines: 1, // one loop after removing service2
|
||
|
},
|
||
|
{
|
||
|
name: "add_domain",
|
||
|
initialConfig: &ipn.ServeConfig{
|
||
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
||
|
"svc:my-app": {
|
||
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||
|
"my-app.tailnetxyz.ts.net:443": {},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
updatedConfig: &ipn.ServeConfig{
|
||
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
||
|
"svc:my-app": {
|
||
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||
|
"my-app.tailnetxyz.ts.net:443": {},
|
||
|
},
|
||
|
},
|
||
|
"svc:my-other-app": {
|
||
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||
|
"my-other-app.tailnetxyz.ts.net:443": {},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
initialGoroutines: 1,
|
||
|
updatedGoroutines: 2,
|
||
|
},
|
||
|
}
|
||
|
|
||
|
for _, tt := range tests {
|
||
|
t.Run(tt.name, func(t *testing.T) {
|
||
|
ctx, cancel := context.WithCancel(context.Background())
|
||
|
defer cancel()
|
||
|
|
||
|
cm := &certManager{
|
||
|
lc: &fakeLocalClient{},
|
||
|
certLoops: make(map[string]context.CancelFunc),
|
||
|
}
|
||
|
|
||
|
allDone := make(chan bool, 1)
|
||
|
defer cm.tracker.AddDoneCallback(func() {
|
||
|
cm.mu.Lock()
|
||
|
defer cm.mu.Unlock()
|
||
|
if cm.tracker.RunningGoroutines() > 0 {
|
||
|
return
|
||
|
}
|
||
|
select {
|
||
|
case allDone <- true:
|
||
|
default:
|
||
|
}
|
||
|
})()
|
||
|
|
||
|
err := cm.ensureCertLoops(ctx, tt.initialConfig)
|
||
|
if (err != nil) != tt.wantErr {
|
||
|
t.Fatalf("ensureCertLoops() error = %v", err)
|
||
|
}
|
||
|
|
||
|
if got := cm.tracker.RunningGoroutines(); got != tt.initialGoroutines {
|
||
|
t.Errorf("after initial config: got %d running goroutines, want %d", got, tt.initialGoroutines)
|
||
|
}
|
||
|
|
||
|
if tt.updatedConfig != nil {
|
||
|
if err := cm.ensureCertLoops(ctx, tt.updatedConfig); err != nil {
|
||
|
t.Fatalf("ensureCertLoops() error on update = %v", err)
|
||
|
}
|
||
|
|
||
|
// Although starting goroutines and cancelling
|
||
|
// the context happens in the main goroutine, it
|
||
|
// the actual goroutine exit when a context is
|
||
|
// cancelled does not- so wait for a bit for the
|
||
|
// running goroutine count to reach the expected
|
||
|
// number.
|
||
|
deadline := time.After(5 * time.Second)
|
||
|
for {
|
||
|
if got := cm.tracker.RunningGoroutines(); got == tt.updatedGoroutines {
|
||
|
break
|
||
|
}
|
||
|
select {
|
||
|
case <-deadline:
|
||
|
t.Fatalf("timed out waiting for goroutine count to reach %d, currently at %d",
|
||
|
tt.updatedGoroutines, cm.tracker.RunningGoroutines())
|
||
|
case <-time.After(10 * time.Millisecond):
|
||
|
continue
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if tt.updatedGoroutines == 0 {
|
||
|
return // no goroutines to wait for
|
||
|
}
|
||
|
// cancel context to make goroutines exit
|
||
|
cancel()
|
||
|
select {
|
||
|
case <-time.After(5 * time.Second):
|
||
|
t.Fatal("timed out waiting for goroutine to finish")
|
||
|
case <-allDone:
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
}
|