feat: support verify against Kubernetes resources (#41)
* feat: support check the response againt k8s * feat: support all the CRDs * feat: support to verify fields * add more unit tests
This commit is contained in:
parent
e08c2046d5
commit
f6d271b7eb
4
Makefile
4
Makefile
|
@ -10,6 +10,10 @@ run-image:
|
||||||
docker run ghcr.io/linuxsuren/api-testing:dev
|
docker run ghcr.io/linuxsuren/api-testing:dev
|
||||||
copy: build
|
copy: build
|
||||||
sudo cp bin/atest /usr/local/bin/
|
sudo cp bin/atest /usr/local/bin/
|
||||||
|
copy-restart:
|
||||||
|
atest service stop
|
||||||
|
make copy
|
||||||
|
atest service restart
|
||||||
test:
|
test:
|
||||||
go test ./... -cover -v -coverprofile=coverage.out
|
go test ./... -cover -v -coverprofile=coverage.out
|
||||||
go tool cover -func=coverage.out
|
go tool cover -func=coverage.out
|
||||||
|
|
10
README.md
10
README.md
|
@ -8,6 +8,7 @@ This is a API testing tool.
|
||||||
|
|
||||||
* Response Body fields equation check
|
* Response Body fields equation check
|
||||||
* Response Body [eval](https://expr.medv.io/)
|
* Response Body [eval](https://expr.medv.io/)
|
||||||
|
* Verify the Kubernetes resources
|
||||||
* Validate the response body with [JSON schema](https://json-schema.org/)
|
* Validate the response body with [JSON schema](https://json-schema.org/)
|
||||||
* Output reference between TestCase
|
* Output reference between TestCase
|
||||||
* Run in server mode, and provide the gRPC endpoint
|
* 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 Body
|
||||||
* Request Header
|
* 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
|
## TODO
|
||||||
|
|
||||||
* Reduce the size of context
|
* Reduce the size of context
|
||||||
|
|
|
@ -11,7 +11,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestJSONSchemaCmd(t *testing.T) {
|
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)
|
buf := new(bytes.Buffer)
|
||||||
c.SetOut(buf)
|
c.SetOut(buf)
|
||||||
|
|
|
@ -6,18 +6,16 @@ import (
|
||||||
"github.com/linuxsuren/api-testing/pkg/version"
|
"github.com/linuxsuren/api-testing/pkg/version"
|
||||||
fakeruntime "github.com/linuxsuren/go-fake-runtime"
|
fakeruntime "github.com/linuxsuren/go-fake-runtime"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"google.golang.org/grpc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewRootCmd creates the root command
|
// 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{
|
c = &cobra.Command{
|
||||||
Use: "atest",
|
Use: "atest",
|
||||||
Short: "API testing tool",
|
Short: "API testing tool",
|
||||||
}
|
}
|
||||||
c.SetOut(os.Stdout)
|
c.SetOut(os.Stdout)
|
||||||
c.Version = version.GetVersion()
|
c.Version = version.GetVersion()
|
||||||
gRPCServer := grpc.NewServer()
|
|
||||||
c.AddCommand(createInitCommand(execer),
|
c.AddCommand(createInitCommand(execer),
|
||||||
createRunCommand(), createSampleCmd(),
|
createRunCommand(), createSampleCmd(),
|
||||||
createServerCmd(gRPCServer), createJSONSchemaCmd(),
|
createServerCmd(gRPCServer), createJSONSchemaCmd(),
|
||||||
|
|
|
@ -51,7 +51,7 @@ func TestCreateRunCommand(t *testing.T) {
|
||||||
assert.NotNil(t, server)
|
assert.NotNil(t, server)
|
||||||
assert.Equal(t, "server", server.Use)
|
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"})
|
root.SetArgs([]string{"init", "-k=demo.yaml", "--wait-namespace", "demo", "--wait-resource", "demo"})
|
||||||
err := root.Execute()
|
err := root.Execute()
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
|
@ -104,7 +104,7 @@ func TestRunCommand(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRootCmd(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.NotNil(t, c)
|
||||||
assert.Equal(t, "atest", c.Use)
|
assert.Equal(t, "atest", c.Use)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSampleCmd(t *testing.T) {
|
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)
|
buf := new(bytes.Buffer)
|
||||||
c.SetOut(buf)
|
c.SetOut(buf)
|
||||||
|
|
|
@ -59,6 +59,11 @@ type gRPCServer interface {
|
||||||
type fakeGRPCServer struct {
|
type fakeGRPCServer struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewFakeGRPCServer creates a fake gRPC server
|
||||||
|
func NewFakeGRPCServer() gRPCServer {
|
||||||
|
return &fakeGRPCServer{}
|
||||||
|
}
|
||||||
|
|
||||||
// Serve is a fake method
|
// Serve is a fake method
|
||||||
func (s *fakeGRPCServer) Serve(net.Listener) error {
|
func (s *fakeGRPCServer) Serve(net.Listener) error {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -27,19 +27,21 @@ func TestPrintProto(t *testing.T) {
|
||||||
verify: func(t *testing.T, buf *bytes.Buffer, err error) {
|
verify: func(t *testing.T, buf *bytes.Buffer, err error) {
|
||||||
assert.NotNil(t, err)
|
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 {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
root := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"})
|
root := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}, &fakeGRPCServer{})
|
||||||
root.SetOut(buf)
|
root.SetOut(buf)
|
||||||
root.SetArgs(tt.args)
|
root.SetArgs(tt.args)
|
||||||
err := root.Execute()
|
err := root.Execute()
|
||||||
tt.verify(t, buf, err)
|
tt.verify(t, buf, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
server := createServerCmd(&fakeGRPCServer{})
|
|
||||||
err := server.Execute()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,12 +10,12 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestService(t *testing.T) {
|
func TestService(t *testing.T) {
|
||||||
root := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"})
|
root := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}, NewFakeGRPCServer())
|
||||||
root.SetArgs([]string{"service", "fake"})
|
root.SetArgs([]string{"service", "fake"})
|
||||||
err := root.Execute()
|
err := root.Execute()
|
||||||
assert.NotNil(t, err)
|
assert.NotNil(t, err)
|
||||||
|
|
||||||
notLinux := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "fake"})
|
notLinux := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "fake"}, NewFakeGRPCServer())
|
||||||
notLinux.SetArgs([]string{"service", "--action", "install"})
|
notLinux.SetArgs([]string{"service", "--action", "install"})
|
||||||
err = notLinux.Execute()
|
err = notLinux.Execute()
|
||||||
assert.NotNil(t, err)
|
assert.NotNil(t, err)
|
||||||
|
@ -26,7 +26,7 @@ func TestService(t *testing.T) {
|
||||||
os.RemoveAll(tmpFile.Name())
|
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()})
|
targetScript.SetArgs([]string{"service", "--action", "install", "--script-path", tmpFile.Name()})
|
||||||
err = targetScript.Execute()
|
err = targetScript.Execute()
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
@ -58,7 +58,7 @@ func TestService(t *testing.T) {
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
buf := new(bytes.Buffer)
|
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.SetOut(buf)
|
||||||
normalRoot.SetArgs([]string{"service", "--action", tt.action})
|
normalRoot.SetArgs([]string{"service", "--action", tt.action})
|
||||||
err = normalRoot.Execute()
|
err = normalRoot.Execute()
|
||||||
|
|
4
main.go
4
main.go
|
@ -5,10 +5,12 @@ import (
|
||||||
|
|
||||||
"github.com/linuxsuren/api-testing/cmd"
|
"github.com/linuxsuren/api-testing/cmd"
|
||||||
exec "github.com/linuxsuren/go-fake-runtime"
|
exec "github.com/linuxsuren/go-fake-runtime"
|
||||||
|
"google.golang.org/grpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
c := cmd.NewRootCmd(exec.DefaultExecer{})
|
gRPCServer := grpc.NewServer()
|
||||||
|
c := cmd.NewRootCmd(exec.DefaultExecer{}, gRPCServer)
|
||||||
if err := c.Execute(); err != nil {
|
if err := c.Execute(); err != nil {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
// Package kubernetes provides a low level client for small footprint consideration
|
||||||
|
package kubernetes
|
|
@ -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))
|
||||||
|
}
|
|
@ -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"}`)
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ package runner
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
@ -17,6 +18,7 @@ import (
|
||||||
"github.com/andreyvit/diff"
|
"github.com/andreyvit/diff"
|
||||||
"github.com/antonmedv/expr"
|
"github.com/antonmedv/expr"
|
||||||
"github.com/antonmedv/expr/vm"
|
"github.com/antonmedv/expr/vm"
|
||||||
|
"github.com/linuxsuren/api-testing/pkg/runner/kubernetes"
|
||||||
"github.com/linuxsuren/api-testing/pkg/testing"
|
"github.com/linuxsuren/api-testing/pkg/testing"
|
||||||
fakeruntime "github.com/linuxsuren/go-fake-runtime"
|
fakeruntime "github.com/linuxsuren/go-fake-runtime"
|
||||||
unstructured "github.com/linuxsuren/unstructured/pkg"
|
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
|
var requestBody io.Reader
|
||||||
if testcase.Request.Body != "" {
|
if testcase.Request.Body != "" {
|
||||||
requestBody = bytes.NewBufferString(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)
|
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
|
// send the HTTP request
|
||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
if resp, err = client.Do(request); err != nil {
|
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 {
|
for _, verify := range testcase.Expect.Verify {
|
||||||
var program *vm.Program
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -336,6 +350,7 @@ func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataConte
|
||||||
|
|
||||||
if !result.(bool) {
|
if !result.(bool) {
|
||||||
err = fmt.Errorf("failed to verify: %s", verify)
|
err = fmt.Errorf("failed to verify: %s", verify)
|
||||||
|
fmt.Println(err)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
Loading…
Reference in New Issue