mirror of
https://github.com/tailscale/tailscale.git
synced 2025-08-01 00:33:43 +00:00
local interactive test code
Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
This commit is contained in:
parent
1363be38ce
commit
30a13ebbd8
@ -7,6 +7,11 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
_ "embed"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
@ -15,6 +20,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@ -47,9 +53,7 @@ func TestContainerBoot(t *testing.T) {
|
||||
defer lapi.Close()
|
||||
|
||||
kube := kubeServer{FSRoot: d}
|
||||
if err := kube.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
kube.Start(t, "127.0.0.1")
|
||||
defer kube.Close()
|
||||
|
||||
tailscaledConf := &ipn.ConfigVAlpha{AuthKey: ptr.To("foo"), Version: "alpha0"}
|
||||
@ -1179,33 +1183,31 @@ func (k *kubeServer) Reset() {
|
||||
k.secret = map[string]string{}
|
||||
}
|
||||
|
||||
func (k *kubeServer) Start() error {
|
||||
func (k *kubeServer) Start(t *testing.T, ip string) {
|
||||
root := filepath.Join(k.FSRoot, "var/run/secrets/kubernetes.io/serviceaccount")
|
||||
|
||||
if err := os.MkdirAll(root, 0700); err != nil {
|
||||
return err
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filepath.Join(root, "namespace"), []byte("default"), 0600); err != nil {
|
||||
return err
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, "token"), []byte("bearer_token"), 0600); err != nil {
|
||||
return err
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
k.srv = httptest.NewTLSServer(k)
|
||||
k.srv = httptestServer(t, ip, k)
|
||||
k.Host = k.srv.Listener.Addr().(*net.TCPAddr).IP.String()
|
||||
k.Port = strconv.Itoa(k.srv.Listener.Addr().(*net.TCPAddr).Port)
|
||||
|
||||
var cert bytes.Buffer
|
||||
if err := pem.Encode(&cert, &pem.Block{Type: "CERTIFICATE", Bytes: k.srv.Certificate().Raw}); err != nil {
|
||||
return err
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, "ca.crt"), cert.Bytes(), 0600); err != nil {
|
||||
return err
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *kubeServer) Close() {
|
||||
@ -1217,6 +1219,8 @@ func (k *kubeServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
panic("client didn't provide bearer token in request")
|
||||
}
|
||||
switch r.URL.Path {
|
||||
case "/api/v1/namespaces/default/secrets":
|
||||
k.serveCreateSecret(w, r)
|
||||
case "/api/v1/namespaces/default/secrets/tailscale":
|
||||
k.serveSecret(w, r)
|
||||
case "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews":
|
||||
@ -1248,12 +1252,47 @@ func (k *kubeServer) serveSSAR(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, `{"status":{"allowed":%v}}`, ok)
|
||||
}
|
||||
|
||||
func (k *kubeServer) serveCreateSecret(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "PUT" {
|
||||
panic(fmt.Sprintf("unhandled HTTP request %s %s", r.Method, r.URL))
|
||||
}
|
||||
|
||||
bs, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("reading request body: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
k.Lock()
|
||||
defer k.Unlock()
|
||||
payload := map[string]any{}
|
||||
if err := json.Unmarshal(bs, &payload); err != nil {
|
||||
panic("unmarshal failed")
|
||||
}
|
||||
data := payload["data"].(map[string]any)
|
||||
k.secret = map[string]string{}
|
||||
for key, value := range data {
|
||||
v, err := base64.StdEncoding.DecodeString(value.(string))
|
||||
if err != nil {
|
||||
panic("base64 decode failed")
|
||||
}
|
||||
k.secret[key] = string(v)
|
||||
}
|
||||
// The real API echoes back the secret with additional fields set.
|
||||
if _, err := w.Write(bs); err != nil {
|
||||
panic("write failed")
|
||||
}
|
||||
}
|
||||
|
||||
func (k *kubeServer) serveSecret(w http.ResponseWriter, r *http.Request) {
|
||||
bs, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("reading request body: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
@ -1308,6 +1347,78 @@ func (k *kubeServer) serveSecret(w http.ResponseWriter, r *http.Request) {
|
||||
panic(fmt.Sprintf("unknown content type %q", r.Header.Get("Content-Type")))
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("unhandled HTTP method %q", r.Method))
|
||||
panic(fmt.Sprintf("unhandled HTTP request %s %s", r.Method, r.URL))
|
||||
}
|
||||
}
|
||||
|
||||
func httptestServer(t *testing.T, ip string, handler http.Handler) *httptest.Server {
|
||||
ln, err := net.Listen("tcp", fmt.Sprintf("%s:0", ip))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
srv := &httptest.Server{
|
||||
Listener: ln,
|
||||
Config: &http.Server{Handler: handler},
|
||||
}
|
||||
|
||||
// Generate a TLS certificate valid for ip.
|
||||
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
template := x509.Certificate{
|
||||
IPAddresses: []net.IP{net.ParseIP(ip)},
|
||||
SerialNumber: big.NewInt(time.Now().Unix()),
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"Acme Co"},
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(24 * time.Hour),
|
||||
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageCertSign,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
}
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
srv.TLS = &tls.Config{
|
||||
Certificates: []tls.Certificate{{
|
||||
Certificate: [][]byte{derBytes},
|
||||
PrivateKey: priv,
|
||||
}},
|
||||
}
|
||||
srv.StartTLS()
|
||||
|
||||
return srv
|
||||
}
|
||||
|
||||
// TestFakeKubernetesAPIServer aims to make it trivially easy to run interactive
|
||||
// tests against containerboot within a container but without a real k8s cluster.
|
||||
// The container just needs enough kube API to store its state in a "Secret".
|
||||
//
|
||||
// export TS_AUTHKEY=ts...
|
||||
// DOCKER_IP=$(docker network inspect bridge --format '{{(index .IPAM.Config 0).Gateway}}')
|
||||
// TAGS=test PLATFORM=local REPO=tailscale make publishdevimage
|
||||
// FAKE_KUBERNETES_API_IP=${DOCKER_IP} go test -timeout 0 -v -run TestFakeKubernetesAPIServer ./cmd/containerboot/
|
||||
// <Run the docker command printed to the terminal>
|
||||
func TestFakeKubernetesAPIServer(t *testing.T) {
|
||||
ip := os.Getenv("FAKE_KUBERNETES_API_IP")
|
||||
if ip == "" {
|
||||
t.Skip("not a real test, set FAKE_KUBERNETES_API_IP to run")
|
||||
}
|
||||
|
||||
d := t.TempDir()
|
||||
kube := &kubeServer{FSRoot: d}
|
||||
kube.Start(t, ip)
|
||||
defer kube.Close()
|
||||
|
||||
t.Logf("Fake Kubernetes API server running at https://%s:%s", kube.Host, kube.Port)
|
||||
t.Logf("Read secret:\ncurl --silent --insecure -H \"Authorization: Bearer bearer_token\" https://%s:%s/api/v1/namespaces/default/secrets/tailscale | jq", kube.Host, kube.Port)
|
||||
t.Logf("Run the client:\ndocker run --rm -e TS_USERSPACE=false -e TS_AUTHKEY -e TS_AUTH_ONCE=true --device /dev/net/tun --cap-add NET_ADMIN --cap-add NET_RAW -e KUBERNETES_SERVICE_HOST=%s -e KUBERNETES_SERVICE_PORT_HTTPS=%s -v %s/var/run/secrets:/var/run/secrets -e TS_KUBERNETES_READ_API_SERVER_ADDRESS_FROM_ENV=true tailscale:test", kube.Host, kube.Port, d)
|
||||
|
||||
<-make(chan struct{})
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user