Only minimal tailscale + tailscaled for now. And a super minimal in-memory logcatcher. No control ... yet. Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>main
parent
08782b92f7
commit
80429b97e5
@ -0,0 +1,175 @@ |
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package integration contains Tailscale integration tests.
|
||||
package integration |
||||
|
||||
import ( |
||||
"bytes" |
||||
"encoding/json" |
||||
"fmt" |
||||
"io" |
||||
"io/ioutil" |
||||
"log" |
||||
"net/http" |
||||
"net/http/httptest" |
||||
"os" |
||||
"os/exec" |
||||
"path" |
||||
"path/filepath" |
||||
"runtime" |
||||
"strings" |
||||
"sync" |
||||
"testing" |
||||
"time" |
||||
|
||||
"go4.org/mem" |
||||
"tailscale.com/smallzstd" |
||||
"tailscale.com/tstest" |
||||
) |
||||
|
||||
func TestIntegration(t *testing.T) { |
||||
if runtime.GOOS == "windows" { |
||||
t.Skip("not tested/working on Windows yet") |
||||
} |
||||
td := t.TempDir() |
||||
daemonExe := build(t, td, "tailscale.com/cmd/tailscaled") |
||||
cliExe := build(t, td, "tailscale.com/cmd/tailscale") |
||||
|
||||
logc := new(logCatcher) |
||||
ts := httptest.NewServer(logc) |
||||
defer ts.Close() |
||||
|
||||
socketPath := filepath.Join(td, "tailscale.sock") |
||||
dcmd := exec.Command(daemonExe, |
||||
"--tun=userspace-networking", |
||||
"--state="+filepath.Join(td, "tailscale.state"), |
||||
"--socket="+socketPath, |
||||
) |
||||
dcmd.Env = append(os.Environ(), "TS_LOG_TARGET="+ts.URL) |
||||
if err := dcmd.Start(); err != nil { |
||||
t.Fatalf("starting tailscaled: %v", err) |
||||
} |
||||
defer dcmd.Process.Kill() |
||||
|
||||
var json []byte |
||||
if err := tstest.WaitFor(20*time.Second, func() (err error) { |
||||
json, err = exec.Command(cliExe, "--socket="+socketPath, "status", "--json").CombinedOutput() |
||||
if err != nil { |
||||
return fmt.Errorf("running tailscale status: %v, %s", err, json) |
||||
} |
||||
return nil |
||||
}); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
if err := tstest.WaitFor(20*time.Second, func() error { |
||||
const sub = `Program starting: ` |
||||
if !logc.logsContains(mem.S(sub)) { |
||||
return fmt.Errorf("log catcher didn't see %#q; got %s", sub, logc.logsString()) |
||||
} |
||||
return nil |
||||
}); err != nil { |
||||
t.Error(err) |
||||
} |
||||
|
||||
dcmd.Process.Signal(os.Interrupt) |
||||
|
||||
ps, err := dcmd.Process.Wait() |
||||
if err != nil { |
||||
t.Fatalf("tailscaled Wait: %v", err) |
||||
} |
||||
if ps.ExitCode() != 0 { |
||||
t.Errorf("tailscaled ExitCode = %d; want 0", ps.ExitCode()) |
||||
} |
||||
|
||||
t.Logf("number of HTTP logcatcher requests: %v", logc.numRequests()) |
||||
} |
||||
|
||||
func build(t *testing.T, outDir, target string) string { |
||||
exe := "" |
||||
if runtime.GOOS == "windows" { |
||||
exe = ".exe" |
||||
} |
||||
bin := filepath.Join(outDir, path.Base(target)) + exe |
||||
errOut, err := exec.Command("go", "build", "-o", bin, target).CombinedOutput() |
||||
if err != nil { |
||||
t.Fatalf("failed to build %v: %v, %s", target, err, errOut) |
||||
} |
||||
return bin |
||||
} |
||||
|
||||
type logCatcher struct { |
||||
mu sync.Mutex |
||||
buf bytes.Buffer |
||||
gotErr error |
||||
reqs int |
||||
} |
||||
|
||||
func (lc *logCatcher) logsContains(sub mem.RO) bool { |
||||
lc.mu.Lock() |
||||
defer lc.mu.Unlock() |
||||
return mem.Contains(mem.B(lc.buf.Bytes()), sub) |
||||
} |
||||
|
||||
func (lc *logCatcher) numRequests() int { |
||||
lc.mu.Lock() |
||||
defer lc.mu.Unlock() |
||||
return lc.reqs |
||||
} |
||||
|
||||
func (lc *logCatcher) logsString() string { |
||||
lc.mu.Lock() |
||||
defer lc.mu.Unlock() |
||||
return lc.buf.String() |
||||
} |
||||
|
||||
func (lc *logCatcher) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
||||
var body io.Reader = r.Body |
||||
if r.Header.Get("Content-Encoding") == "zstd" { |
||||
var err error |
||||
body, err = smallzstd.NewDecoder(body) |
||||
if err != nil { |
||||
log.Printf("bad caught zstd: %v", err) |
||||
http.Error(w, err.Error(), 400) |
||||
return |
||||
} |
||||
} |
||||
bodyBytes, _ := ioutil.ReadAll(body) |
||||
|
||||
type Entry struct { |
||||
Logtail struct { |
||||
ClientTime time.Time `json:"client_time"` |
||||
ServerTime time.Time `json:"server_time"` |
||||
Error struct { |
||||
BadData string `json:"bad_data"` |
||||
} `json:"error"` |
||||
} `json:"logtail"` |
||||
Text string `json:"text"` |
||||
} |
||||
var jreq []Entry |
||||
var err error |
||||
if len(bodyBytes) > 0 && bodyBytes[0] == '[' { |
||||
err = json.Unmarshal(bodyBytes, &jreq) |
||||
} else { |
||||
var ent Entry |
||||
err = json.Unmarshal(bodyBytes, &ent) |
||||
jreq = append(jreq, ent) |
||||
} |
||||
|
||||
lc.mu.Lock() |
||||
defer lc.mu.Unlock() |
||||
lc.reqs++ |
||||
if lc.gotErr == nil && err != nil { |
||||
lc.gotErr = err |
||||
} |
||||
if err != nil { |
||||
fmt.Fprintf(&lc.buf, "error from %s of %#q: %v\n", r.Method, bodyBytes, err) |
||||
} else { |
||||
for _, ent := range jreq { |
||||
fmt.Fprintf(&lc.buf, "%s\n", strings.TrimSpace(ent.Text)) |
||||
} |
||||
} |
||||
w.WriteHeader(200) // must have no content, but not a 204
|
||||
} |
||||
Loading…
Reference in new issue