mirror of https://github.com/linkerd/linkerd2.git
262 lines
7.0 KiB
Go
262 lines
7.0 KiB
Go
package testutil
|
|
|
|
import (
|
|
"bytes"
|
|
"flag"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// TestHelper provides helpers for running the linkerd integration tests.
|
|
type TestHelper struct {
|
|
linkerd string
|
|
version string
|
|
namespace string
|
|
singleNamespace bool
|
|
tls bool
|
|
httpClient http.Client
|
|
KubernetesHelper
|
|
}
|
|
|
|
// NewTestHelper creates a new instance of TestHelper for the current test run.
|
|
// The new TestHelper can be configured via command line flags.
|
|
func NewTestHelper() *TestHelper {
|
|
exit := func(code int, msg string) {
|
|
fmt.Fprintln(os.Stderr, msg)
|
|
os.Exit(code)
|
|
}
|
|
|
|
linkerd := flag.String("linkerd", "", "path to the linkerd binary to test")
|
|
namespace := flag.String("linkerd-namespace", "l5d-integration", "the namespace where linkerd is installed")
|
|
singleNamespace := flag.Bool("single-namespace", false, "configure the control plane to only operate in the installed namespace")
|
|
tls := flag.Bool("enable-tls", false, "enable TLS in tests")
|
|
runTests := flag.Bool("integration-tests", false, "must be provided to run the integration tests")
|
|
verbose := flag.Bool("verbose", false, "turn on debug logging")
|
|
flag.Parse()
|
|
|
|
if !*runTests {
|
|
exit(0, "integration tests not enabled: enable with -integration-tests")
|
|
}
|
|
|
|
if *linkerd == "" {
|
|
exit(1, "-linkerd flag is required")
|
|
}
|
|
|
|
if !filepath.IsAbs(*linkerd) {
|
|
exit(1, "-linkerd path must be absolute")
|
|
}
|
|
|
|
_, err := os.Stat(*linkerd)
|
|
if err != nil {
|
|
exit(1, "-linkerd binary does not exist")
|
|
}
|
|
|
|
if *verbose {
|
|
log.SetLevel(log.DebugLevel)
|
|
} else {
|
|
log.SetLevel(log.PanicLevel)
|
|
}
|
|
|
|
ns := *namespace
|
|
if *tls {
|
|
ns += "-tls"
|
|
}
|
|
if *singleNamespace {
|
|
ns += "-single-namespace"
|
|
}
|
|
|
|
testHelper := &TestHelper{
|
|
linkerd: *linkerd,
|
|
namespace: ns,
|
|
singleNamespace: *singleNamespace,
|
|
tls: *tls,
|
|
}
|
|
|
|
version, _, err := testHelper.LinkerdRun("version", "--client", "--short")
|
|
if err != nil {
|
|
exit(1, "error getting linkerd version: "+err.Error())
|
|
}
|
|
testHelper.version = strings.TrimSpace(version)
|
|
|
|
kubernetesHelper, err := NewKubernetesHelper(testHelper.RetryFor)
|
|
if err != nil {
|
|
exit(1, "error creating kubernetes helper: "+err.Error())
|
|
}
|
|
testHelper.KubernetesHelper = *kubernetesHelper
|
|
|
|
testHelper.httpClient = http.Client{
|
|
Timeout: 10 * time.Second,
|
|
}
|
|
|
|
return testHelper
|
|
}
|
|
|
|
// GetVersion returns the version of linkerd to test. This version corresponds
|
|
// to the client version of the linkerd binary provided via the -linkerd command
|
|
// line flag.
|
|
func (h *TestHelper) GetVersion() string {
|
|
return h.version
|
|
}
|
|
|
|
// GetLinkerdNamespace returns the namespace where linkerd is installed. Set the
|
|
// namespace using the -linkerd-namespace command line flag.
|
|
func (h *TestHelper) GetLinkerdNamespace() string {
|
|
return h.namespace
|
|
}
|
|
|
|
// GetTestNamespace returns the namespace for the given test. The test namespace
|
|
// is prefixed with the linkerd namespace.
|
|
func (h *TestHelper) GetTestNamespace(testName string) string {
|
|
if h.SingleNamespace() {
|
|
return h.namespace
|
|
}
|
|
return h.namespace + "-" + testName
|
|
}
|
|
|
|
// TLS returns whether or not TLS is enabled for the given test.
|
|
func (h *TestHelper) TLS() bool {
|
|
return h.tls
|
|
}
|
|
|
|
// SingleNamespace returns whether --single-namespace is enabled for the given test or not.
|
|
func (h *TestHelper) SingleNamespace() bool {
|
|
return h.singleNamespace
|
|
}
|
|
|
|
// CombinedOutput executes a shell command and returns the output.
|
|
func (h *TestHelper) CombinedOutput(name string, arg ...string) (string, string, error) {
|
|
command := exec.Command(name, arg...)
|
|
var stderr bytes.Buffer
|
|
command.Stderr = &stderr
|
|
|
|
stdout, err := command.Output()
|
|
return string(stdout), stderr.String(), err
|
|
}
|
|
|
|
// LinkerdRun executes a linkerd command appended with the --linkerd-namespace
|
|
// flag.
|
|
func (h *TestHelper) LinkerdRun(arg ...string) (string, string, error) {
|
|
withNamespace := append(arg, "--linkerd-namespace", h.namespace)
|
|
return h.CombinedOutput(h.linkerd, withNamespace...)
|
|
}
|
|
|
|
// LinkerdRunStream initiates a linkerd command appended with the
|
|
// --linkerd-namespace flag, and returns a Stream that can be used to read the
|
|
// command's output while it is still executing.
|
|
func (h *TestHelper) LinkerdRunStream(arg ...string) (*Stream, error) {
|
|
withNamespace := append(arg, "--linkerd-namespace", h.namespace)
|
|
cmd := exec.Command(h.linkerd, withNamespace...)
|
|
|
|
cmdReader, err := cmd.StdoutPipe()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = cmd.Start()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
if cmd.ProcessState != nil && cmd.ProcessState.Exited() {
|
|
return nil, fmt.Errorf("Process exited: %s", cmd.ProcessState)
|
|
}
|
|
|
|
return &Stream{cmd: cmd, out: cmdReader}, nil
|
|
}
|
|
|
|
// ValidateOutput validates a string against the contents of a file in the
|
|
// test's testdata directory.
|
|
func (h *TestHelper) ValidateOutput(out, fixtureFile string) error {
|
|
b, err := ioutil.ReadFile("testdata/" + fixtureFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
expected := string(b)
|
|
|
|
if out != expected {
|
|
return fmt.Errorf(
|
|
"Expected:\n%s\nActual:\n%s", expected, out)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CheckVersion validates the the output of the "linkerd version" command.
|
|
func (h *TestHelper) CheckVersion(serverVersion string) error {
|
|
out, _, err := h.LinkerdRun("version")
|
|
if err != nil {
|
|
return fmt.Errorf("Unexpected error: %s\n%s", err.Error(), out)
|
|
}
|
|
if !strings.Contains(out, fmt.Sprintf("Client version: %s", h.version)) {
|
|
return fmt.Errorf("Expected client version [%s], got:\n%s", h.version, out)
|
|
}
|
|
if !strings.Contains(out, fmt.Sprintf("Server version: %s", serverVersion)) {
|
|
return fmt.Errorf("Expected server version [%s], got:\n%s", serverVersion, out)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RetryFor retries a given function every second until the function returns
|
|
// without an error, or a timeout is reached. If the timeout is reached, it
|
|
// returns the last error received from the function.
|
|
func (h *TestHelper) RetryFor(timeout time.Duration, fn func() error) error {
|
|
err := fn()
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
timeoutAfter := time.After(timeout)
|
|
retryAfter := time.Tick(time.Second)
|
|
|
|
for {
|
|
select {
|
|
case <-timeoutAfter:
|
|
return err
|
|
case <-retryAfter:
|
|
err = fn()
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// HTTPGetURL sends a GET request to the given URL. It returns the response body
|
|
// in the event of a successful 200 response. In the event of a non-200
|
|
// response, it returns an error. It retries requests for up to 30 seconds,
|
|
// giving pods time to start.
|
|
func (h *TestHelper) HTTPGetURL(url string) (string, error) {
|
|
var body string
|
|
err := h.RetryFor(time.Minute, func() error {
|
|
resp, err := h.httpClient.Get(url)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
bytes, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return fmt.Errorf("Error reading response body: %v", err)
|
|
}
|
|
body = string(bytes)
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("GET request to [%s] returned status [%d]\n%s", url, resp.StatusCode, body)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
return body, err
|
|
}
|