@ -31,9 +31,11 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp"
"golang.org/x/sys/unix"
"golang.org/x/sys/unix"
"tailscale.com/ipn /ipnstate "
"tailscale.com/ipn "
"tailscale.com/tailcfg"
"tailscale.com/tailcfg"
"tailscale.com/tstest"
"tailscale.com/tstest"
"tailscale.com/types/netmap"
"tailscale.com/types/ptr"
)
)
func TestContainerBoot ( t * testing . T ) {
func TestContainerBoot ( t * testing . T ) {
@ -91,18 +93,13 @@ func TestContainerBoot(t *testing.T) {
}
}
argFile := filepath . Join ( d , "args" )
argFile := filepath . Join ( d , "args" )
tsIPs := [ ] netip . Addr { netip . MustParseAddr ( "100.64.0.1" ) }
runningSockPath := filepath . Join ( d , "tmp/tailscaled.sock" )
runningSockPath := filepath . Join ( d , "tmp/tailscaled.sock" )
// TODO: refactor this 1-2 stuff if we ever need a third
// step. Right now all of containerboot's modes either converge
// with no further interaction needed, or with one extra step
// only.
type phase struct {
type phase struct {
// Make LocalAPI report this status, then wait for the Wants below to b e
// If non-nil, send this IPN bus notification (and remember it as the
// satisfied. A zero Status is a valid state for a just-started
// initial update for any future new watchers, then wait for all the
// tailscaled .
// Waits below to be true before proceeding to the next phase.
Status ipnstate . Status
Notify * ipn . Notify
// WantCmds is the commands that containerboot should run in this phase.
// WantCmds is the commands that containerboot should run in this phase.
WantCmds [ ] string
WantCmds [ ] string
@ -113,6 +110,16 @@ func TestContainerBoot(t *testing.T) {
// contents.
// contents.
WantFiles map [ string ] string
WantFiles map [ string ] string
}
}
runningNotify := & ipn . Notify {
State : ptr . To ( ipn . Running ) ,
NetMap : & netmap . NetworkMap {
SelfNode : & tailcfg . Node {
StableID : tailcfg . StableNodeID ( "myID" ) ,
Name : "test-node.test.ts.net" ,
} ,
Addresses : [ ] netip . Prefix { netip . MustParsePrefix ( "100.64.0.1/32" ) } ,
} ,
}
tests := [ ] struct {
tests := [ ] struct {
Name string
Name string
Env map [ string ] string
Env map [ string ] string
@ -132,10 +139,7 @@ func TestContainerBoot(t *testing.T) {
} ,
} ,
} ,
} ,
{
{
Status : ipnstate . Status {
Notify : runningNotify ,
BackendState : "Running" ,
TailscaleIPs : tsIPs ,
} ,
} ,
} ,
} ,
} ,
} ,
} ,
@ -153,10 +157,7 @@ func TestContainerBoot(t *testing.T) {
} ,
} ,
} ,
} ,
{
{
Status : ipnstate . Status {
Notify : runningNotify ,
BackendState : "Running" ,
TailscaleIPs : tsIPs ,
} ,
} ,
} ,
} ,
} ,
} ,
} ,
@ -174,10 +175,7 @@ func TestContainerBoot(t *testing.T) {
} ,
} ,
} ,
} ,
{
{
Status : ipnstate . Status {
Notify : runningNotify ,
BackendState : "Running" ,
TailscaleIPs : tsIPs ,
} ,
} ,
} ,
} ,
} ,
} ,
} ,
@ -195,10 +193,7 @@ func TestContainerBoot(t *testing.T) {
} ,
} ,
} ,
} ,
{
{
Status : ipnstate . Status {
Notify : runningNotify ,
BackendState : "Running" ,
TailscaleIPs : tsIPs ,
} ,
WantFiles : map [ string ] string {
WantFiles : map [ string ] string {
"proc/sys/net/ipv4/ip_forward" : "0" ,
"proc/sys/net/ipv4/ip_forward" : "0" ,
"proc/sys/net/ipv6/conf/all/forwarding" : "0" ,
"proc/sys/net/ipv6/conf/all/forwarding" : "0" ,
@ -221,10 +216,7 @@ func TestContainerBoot(t *testing.T) {
} ,
} ,
} ,
} ,
{
{
Status : ipnstate . Status {
Notify : runningNotify ,
BackendState : "Running" ,
TailscaleIPs : tsIPs ,
} ,
WantFiles : map [ string ] string {
WantFiles : map [ string ] string {
"proc/sys/net/ipv4/ip_forward" : "1" ,
"proc/sys/net/ipv4/ip_forward" : "1" ,
"proc/sys/net/ipv6/conf/all/forwarding" : "0" ,
"proc/sys/net/ipv6/conf/all/forwarding" : "0" ,
@ -247,10 +239,7 @@ func TestContainerBoot(t *testing.T) {
} ,
} ,
} ,
} ,
{
{
Status : ipnstate . Status {
Notify : runningNotify ,
BackendState : "Running" ,
TailscaleIPs : tsIPs ,
} ,
WantFiles : map [ string ] string {
WantFiles : map [ string ] string {
"proc/sys/net/ipv4/ip_forward" : "0" ,
"proc/sys/net/ipv4/ip_forward" : "0" ,
"proc/sys/net/ipv6/conf/all/forwarding" : "1" ,
"proc/sys/net/ipv6/conf/all/forwarding" : "1" ,
@ -273,10 +262,7 @@ func TestContainerBoot(t *testing.T) {
} ,
} ,
} ,
} ,
{
{
Status : ipnstate . Status {
Notify : runningNotify ,
BackendState : "Running" ,
TailscaleIPs : tsIPs ,
} ,
WantFiles : map [ string ] string {
WantFiles : map [ string ] string {
"proc/sys/net/ipv4/ip_forward" : "1" ,
"proc/sys/net/ipv4/ip_forward" : "1" ,
"proc/sys/net/ipv6/conf/all/forwarding" : "1" ,
"proc/sys/net/ipv6/conf/all/forwarding" : "1" ,
@ -299,10 +285,7 @@ func TestContainerBoot(t *testing.T) {
} ,
} ,
} ,
} ,
{
{
Status : ipnstate . Status {
Notify : runningNotify ,
BackendState : "Running" ,
TailscaleIPs : tsIPs ,
} ,
WantCmds : [ ] string {
WantCmds : [ ] string {
"/usr/bin/iptables -t nat -I PREROUTING 1 -d 100.64.0.1 -j DNAT --to-destination 1.2.3.4" ,
"/usr/bin/iptables -t nat -I PREROUTING 1 -d 100.64.0.1 -j DNAT --to-destination 1.2.3.4" ,
} ,
} ,
@ -322,18 +305,15 @@ func TestContainerBoot(t *testing.T) {
} ,
} ,
} ,
} ,
{
{
Status: ipnstate . Status {
Notify: & ipn . Notify {
BackendState: "NeedsLogin" ,
State: ptr . To ( ipn . NeedsLogin ) ,
} ,
} ,
WantCmds : [ ] string {
WantCmds : [ ] string {
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key" ,
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key" ,
} ,
} ,
} ,
} ,
{
{
Status : ipnstate . Status {
Notify : runningNotify ,
BackendState : "Running" ,
TailscaleIPs : tsIPs ,
} ,
} ,
} ,
} ,
} ,
} ,
} ,
@ -357,15 +337,10 @@ func TestContainerBoot(t *testing.T) {
} ,
} ,
} ,
} ,
{
{
Status : ipnstate . Status {
Notify : runningNotify ,
BackendState : "Running" ,
TailscaleIPs : tsIPs ,
Self : & ipnstate . PeerStatus {
ID : tailcfg . StableNodeID ( "myID" ) ,
} ,
} ,
WantKubeSecret : map [ string ] string {
WantKubeSecret : map [ string ] string {
"authkey" : "tskey-key" ,
"authkey" : "tskey-key" ,
"device_fqdn" : "test-node.test.ts.net" ,
"device_id" : "myID" ,
"device_id" : "myID" ,
} ,
} ,
} ,
} ,
@ -389,13 +364,7 @@ func TestContainerBoot(t *testing.T) {
WantKubeSecret : map [ string ] string { } ,
WantKubeSecret : map [ string ] string { } ,
} ,
} ,
{
{
Status : ipnstate . Status {
Notify : runningNotify ,
BackendState : "Running" ,
TailscaleIPs : tsIPs ,
Self : & ipnstate . PeerStatus {
ID : tailcfg . StableNodeID ( "myID" ) ,
} ,
} ,
WantKubeSecret : map [ string ] string { } ,
WantKubeSecret : map [ string ] string { } ,
} ,
} ,
} ,
} ,
@ -421,8 +390,8 @@ func TestContainerBoot(t *testing.T) {
} ,
} ,
} ,
} ,
{
{
Status: ipnstate . Status {
Notify: & ipn . Notify {
BackendState: "NeedsLogin" ,
State: ptr . To ( ipn . NeedsLogin ) ,
} ,
} ,
WantCmds : [ ] string {
WantCmds : [ ] string {
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key" ,
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key" ,
@ -432,17 +401,58 @@ func TestContainerBoot(t *testing.T) {
} ,
} ,
} ,
} ,
{
{
Status : ipnstate . Status {
Notify : runningNotify ,
BackendState : "Running" ,
WantKubeSecret : map [ string ] string {
TailscaleIPs : tsIPs ,
"device_fqdn" : "test-node.test.ts.net" ,
Self : & ipnstate . PeerStatus {
"device_id" : "myID" ,
ID : tailcfg . StableNodeID ( "myID" ) ,
} ,
} ,
} ,
} ,
{
Name : "kube_storage_updates" ,
Env : map [ string ] string {
"KUBERNETES_SERVICE_HOST" : kube . Host ,
"KUBERNETES_SERVICE_PORT_HTTPS" : kube . Port ,
} ,
KubeSecret : map [ string ] string {
"authkey" : "tskey-key" ,
} ,
Phases : [ ] phase {
{
WantCmds : [ ] string {
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=kube:tailscale --statedir=/tmp --tun=userspace-networking" ,
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key" ,
} ,
} ,
WantKubeSecret : map [ string ] string {
"authkey" : "tskey-key" ,
} ,
} ,
} ,
{
Notify : runningNotify ,
WantKubeSecret : map [ string ] string {
WantKubeSecret : map [ string ] string {
"authkey" : "tskey-key" ,
"device_fqdn" : "test-node.test.ts.net" ,
"device_id" : "myID" ,
"device_id" : "myID" ,
} ,
} ,
} ,
} ,
{
Notify : & ipn . Notify {
State : ptr . To ( ipn . Running ) ,
NetMap : & netmap . NetworkMap {
SelfNode : & tailcfg . Node {
StableID : tailcfg . StableNodeID ( "newID" ) ,
Name : "new-name.test.ts.net" ,
} ,
Addresses : [ ] netip . Prefix { netip . MustParsePrefix ( "100.64.0.1/32" ) } ,
} ,
} ,
WantKubeSecret : map [ string ] string {
"authkey" : "tskey-key" ,
"device_fqdn" : "new-name.test.ts.net" ,
"device_id" : "newID" ,
} ,
} ,
} ,
} ,
} ,
} ,
{
{
@ -459,13 +469,7 @@ func TestContainerBoot(t *testing.T) {
} ,
} ,
} ,
} ,
{
{
// The tailscale up call blocks until auth is complete, so
Notify : runningNotify ,
// by the time it returns the next converged state is
// Running.
Status : ipnstate . Status {
BackendState : "Running" ,
TailscaleIPs : tsIPs ,
} ,
} ,
} ,
} ,
} ,
} ,
} ,
@ -482,10 +486,7 @@ func TestContainerBoot(t *testing.T) {
} ,
} ,
} ,
} ,
{
{
Status : ipnstate . Status {
Notify : runningNotify ,
BackendState : "Running" ,
TailscaleIPs : tsIPs ,
} ,
} ,
} ,
} ,
} ,
} ,
} ,
@ -502,10 +503,7 @@ func TestContainerBoot(t *testing.T) {
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --widget=rotated" ,
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --widget=rotated" ,
} ,
} ,
} , {
} , {
Status : ipnstate . Status {
Notify : runningNotify ,
BackendState : "Running" ,
TailscaleIPs : tsIPs ,
} ,
} ,
} ,
} ,
} ,
} ,
} ,
@ -552,7 +550,7 @@ func TestContainerBoot(t *testing.T) {
var wantCmds [ ] string
var wantCmds [ ] string
for _ , p := range test . Phases {
for _ , p := range test . Phases {
lapi . SetStatus( p . Status )
lapi . Notify( p . Notify )
wantCmds = append ( wantCmds , p . WantCmds ... )
wantCmds = append ( wantCmds , p . WantCmds ... )
waitArgs ( t , 2 * time . Second , d , argFile , strings . Join ( wantCmds , "\n" ) )
waitArgs ( t , 2 * time . Second , d , argFile , strings . Join ( wantCmds , "\n" ) )
err := tstest . WaitFor ( 2 * time . Second , func ( ) error {
err := tstest . WaitFor ( 2 * time . Second , func ( ) error {
@ -688,7 +686,8 @@ type localAPI struct {
srv * http . Server
srv * http . Server
sync . Mutex
sync . Mutex
status ipnstate . Status
cond * sync . Cond
notify * ipn . Notify
}
}
func ( l * localAPI ) Start ( ) error {
func ( l * localAPI ) Start ( ) error {
@ -706,6 +705,7 @@ func (l *localAPI) Start() error {
Handler : l ,
Handler : l ,
}
}
l . Path = path
l . Path = path
l . cond = sync . NewCond ( & l . Mutex )
go l . srv . Serve ( ln )
go l . srv . Serve ( ln )
return nil
return nil
}
}
@ -715,29 +715,49 @@ func (l *localAPI) Close() {
}
}
func ( l * localAPI ) Reset ( ) {
func ( l * localAPI ) Reset ( ) {
l . SetStatus ( ipnstate . Status {
l . Lock ( )
BackendState : "NoState" ,
defer l . Unlock ( )
} )
l . notify = nil
l . cond . Broadcast ( )
}
}
func ( l * localAPI ) SetStatus ( st ipnstate . Status ) {
func ( l * localAPI ) Notify ( n * ipn . Notify ) {
if n == nil {
return
}
l . Lock ( )
l . Lock ( )
defer l . Unlock ( )
defer l . Unlock ( )
l . status = st
l . notify = n
l . cond . Broadcast ( )
}
}
func ( l * localAPI ) ServeHTTP ( w http . ResponseWriter , r * http . Request ) {
func ( l * localAPI ) ServeHTTP ( w http . ResponseWriter , r * http . Request ) {
if r . Method != "GET" {
if r . Method != "GET" {
panic ( fmt . Sprintf ( "unsupported method %q" , r . Method ) )
panic ( fmt . Sprintf ( "unsupported method %q" , r . Method ) )
}
}
if r . URL . Path != "/localapi/v0/ stat us" {
if r . URL . Path != "/localapi/v0/ watch-ipn-b us" {
panic ( fmt . Sprintf ( "unsupported localAPI path %q", r . URL . Path ) )
panic ( fmt . Sprintf ( "unsupported path %q", r . URL . Path ) )
}
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
w . Header ( ) . Set ( "Content-Type" , "application/json" )
w . WriteHeader ( http . StatusOK )
if f , ok := w . ( http . Flusher ) ; ok {
f . Flush ( )
}
enc := json . NewEncoder ( w )
l . Lock ( )
l . Lock ( )
defer l . Unlock ( )
defer l . Unlock ( )
if err := json . NewEncoder ( w ) . Encode ( l . status ) ; err != nil {
for {
panic ( "json encode failed" )
if l . notify != nil {
if err := enc . Encode ( l . notify ) ; err != nil {
// Usually broken pipe as the test client disconnects.
return
}
if f , ok := w . ( http . Flusher ) ; ok {
f . Flush ( )
}
}
l . cond . Wait ( )
}
}
}
}