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:
Rick 2023-04-15 21:47:50 +08:00 committed by GitHub
parent e08c2046d5
commit f6d271b7eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 456 additions and 129 deletions

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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(),

View File

@ -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)

View File

@ -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)
}

View File

@ -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)

View File

@ -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

View File

@ -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)
}

View File

@ -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()

View File

@ -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)
}

View File

@ -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
}
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -0,0 +1,2 @@
// Package kubernetes provides a low level client for small footprint consideration
package kubernetes

View File

@ -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))
}

View File

@ -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"}`)
}

View File

@ -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
}
}

19
sample/kubernetes.yaml Normal file
View File

@ -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()