diff --git a/Makefile b/Makefile index e577fc6..8911709 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,10 @@ run-image: docker run ghcr.io/linuxsuren/api-testing:dev copy: build sudo cp bin/atest /usr/local/bin/ +copy-restart: + atest service stop + make copy + atest service restart test: go test ./... -cover -v -coverprofile=coverage.out go tool cover -func=coverage.out diff --git a/README.md b/README.md index 44db562..be594e1 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ This is a API testing tool. * Response Body fields equation check * Response Body [eval](https://expr.medv.io/) +* Verify the Kubernetes resources * Validate the response body with [JSON schema](https://json-schema.org/) * Output reference between TestCase * Run in server mode, and provide the gRPC endpoint @@ -62,6 +63,15 @@ The following fields are templated with [sprig](http://masterminds.github.io/spr * Request Body * Request Header +## Verify against Kubernetes + +It could verify any kinds of Kubernetes resources. Please set the environment variables before using it: + +* `KUBERNETES_SERVER` +* `KUBERNETES_TOKEN` + +See also the [example](sample/kubernetes.yaml). + ## TODO * Reduce the size of context diff --git a/cmd/jsonschema_test.go b/cmd/jsonschema_test.go index 830d675..d7539c8 100644 --- a/cmd/jsonschema_test.go +++ b/cmd/jsonschema_test.go @@ -11,7 +11,7 @@ import ( ) func TestJSONSchemaCmd(t *testing.T) { - c := cmd.NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}) + c := cmd.NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}, cmd.NewFakeGRPCServer()) buf := new(bytes.Buffer) c.SetOut(buf) diff --git a/cmd/root.go b/cmd/root.go index 7f01221..aaee82a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -6,18 +6,16 @@ import ( "github.com/linuxsuren/api-testing/pkg/version" fakeruntime "github.com/linuxsuren/go-fake-runtime" "github.com/spf13/cobra" - "google.golang.org/grpc" ) // NewRootCmd creates the root command -func NewRootCmd(execer fakeruntime.Execer) (c *cobra.Command) { +func NewRootCmd(execer fakeruntime.Execer, gRPCServer gRPCServer) (c *cobra.Command) { c = &cobra.Command{ Use: "atest", Short: "API testing tool", } c.SetOut(os.Stdout) c.Version = version.GetVersion() - gRPCServer := grpc.NewServer() c.AddCommand(createInitCommand(execer), createRunCommand(), createSampleCmd(), createServerCmd(gRPCServer), createJSONSchemaCmd(), diff --git a/cmd/root_test.go b/cmd/root_test.go index 87f7afe..cea9c33 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -51,7 +51,7 @@ func TestCreateRunCommand(t *testing.T) { assert.NotNil(t, server) assert.Equal(t, "server", server.Use) - root := NewRootCmd(exec.FakeExecer{}) + root := NewRootCmd(exec.FakeExecer{}, NewFakeGRPCServer()) root.SetArgs([]string{"init", "-k=demo.yaml", "--wait-namespace", "demo", "--wait-resource", "demo"}) err := root.Execute() assert.Nil(t, err) diff --git a/cmd/run_test.go b/cmd/run_test.go index 3dac0d7..778742d 100644 --- a/cmd/run_test.go +++ b/cmd/run_test.go @@ -104,7 +104,7 @@ func TestRunCommand(t *testing.T) { } func TestRootCmd(t *testing.T) { - c := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}) + c := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}, NewFakeGRPCServer()) assert.NotNil(t, c) assert.Equal(t, "atest", c.Use) } diff --git a/cmd/sample_test.go b/cmd/sample_test.go index eb81957..82d977c 100644 --- a/cmd/sample_test.go +++ b/cmd/sample_test.go @@ -11,7 +11,7 @@ import ( ) func TestSampleCmd(t *testing.T) { - c := cmd.NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}) + c := cmd.NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}, cmd.NewFakeGRPCServer()) buf := new(bytes.Buffer) c.SetOut(buf) diff --git a/cmd/server.go b/cmd/server.go index f770902..ef04ddc 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -59,6 +59,11 @@ type gRPCServer interface { type fakeGRPCServer struct { } +// NewFakeGRPCServer creates a fake gRPC server +func NewFakeGRPCServer() gRPCServer { + return &fakeGRPCServer{} +} + // Serve is a fake method func (s *fakeGRPCServer) Serve(net.Listener) error { return nil diff --git a/cmd/server_test.go b/cmd/server_test.go index f941fe8..b81d2e3 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -27,19 +27,21 @@ func TestPrintProto(t *testing.T) { verify: func(t *testing.T, buf *bytes.Buffer, err error) { assert.NotNil(t, err) }, + }, { + name: "random port", + args: []string{"server", "-p=0"}, + verify: func(t *testing.T, buf *bytes.Buffer, err error) { + assert.Nil(t, err) + }, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { buf := new(bytes.Buffer) - root := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}) + root := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}, &fakeGRPCServer{}) root.SetOut(buf) root.SetArgs(tt.args) err := root.Execute() tt.verify(t, buf, err) }) } - - server := createServerCmd(&fakeGRPCServer{}) - err := server.Execute() - assert.Nil(t, err) } diff --git a/cmd/service_test.go b/cmd/service_test.go index d70e0bb..5fb124b 100644 --- a/cmd/service_test.go +++ b/cmd/service_test.go @@ -10,12 +10,12 @@ import ( ) func TestService(t *testing.T) { - root := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}) + root := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}, NewFakeGRPCServer()) root.SetArgs([]string{"service", "fake"}) err := root.Execute() assert.NotNil(t, err) - notLinux := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "fake"}) + notLinux := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "fake"}, NewFakeGRPCServer()) notLinux.SetArgs([]string{"service", "--action", "install"}) err = notLinux.Execute() assert.NotNil(t, err) @@ -26,7 +26,7 @@ func TestService(t *testing.T) { os.RemoveAll(tmpFile.Name()) }() - targetScript := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}) + targetScript := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}, NewFakeGRPCServer()) targetScript.SetArgs([]string{"service", "--action", "install", "--script-path", tmpFile.Name()}) err = targetScript.Execute() assert.Nil(t, err) @@ -58,7 +58,7 @@ func TestService(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { buf := new(bytes.Buffer) - normalRoot := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux", ExpectOutput: tt.expectOutput}) + normalRoot := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux", ExpectOutput: tt.expectOutput}, NewFakeGRPCServer()) normalRoot.SetOut(buf) normalRoot.SetArgs([]string{"service", "--action", tt.action}) err = normalRoot.Execute() diff --git a/main.go b/main.go index 8a5db53..c0cffb7 100644 --- a/main.go +++ b/main.go @@ -5,10 +5,12 @@ import ( "github.com/linuxsuren/api-testing/cmd" exec "github.com/linuxsuren/go-fake-runtime" + "google.golang.org/grpc" ) func main() { - c := cmd.NewRootCmd(exec.DefaultExecer{}) + gRPCServer := grpc.NewServer() + c := cmd.NewRootCmd(exec.DefaultExecer{}, gRPCServer) if err := c.Execute(); err != nil { os.Exit(1) } diff --git a/pkg/exec/command.go b/pkg/exec/command.go deleted file mode 100644 index 02ed261..0000000 --- a/pkg/exec/command.go +++ /dev/null @@ -1,110 +0,0 @@ -package exec - -import ( - "bytes" - "io" - "os" - "os/exec" - "sync" -) - -// LookPath is the wrapper of os/exec.LookPath -func LookPath(file string) (string, error) { - return exec.LookPath(file) -} - -// RunCommandAndReturn runs a command, then returns the output -func RunCommandAndReturn(name, dir string, args ...string) (result string, err error) { - stdout := &bytes.Buffer{} - if err = RunCommandWithBuffer(name, dir, stdout, nil, args...); err == nil { - result = stdout.String() - } - return -} - -// RunCommandWithBuffer runs a command with buffer -// stdout and stderr could be nil -func RunCommandWithBuffer(name, dir string, stdout, stderr *bytes.Buffer, args ...string) error { - if stdout == nil { - stdout = &bytes.Buffer{} - } - if stderr != nil { - stderr = &bytes.Buffer{} - } - return RunCommandWithIO(name, dir, stdout, stderr, args...) -} - -// RunCommandWithIO runs a command with given IO -func RunCommandWithIO(name, dir string, stdout, stderr io.Writer, args ...string) (err error) { - command := exec.Command(name, args...) - if dir != "" { - command.Dir = dir - } - - //var stdout []byte - //var errStdout error - stdoutIn, _ := command.StdoutPipe() - stderrIn, _ := command.StderrPipe() - err = command.Start() - if err != nil { - return - } - - // cmd.Wait() should be called only after we finish reading - // from stdoutIn and stderrIn. - // wg ensures that we finish - var wg sync.WaitGroup - wg.Add(1) - go func() { - _, _ = copyAndCapture(stdout, stdoutIn) - wg.Done() - }() - - _, _ = copyAndCapture(stderr, stderrIn) - - wg.Wait() - - err = command.Wait() - return -} - -// RunCommandInDir runs a command -func RunCommandInDir(name, dir string, args ...string) error { - return RunCommandWithIO(name, dir, os.Stdout, os.Stderr, args...) -} - -// RunCommand runs a command -func RunCommand(name string, arg ...string) (err error) { - return RunCommandInDir(name, "", arg...) -} - -// RunCommandWithSudo runs a command with sudo -func RunCommandWithSudo(name string, args ...string) (err error) { - newArgs := make([]string, 0) - newArgs = append(newArgs, name) - newArgs = append(newArgs, args...) - return RunCommand("sudo", newArgs...) -} - -func copyAndCapture(w io.Writer, r io.Reader) ([]byte, error) { - var out []byte - buf := make([]byte, 1024, 1024) - for { - n, err := r.Read(buf[:]) - if n > 0 { - d := buf[:n] - out = append(out, d...) - _, err := w.Write(d) - if err != nil { - return out, err - } - } - if err != nil { - // Read returns io.EOF at the end of file, which is not an error for us - if err == io.EOF { - err = nil - } - return out, err - } - } -} diff --git a/pkg/runner/kubernetes/client.go b/pkg/runner/kubernetes/client.go new file mode 100644 index 0000000..88bb772 --- /dev/null +++ b/pkg/runner/kubernetes/client.go @@ -0,0 +1,158 @@ +package kubernetes + +import ( + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "os" + "strings" + + unstructured "github.com/linuxsuren/unstructured/pkg" +) + +// Reader represents a reader interface +type Reader interface { + GetResource(group, kind, version, namespace, name string) (map[string]interface{}, error) +} + +type defualtReader struct { + server string + token string +} + +// NewDefaultReader returns a reader implement +func NewDefaultReader(server, token string) Reader { + return &defualtReader{ + server: server, + token: token, + } +} + +// GetResource returns the resource +func (r *defualtReader) GetResource(group, kind, version, namespace, name string) (map[string]interface{}, error) { + api := fmt.Sprintf("%s/api/%s/%s/namespaces/%s/%s/%s", r.server, group, version, namespace, kind, name) + api = strings.ReplaceAll(api, "api//", "api/") + if !strings.Contains(api, "api/v1") { + api = strings.ReplaceAll(api, "api/", "apis/") + } + return r.request(api) +} + +func (r *defualtReader) request(api string) (result map[string]interface{}, err error) { + client := GetClient() + var req *http.Request + if req, err = http.NewRequest(http.MethodGet, api, nil); err == nil { + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.token)) + var resp *http.Response + if resp, err = client.Do(req); err == nil && resp.StatusCode == http.StatusOK { + var data []byte + if data, err = io.ReadAll(resp.Body); err == nil { + result = make(map[string]interface{}) + + err = json.Unmarshal(data, &result) + } + } + } + return +} + +var client *http.Client + +// GetClient returns a default client +func GetClient() *http.Client { + if client == nil { + client = &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, + } + } + return client +} + +// ResourceValidator represents a generic resource validator +type ResourceValidator interface { + Exist() bool + ExpectField(value interface{}, fields ...string) bool +} + +type defaultResourceValidator struct { + data map[string]interface{} + err error +} + +func (v *defaultResourceValidator) Exist() bool { + if v.err != nil { + fmt.Println(v.err) + return false + } + return v.data != nil && len(v.data) > 0 +} + +func (v *defaultResourceValidator) ExpectField(value interface{}, fields ...string) (result bool) { + val, ok, err := unstructured.NestedField(v.data, fields...) + if !ok || err != nil { + fmt.Printf("cannot find '%v',error: %v\n", fields, err) + return + } + if result = fmt.Sprintf("%v", val) == fmt.Sprintf("%v", value); !result { + fmt.Printf("expect: '%v', actual: '%v'\n", value, val) + } + return +} + +func podValidator(params ...interface{}) (validator interface{}, err error) { + return resourceValidator(append([]interface{}{"pods"}, params...)...) +} + +func resourceValidator(params ...interface{}) (validator interface{}, err error) { + if len(params) < 3 { + err = errors.New("there are three params at least") + return + } + + var kind string + version := "v1" + group := "" + switch obj := params[0].(type) { + case string: + kind = obj + case map[string]interface{}: + if obj["kind"] != nil { + kind = obj["kind"].(string) + } + if obj["version"] != nil { + version = obj["version"].(string) + } + if obj["group"] != nil { + group = obj["group"].(string) + } + } + + if kind == "" { + err = errors.New("kind is required") + return + } + + switch kind { + case "deployments", "statefulsets", "daemonsets": + group = "apps" + } + + server := os.Getenv("KUBERNETES_SERVER") + token := os.Getenv("KUBERNETES_TOKEN") + if server == "" || token == "" { + err = errors.New("KUBERNETES_SERVER and KUBERNETES_TOKEN are required") + return + } + reader := NewDefaultReader(server, token) + data, err := reader.GetResource(group, kind, version, params[1].(string), params[2].(string)) + validator = &defaultResourceValidator{ + data: data, + err: err, + } + return +} diff --git a/pkg/runner/kubernetes/client_test.go b/pkg/runner/kubernetes/client_test.go new file mode 100644 index 0000000..422e3cc --- /dev/null +++ b/pkg/runner/kubernetes/client_test.go @@ -0,0 +1,74 @@ +package kubernetes_test + +import ( + "net/http" + "testing" + + "github.com/h2non/gock" + "github.com/linuxsuren/api-testing/pkg/runner/kubernetes" + "github.com/stretchr/testify/assert" +) + +func TestGetPod(t *testing.T) { + tests := []struct { + name string + group string + version string + kind string + namespacedName namespacedName + prepare func() + expect map[string]interface{} + }{{ + name: "normal", + kind: "pods", + version: "v1", + namespacedName: namespacedName{ + namespace: "ns", + name: "fake", + }, + prepare: func() { + gock.New("http://foo"). + Get("/api/v1/namespaces/ns/pods/fake"). + Reply(http.StatusOK). + JSON(`{"kind":"pod"}`) + gock.InterceptClient(kubernetes.GetClient()) + }, + expect: map[string]interface{}{ + "kind": "pod", + }, + }, { + name: "deployments", + kind: "deployments", + version: "v1", + group: "apps", + namespacedName: namespacedName{ + namespace: "ns", + name: "fake", + }, + prepare: func() { + gock.New("http://foo"). + Get("/apis/apps/v1/namespaces/ns/deployments/fake"). + Reply(http.StatusOK). + JSON(`{"kind":"deployment"}`) + gock.InterceptClient(kubernetes.GetClient()) + }, + expect: map[string]interface{}{ + "kind": "deployment", + }, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer gock.Clean() + tt.prepare() + reader := kubernetes.NewDefaultReader("http://foo", "") + result, err := reader.GetResource(tt.group, tt.kind, tt.version, tt.namespacedName.namespace, tt.namespacedName.name) + assert.Equal(t, tt.expect, result) + assert.Nil(t, err) + }) + } +} + +type namespacedName struct { + namespace string + name string +} diff --git a/pkg/runner/kubernetes/doc.go b/pkg/runner/kubernetes/doc.go new file mode 100644 index 0000000..6efd788 --- /dev/null +++ b/pkg/runner/kubernetes/doc.go @@ -0,0 +1,2 @@ +// Package kubernetes provides a low level client for small footprint consideration +package kubernetes diff --git a/pkg/runner/kubernetes/verify.go b/pkg/runner/kubernetes/verify.go new file mode 100644 index 0000000..2bb919d --- /dev/null +++ b/pkg/runner/kubernetes/verify.go @@ -0,0 +1,13 @@ +package kubernetes + +import "github.com/antonmedv/expr" + +// PodValidatorFunc returns a expr for checking pod existing +func PodValidatorFunc() expr.Option { + return expr.Function("pod", podValidator, new(func(...string) ResourceValidator)) +} + +// KubernetesValidatorFunc returns a expr for checking the generic Kubernetes resources +func KubernetesValidatorFunc() expr.Option { + return expr.Function("k8s", resourceValidator, new(func(interface{}, ...string) ResourceValidator)) +} diff --git a/pkg/runner/kubernetes/verify_test.go b/pkg/runner/kubernetes/verify_test.go new file mode 100644 index 0000000..b3b4be6 --- /dev/null +++ b/pkg/runner/kubernetes/verify_test.go @@ -0,0 +1,135 @@ +package kubernetes_test + +import ( + "net/http" + "os" + "testing" + + "github.com/antonmedv/expr" + "github.com/h2non/gock" + "github.com/linuxsuren/api-testing/pkg/runner/kubernetes" + "github.com/stretchr/testify/assert" +) + +func TestKubernetesValidatorFunc(t *testing.T) { + os.Setenv("KUBERNETES_SERVER", "http://foo") + os.Setenv("KUBERNETES_TOKEN", "token") + gock.InterceptClient(kubernetes.GetClient()) + defer gock.RestoreClient(http.DefaultClient) + defer gock.Off() + + tests := []struct { + name string + prepare func() + expression string + expectBool bool + expectErr bool + }{{ + name: "pod exist expr", + prepare: preparePod, + expression: `pod('ns', 'foo').Exist()`, + expectBool: true, + }, { + name: "pod expectField expr", + prepare: preparePod, + expression: `pod('ns', 'foo').ExpectField('pod', 'kind')`, + expectBool: true, + }, { + name: "pod expectField expr, not match", + prepare: preparePod, + expression: `pod('ns', 'foo').ExpectField('pods', 'kind')`, + expectBool: false, + }, { + name: "pod expectField expr, not find field", + prepare: preparePod, + expression: `pod('ns', 'foo').ExpectField('pods', 'kinds')`, + expectBool: false, + }, { + name: "no enough params", + expression: `k8s('crd')`, + prepare: emptyPrepare, + expectBool: false, + expectErr: true, + }, { + name: "crd", + expression: `k8s({"kind":"vms","group":"bar","version":"v2"}, "ns", "foo").Exist()`, + prepare: prepareCRDVM, + expectBool: true, + }, { + name: "deploy", + expression: `k8s("deployments", "ns", "foo").Exist()`, + prepare: prepareDeploy, + expectBool: true, + }, { + name: "statefulset", + expression: `k8s("statefulsets", "ns", "foo").Exist()`, + prepare: prepareStatefulset, + expectBool: true, + }, { + name: "daemonset", + expression: `k8s("daemonsets", "ns", "foo").Exist()`, + prepare: prepareDaemonset, + expectBool: true, + }, { + name: "no kind", + expression: `k8s({"foo": "bar"}, "ns", "foo").Exist()`, + prepare: emptyPrepare, + expectErr: true, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.prepare() + vm, err := expr.Compile(tt.expression, kubernetes.KubernetesValidatorFunc(), + kubernetes.PodValidatorFunc()) + assert.Nil(t, err) + + result, err := expr.Run(vm, expr.Env(tt)) + assert.Equal(t, tt.expectErr, err != nil) + if err == nil { + assert.Equal(t, tt.expectBool, result) + } + }) + } +} + +func emptyPrepare() {} + +func preparePod() { + gock.New("http://foo"). + Get("/api/v1/namespaces/ns/pods/foo"). + MatchHeader("Authorization", "Bearer token"). + Reply(http.StatusOK). + JSON(`{"kind":"pod"}`) +} + +func prepareDeploy() { + gock.New("http://foo"). + Get("/apis/apps/v1/namespaces/ns/deployments/foo"). + MatchHeader("Authorization", "Bearer token"). + Reply(http.StatusOK). + JSON(`{"kind":"deploy"}`) +} + +func prepareStatefulset() { + gock.New("http://foo"). + Get("/apis/apps/v1/namespaces/ns/statefulsets/foo"). + MatchHeader("Authorization", "Bearer token"). + Reply(http.StatusOK). + JSON(`{"kind":"statefulset"}`) +} + +func prepareDaemonset() { + gock.New("http://foo"). + Get("/apis/apps/v1/namespaces/ns/daemonsets/foo"). + MatchHeader("Authorization", "Bearer token"). + Reply(http.StatusOK). + JSON(`{"kind":"daemonset"}`) +} + +func prepareCRDVM() { + gock.New("http://foo"). + Get("/apis/bar/v2/namespaces/ns/vms/foo"). + MatchHeader("Authorization", "Bearer token"). + Reply(http.StatusOK). + JSON(`{"kind":"vm"}`) +} diff --git a/pkg/runner/simple.go b/pkg/runner/simple.go index efd9e64..8fd46cd 100644 --- a/pkg/runner/simple.go +++ b/pkg/runner/simple.go @@ -3,6 +3,7 @@ package runner import ( "bytes" "context" + "crypto/tls" "encoding/json" "fmt" "io" @@ -17,6 +18,7 @@ import ( "github.com/andreyvit/diff" "github.com/antonmedv/expr" "github.com/antonmedv/expr/vm" + "github.com/linuxsuren/api-testing/pkg/runner/kubernetes" "github.com/linuxsuren/api-testing/pkg/testing" fakeruntime "github.com/linuxsuren/go-fake-runtime" unstructured "github.com/linuxsuren/unstructured/pkg" @@ -193,7 +195,12 @@ func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataConte } }() - client := http.Client{} + client := http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, + } + var requestBody io.Reader if testcase.Request.Body != "" { requestBody = bytes.NewBufferString(testcase.Request.Body) @@ -241,6 +248,11 @@ func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataConte r.log.Info("start to send request to %s\n", testcase.Request.API) + // TODO only do this for unit testing, should remove it once we have a better way + if strings.HasPrefix(testcase.Request.API, "http://") { + client = *http.DefaultClient + } + // send the HTTP request var resp *http.Response if resp, err = client.Do(request); err != nil { @@ -325,7 +337,9 @@ func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataConte for _, verify := range testcase.Expect.Verify { var program *vm.Program - if program, err = expr.Compile(verify, expr.Env(mapOutput), expr.AsBool()); err != nil { + if program, err = expr.Compile(verify, expr.Env(mapOutput), + expr.AsBool(), kubernetes.PodValidatorFunc(), + kubernetes.KubernetesValidatorFunc()); err != nil { return } @@ -336,6 +350,7 @@ func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataConte if !result.(bool) { err = fmt.Errorf("failed to verify: %s", verify) + fmt.Println(err) break } } diff --git a/sample/kubernetes.yaml b/sample/kubernetes.yaml new file mode 100644 index 0000000..54d3070 --- /dev/null +++ b/sample/kubernetes.yaml @@ -0,0 +1,19 @@ +#!api-testing +name: Kubernetes +api: https://192.168.123.121:6443 +items: +- name: pods + request: + api: /api/v1/namespaces/kube-system/pods + header: + Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6Il9zTmhqWDI0aUZadURCWkpCeUhuLUl2S1pYMjczZWJVdFh5M0lwVzkwTzgifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJjbHVzdGVyLWFkbWluLXRva2VuLWg5NTZjIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImNsdXN0ZXItYWRtaW4iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiIyZGQ0NzcyNy0wNDEyLTQyYzYtOTg0NC05OWFiM2JlMDkzMDEiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6a3ViZS1zeXN0ZW06Y2x1c3Rlci1hZG1pbiJ9.fp7kcr2Tgg7O01C0rCs1YEUGynBHKflKnN0K94hTAtelP9CDwTRMj2Y3rHXrvVisjPMXQ_qJtUb9cLL_QXtihgWIQkGZJYD6uQeatWPqRfAE26BZA-bc3Y4RvuTjgWkwR3PNhfoCDiWx-Y0OkLONG90n40f-1Bq_B5zsf_yVHukeUln8UCL0o8Bi7k2TQXycUOToI_BRC1-q7bkME8-WUFMdbbjKkJzW5FHQg1Y4OL2Dd5_Bv24sT6-P5k8DV8btYYUbvpeYMIP_Vzg8T5N9G4TULPGb41KJ1dm66JNNFFjGB7bqOdC7RR32xrB2mNYodP8tDSyeR_as1BxyQoXkZg + expect: + verify: + - data.kind == "PodList" + - pod("kube-system", "kube-ovn-cni-55bz9").Exist() + - k8s("pods", "kube-system", "kube-ovn-cni-55bz9").Exist() + - k8s("deployments", "kube-system", "coredns").Exist() + - k8s("deployments", "kube-system", "coredns").ExpectField(2, "spec", "replicas") + - k8s("deployments", "kube-system", "coredns").ExpectField("kube-dns", "metadata", "labels", "k8s-app") + - k8s("daemonsets", "kube-system", "kube-ovn-cni").Exist() + - k8s({"kind":"virtualmachines","group":"kubevirt.io"}, "vm-test", "vm-win10-dkkhl").Exist()