blob: 6ceb94018625b02d682b222e73abdf8cb2adcb45 [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package testing
import (
_ "embed"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"path"
"strings"
"sync"
"testing"
"go.uber.org/atomic"
)
const (
ExpectedNamespace = "default"
ExpectedPodName = "shell-demo"
)
// Pull real-life-based testing data in from files at compile time.
// We decided to embed them in the test binary because of past issues
// with reading files that we encountered on CI workers.
//go:embed ca.crt
var caCrt string
//go:embed resp-get-pod.json
var getPodResponse string
//go:embed resp-not-found.json
var notFoundResponse string
//go:embed resp-update-pod.json
var updatePodTagsResponse string
//go:embed token
var token string
var (
// ReturnGatewayTimeouts toggles whether the test server should return,
// well, gateway timeouts...
ReturnGatewayTimeouts = atomic.NewBool(false)
pathToFiles = func() string {
wd, _ := os.Getwd()
repoName := "vault-enterprise"
if !strings.Contains(wd, repoName) {
repoName = "vault"
}
pathParts := strings.Split(wd, repoName)
return pathParts[0] + "vault/serviceregistration/kubernetes/testing/"
}()
)
// Conf returns the info needed to configure the client to point at
// the test server. This must be done by the caller to avoid an import
// cycle between the client and the testserver. Example usage:
//
// client.Scheme = testConf.ClientScheme
// client.TokenFile = testConf.PathToTokenFile
// client.RootCAFile = testConf.PathToRootCAFile
// if err := os.Setenv(client.EnvVarKubernetesServiceHost, testConf.ServiceHost); err != nil {
// t.Fatal(err)
// }
// if err := os.Setenv(client.EnvVarKubernetesServicePort, testConf.ServicePort); err != nil {
// t.Fatal(err)
// }
type Conf struct {
ClientScheme, PathToTokenFile, PathToRootCAFile, ServiceHost, ServicePort string
}
// Server returns an http test server that can be used to test
// Kubernetes client code. It also retains the current state,
// and a func to close the server and to clean up any temporary
// files.
func Server(t *testing.T) (testState *State, testConf *Conf, closeFunc func()) {
testState = &State{m: &sync.Map{}}
testConf = &Conf{
ClientScheme: "http://",
}
// We're going to have multiple close funcs to call.
var closers []func()
closeFunc = func() {
for _, closer := range closers {
closer()
}
}
// Plant our token in a place where it can be read for the config.
tmpToken, err := ioutil.TempFile("", "token")
if err != nil {
t.Fatal(err)
}
closers = append(closers, func() {
os.Remove(tmpToken.Name())
})
if _, err = tmpToken.WriteString(token); err != nil {
closeFunc()
t.Fatal(err)
}
if err := tmpToken.Close(); err != nil {
closeFunc()
t.Fatal(err)
}
testConf.PathToTokenFile = tmpToken.Name()
tmpCACrt, err := ioutil.TempFile("", "ca.crt")
if err != nil {
closeFunc()
t.Fatal(err)
}
closers = append(closers, func() {
os.Remove(tmpCACrt.Name())
})
if _, err = tmpCACrt.WriteString(caCrt); err != nil {
closeFunc()
t.Fatal(err)
}
if err := tmpCACrt.Close(); err != nil {
closeFunc()
t.Fatal(err)
}
testConf.PathToRootCAFile = tmpCACrt.Name()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if ReturnGatewayTimeouts.Load() {
w.WriteHeader(504)
return
}
namespace, podName, err := parsePath(r.URL.Path)
if err != nil {
w.WriteHeader(400)
w.Write([]byte(fmt.Sprintf("unable to parse %s: %s", r.URL.Path, err.Error())))
return
}
switch {
case namespace != ExpectedNamespace, podName != ExpectedPodName:
w.WriteHeader(404)
w.Write([]byte(notFoundResponse))
return
case r.Method == http.MethodGet:
w.WriteHeader(200)
w.Write([]byte(getPodResponse))
return
case r.Method == http.MethodPatch:
var patches []interface{}
if err := json.NewDecoder(r.Body).Decode(&patches); err != nil {
w.WriteHeader(400)
w.Write([]byte(fmt.Sprintf("unable to decode patches %s: %s", r.URL.Path, err.Error())))
return
}
for _, patch := range patches {
patchMap := patch.(map[string]interface{})
p := patchMap["path"].(string)
testState.store(p, patchMap)
}
w.WriteHeader(200)
w.Write([]byte(updatePodTagsResponse))
return
default:
w.WriteHeader(400)
w.Write([]byte(fmt.Sprintf("unexpected request method: %s", r.Method)))
}
}))
closers = append(closers, ts.Close)
// ts.URL example: http://127.0.0.1:35681
urlFields := strings.Split(ts.URL, "://")
if len(urlFields) != 2 {
closeFunc()
t.Fatal("received unexpected test url: " + ts.URL)
}
urlFields = strings.Split(urlFields[1], ":")
if len(urlFields) != 2 {
closeFunc()
t.Fatal("received unexpected test url: " + ts.URL)
}
testConf.ServiceHost = urlFields[0]
testConf.ServicePort = urlFields[1]
return testState, testConf, closeFunc
}
type State struct {
m *sync.Map
}
func (s *State) NumPatches() int {
l := 0
f := func(key, value interface{}) bool {
l++
return true
}
s.m.Range(f)
return l
}
func (s *State) Get(key string) map[string]interface{} {
v, ok := s.m.Load(key)
if !ok {
return nil
}
patch, ok := v.(map[string]interface{})
if !ok {
return nil
}
return patch
}
func (s *State) store(k string, p map[string]interface{}) {
s.m.Store(k, p)
}
// The path should be formatted like this:
// fmt.Sprintf("/api/v1/namespaces/%s/pods/%s", namespace, podName)
func parsePath(urlPath string) (namespace, podName string, err error) {
original := urlPath
podName = path.Base(urlPath)
urlPath = strings.TrimSuffix(urlPath, "/pods/"+podName)
namespace = path.Base(urlPath)
if original != fmt.Sprintf("/api/v1/namespaces/%s/pods/%s", namespace, podName) {
return "", "", fmt.Errorf("received unexpected path: %s", original)
}
return namespace, podName, nil
}
func readFile(fileName string) (string, error) {
b, err := ioutil.ReadFile(pathToFiles + fileName)
if err != nil {
return "", err
}
return string(b), nil
}