e2e test framework

Signed-off-by: Roland.Ma <rolandma@yunify.com>
This commit is contained in:
Roland.Ma
2021-02-20 07:34:31 +00:00
parent 5a8e8ca35e
commit fd2f213f3a
30 changed files with 3745 additions and 210 deletions

View File

@@ -0,0 +1,62 @@
/*
Copyright 2020 KubeSphere Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package framework
import (
"github.com/onsi/gomega"
)
// ExpectEqual expects the specified two are the same, otherwise an exception raises
func ExpectEqual(actual interface{}, extra interface{}, explain ...interface{}) {
gomega.ExpectWithOffset(1, actual).To(gomega.Equal(extra), explain...)
}
// ExpectNotEqual expects the specified two are not the same, otherwise an exception raises
func ExpectNotEqual(actual interface{}, extra interface{}, explain ...interface{}) {
gomega.ExpectWithOffset(1, actual).NotTo(gomega.Equal(extra), explain...)
}
// ExpectError expects an error happens, otherwise an exception raises
func ExpectError(err error, explain ...interface{}) {
gomega.ExpectWithOffset(1, err).To(gomega.HaveOccurred(), explain...)
}
// ExpectNoError checks if "err" is set, and if so, fails assertion while logging the error.
func ExpectNoError(err error, explain ...interface{}) {
ExpectNoErrorWithOffset(1, err, explain...)
}
// ExpectNoErrorWithOffset checks if "err" is set, and if so, fails assertion while logging the error at "offset" levels above its caller
// (for example, for call chain f -> g -> ExpectNoErrorWithOffset(1, ...) error would be logged for "f").
func ExpectNoErrorWithOffset(offset int, err error, explain ...interface{}) {
gomega.ExpectWithOffset(1+offset, err).NotTo(gomega.HaveOccurred(), explain...)
}
// ExpectConsistOf expects actual contains precisely the extra elements. The ordering of the elements does not matter.
func ExpectConsistOf(actual interface{}, extra interface{}, explain ...interface{}) {
gomega.ExpectWithOffset(1, actual).To(gomega.ConsistOf(extra), explain...)
}
// ExpectHaveKey expects the actual map has the key in the keyset
func ExpectHaveKey(actual interface{}, key interface{}, explain ...interface{}) {
gomega.ExpectWithOffset(1, actual).To(gomega.HaveKey(key), explain...)
}
// ExpectEmpty expects actual is empty
func ExpectEmpty(actual interface{}, explain ...interface{}) {
gomega.ExpectWithOffset(1, actual).To(gomega.BeEmpty(), explain...)
}

View File

@@ -0,0 +1,151 @@
/*
Copyright 2020 KubeSphere Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package framework
import (
"context"
"fmt"
"github.com/onsi/ginkgo" //nolint:stylecheck
"github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"kubesphere.io/client-go/client"
"kubesphere.io/client-go/client/generic"
"kubesphere.io/kubesphere/pkg/apis"
"kubesphere.io/kubesphere/pkg/constants"
"kubesphere.io/kubesphere/test/e2e/framework/workspace"
)
type Framework struct {
BaseName string
Workspace string
Namespaces []string
Scheme *runtime.Scheme
}
// KubeSphereFramework provides an interface to a test control plane so
// that the implementation can vary without affecting tests.
type KubeSphereFramework interface {
GenericClient(userAgent string) client.Client
KubeSphereSystemNamespace() string
// Name of the workspace for the current test to target
TestWorkSpaceName() string
// Create a Namespace under current Worksapce
CreateNamespace(name string) string
// Get Names of the namespaces for the current test to target
GetNamespaceNames() []string
}
func NewKubeSphereFramework(baseName string) KubeSphereFramework {
sch := runtime.NewScheme()
if err := apis.AddToScheme(sch); err != nil {
Failf("unable add KubeSphere APIs to scheme: %v", err)
}
if err := scheme.AddToScheme(sch); err != nil {
Failf("unable add Kubernetes APIs to scheme: %v", err)
}
f := &Framework{
BaseName: baseName,
Scheme: sch,
}
ginkgo.AfterEach(f.AfterEach)
ginkgo.BeforeEach(f.BeforeEach)
return f
}
// BeforeEach
func (f *Framework) BeforeEach() {
}
// AfterEach
func (f *Framework) AfterEach() {
}
func (f *Framework) TestWorkSpaceName() string {
if f.Workspace == "" {
f.Workspace = CreateTestWorkSpace(f.GenericClient(f.BaseName), f.BaseName)
}
return f.Workspace
}
func CreateTestWorkSpace(client client.Client, baseName string) string {
ginkgo.By("Creating a WorkSpace to execute the test in")
wspt := workspace.NewWorkspaceTemplate("", "admin")
wspt.GenerateName = fmt.Sprintf("e2e-tests-%v-", baseName)
wspt, err := workspace.CreateWorkspace(client, wspt)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
ginkgo.By(fmt.Sprintf("Created test namespace %s", wspt.Name))
return wspt.Name
}
func (f *Framework) GetNamespaceNames() []string {
return f.Namespaces
}
func (f *Framework) CreateNamespace(name string) string {
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Labels: map[string]string{
constants.WorkspaceLabelKey: f.TestWorkSpaceName(),
},
},
}
opts := &client.URLOptions{
Group: "tenant.kubesphere.io",
Version: "v1alpha2",
}
err := f.GenericClient(f.BaseName).Create(context.TODO(), ns, opts, &client.WorkspaceOptions{Name: f.TestWorkSpaceName()})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
return ns.Name
}
func (f *Framework) KubeSphereSystemNamespace() string {
return "Kubesphere-system"
}
func (f *Framework) GenericClient(userAgent string) client.Client {
ctx := TestContext
config := &rest.Config{
Host: "127.0.0.1:9090",
Username: "admin",
Password: "P@88w0rd",
}
if ctx.Host != "" {
config = &rest.Config{
Host: ctx.Host,
Username: ctx.Username,
Password: ctx.Password,
}
}
rest.AddUserAgent(config, userAgent)
return generic.NewForConfigOrDie(config, client.Options{Scheme: f.Scheme})
}

View File

@@ -0,0 +1,96 @@
/*
Copyright 2020 KubeSphere Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package ginkgowrapper wraps Ginkgo Fail and Skip functions to panic
// with structured data instead of a constant string.
package ginkgowrapper
import (
"bufio"
"bytes"
"regexp"
"runtime"
"runtime/debug"
"strings"
"github.com/onsi/ginkgo"
)
// FailurePanic is the value that will be panicked from Fail.
type FailurePanic struct {
Message string // The failure message passed to Fail
Filename string // The filename that is the source of the failure
Line int // The line number of the filename that is the source of the failure
FullStackTrace string // A full stack trace starting at the source of the failure
}
// String makes FailurePanic look like the old Ginkgo panic when printed.
func (FailurePanic) String() string { return ginkgo.GINKGO_PANIC }
// Fail wraps ginkgo.Fail so that it panics with more useful
// information about the failure. This function will panic with a
// FailurePanic.
func Fail(message string, callerSkip ...int) {
skip := 1
if len(callerSkip) > 0 {
skip += callerSkip[0]
}
_, file, line, _ := runtime.Caller(skip)
fp := FailurePanic{
Message: message,
Filename: file,
Line: line,
FullStackTrace: pruneStack(skip),
}
defer func() {
e := recover()
if e != nil {
panic(fp)
}
}()
ginkgo.Fail(message, skip)
}
// ginkgo adds a lot of test running infrastructure to the stack, so
// we filter those out
var stackSkipPattern = regexp.MustCompile(`onsi/ginkgo`)
func pruneStack(skip int) string {
skip += 2 // one for pruneStack and one for debug.Stack
stack := debug.Stack()
scanner := bufio.NewScanner(bytes.NewBuffer(stack))
var prunedStack []string
// skip the top of the stack
for i := 0; i < 2*skip+1; i++ {
scanner.Scan()
}
for scanner.Scan() {
if stackSkipPattern.Match(scanner.Bytes()) {
scanner.Scan() // these come in pairs
} else {
prunedStack = append(prunedStack, scanner.Text())
scanner.Scan() // these come in pairs
prunedStack = append(prunedStack, scanner.Text())
}
}
return strings.Join(prunedStack, "\n")
}

View File

@@ -0,0 +1,93 @@
/*
Copyright 2020 KubeSphere Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package iam
import (
"context"
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
"kubesphere.io/client-go/client"
)
var URLOptions = client.URLOptions{
Group: "iam.kubesphere.io",
Version: "v1alpha2",
}
// NewUser returns a User spec with the specified argument.
func NewUser(name, globelRole string) *iamv1alpha2.User {
return &iamv1alpha2.User{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Annotations: map[string]string{
"iam.kubesphere.io/globalrole": globelRole,
},
},
Spec: iamv1alpha2.UserSpec{
Email: fmt.Sprint("%v@kubesphere.io", name),
EncryptedPassword: "P@88w0rd",
},
}
}
// CreateUser uses c to create User. If the returned error is nil, the returned User is valid and has
// been created.
func CreateUser(c client.Client, u *iamv1alpha2.User) (*iamv1alpha2.User, error) {
err := c.Create(context.TODO(), u, &URLOptions)
return u, err
}
// GetUser uses c to get the User by name. If the returned error is nil, the returned User is valid.
func GetUser(c client.Client, name string) (*iamv1alpha2.User, error) {
u := &iamv1alpha2.User{}
err := c.Get(context.TODO(), client.ObjectKey{Name: name}, u, &URLOptions)
if err != nil {
return nil, err
}
return u, nil
}
// NewGroup returns a Group spec with the specified argument.
func NewGroup(name, workspace string) *iamv1alpha2.Group {
return &iamv1alpha2.Group{
ObjectMeta: metav1.ObjectMeta{
GenerateName: name,
},
}
}
// CreateGroup uses c to create Group. If the returned error is nil, the returned Group is valid and has
// been created.
func CreateGroup(c client.Client, u *iamv1alpha2.Group, workspace string) (*iamv1alpha2.Group, error) {
err := c.Create(context.TODO(), u, &URLOptions, &client.WorkspaceOptions{Name: workspace})
return u, err
}
// GetGroup uses c to get the User by name. If the returned error is nil, the returned User is valid.
func GetGroup(c client.Client, name, workspace string) (*iamv1alpha2.Group, error) {
u := &iamv1alpha2.Group{}
err := c.Get(context.TODO(), client.ObjectKey{Name: name}, u, &URLOptions, &client.WorkspaceOptions{Name: workspace})
if err != nil {
return nil, err
}
return u, nil
}

110
test/e2e/framework/log.go Normal file
View File

@@ -0,0 +1,110 @@
/*
Copyright 2020 KubeSphere Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package framework
import (
"bytes"
"fmt"
"regexp"
"runtime/debug"
"time"
"github.com/onsi/ginkgo"
"kubesphere.io/kubesphere/test/e2e/framework/ginkgowrapper"
)
func nowStamp() string {
return time.Now().Format(time.StampMilli)
}
func log(level string, format string, args ...interface{}) {
fmt.Fprintf(ginkgo.GinkgoWriter, nowStamp()+": "+level+": "+format+"\n", args...)
}
// Logf logs the info.
func Logf(format string, args ...interface{}) {
log("INFO", format, args...)
}
// Failf logs the fail info, including a stack trace.
func Failf(format string, args ...interface{}) {
FailfWithOffset(1, format, args...)
}
// FailfWithOffset calls "Fail" and logs the error with a stack trace that starts at "offset" levels above its caller
// (for example, for call chain f -> g -> FailfWithOffset(1, ...) error would be logged for "f").
func FailfWithOffset(offset int, format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
skip := offset + 1
log("FAIL", "%s\n\nFull Stack Trace\n%s", msg, PrunedStack(skip))
ginkgowrapper.Fail(nowStamp()+": "+msg, skip)
}
// Fail is a replacement for ginkgo.Fail which logs the problem as it occurs
// together with a stack trace and then calls ginkgowrapper.Fail.
func Fail(msg string, callerSkip ...int) {
skip := 1
if len(callerSkip) > 0 {
skip += callerSkip[0]
}
log("FAIL", "%s\n\nFull Stack Trace\n%s", msg, PrunedStack(skip))
ginkgowrapper.Fail(nowStamp()+": "+msg, skip)
}
var codeFilterRE = regexp.MustCompile(`/github.com/onsi/ginkgo/`)
// PrunedStack is a wrapper around debug.Stack() that removes information
// about the current goroutine and optionally skips some of the initial stack entries.
// With skip == 0, the returned stack will start with the caller of PruneStack.
// From the remaining entries it automatically filters out useless ones like
// entries coming from Ginkgo.
//
// This is a modified copy of PruneStack in https://github.com/onsi/ginkgo/blob/f90f37d87fa6b1dd9625e2b1e83c23ffae3de228/internal/codelocation/code_location.go#L25:
// - simplified API and thus renamed (calls debug.Stack() instead of taking a parameter)
// - source code filtering updated to be specific to Kubernetes
// - optimized to use bytes and in-place slice filtering from
// https://github.com/golang/go/wiki/SliceTricks#filter-in-place
func PrunedStack(skip int) []byte {
fullStackTrace := debug.Stack()
stack := bytes.Split(fullStackTrace, []byte("\n"))
// Ensure that the even entries are the method names and the
// the odd entries the source code information.
if len(stack) > 0 && bytes.HasPrefix(stack[0], []byte("goroutine ")) {
// Ignore "goroutine 29 [running]:" line.
stack = stack[1:]
}
// The "+2" is for skipping over:
// - runtime/debug.Stack()
// - PrunedStack()
skip += 2
if len(stack) > 2*skip {
stack = stack[2*skip:]
}
n := 0
for i := 0; i < len(stack)/2; i++ {
// We filter out based on the source code file name.
if !codeFilterRE.Match([]byte(stack[i*2+1])) {
stack[n] = stack[i*2]
stack[n+1] = stack[i*2+1]
n += 2
}
}
stack = stack[:n]
return bytes.Join(stack, []byte("\n"))
}

View File

@@ -0,0 +1,47 @@
/*
Copyright 2020 KubeSphere Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package framework
import (
"flag"
"os"
)
type TestContextType struct {
Host string
InMemoryTest bool
Username string
Password string
}
func registerFlags(t *TestContextType) {
flag.BoolVar(&t.InMemoryTest, "in-memory-test", false,
"Whether KubeSphere controllers and APIServer be started in memory.")
flag.StringVar(&t.Host, "ks-apiserver", os.Getenv("KS_APISERVER"),
"KubeSphere API Server IP/DNS")
flag.StringVar(&t.Username, "username", os.Getenv("KS_USERNAME"),
"Username to login to KubeSphere API Server")
flag.StringVar(&t.Password, "password", os.Getenv("KS_PASSWORD"),
"Password to login to KubeSphere API Server")
}
var TestContext *TestContextType = &TestContextType{}
func ParseFlags() {
registerFlags(TestContext)
flag.Parse()
}

View File

@@ -0,0 +1,95 @@
/*
Copyright 2020 KubeSphere Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package workspace
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
tenantv1alpha1 "kubesphere.io/kubesphere/pkg/apis/tenant/v1alpha1"
tenantv1alpha2 "kubesphere.io/kubesphere/pkg/apis/tenant/v1alpha2"
fedb1 "kubesphere.io/kubesphere/pkg/apis/types/v1beta1"
"context"
"kubesphere.io/client-go/client"
)
var URLOptions = client.URLOptions{
Group: "tenant.kubesphere.io",
Version: "v1alpha2",
}
// NewWorkspaceTemplate returns a WorkspaceTemplate spec with the specified argument.
func NewWorkspaceTemplate(name string, manager string, hosts ...string) *tenantv1alpha2.WorkspaceTemplate {
clusters := []fedb1.GenericClusterReference{}
if hosts != nil {
for _, h := range hosts {
clusters = append(clusters, fedb1.GenericClusterReference{Name: h})
}
}
return &tenantv1alpha2.WorkspaceTemplate{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: fedb1.FederatedWorkspaceSpec{
Placement: fedb1.GenericPlacementFields{
Clusters: clusters,
},
Template: fedb1.WorkspaceTemplate{
Spec: tenantv1alpha1.WorkspaceSpec{
Manager: manager,
},
},
},
}
}
// CreateWorkSpace uses c to create Workspace. If the returned error is nil, the returned Workspace is valid and has
// been created.
func CreateWorkspace(c client.Client, w *tenantv1alpha2.WorkspaceTemplate) (*tenantv1alpha2.WorkspaceTemplate, error) {
opts := &client.URLOptions{
AbsPath: "kapis/tenant.kubesphere.io/v1alpha2/workspaces",
}
err := c.Create(context.TODO(), w, opts)
return w, err
}
// GetJob uses c to get the Workspace by name. If the returned error is nil, the returned Workspace is valid.
func GetWorkspace(c client.Client, name string) (*tenantv1alpha1.Workspace, error) {
wsp := &tenantv1alpha1.Workspace{}
err := c.Get(context.TODO(), client.ObjectKey{Name: name}, wsp, &URLOptions)
if err != nil {
return nil, err
}
return wsp, nil
}
// DeleteWorkspace uses c to delete the Workspace by name. If the returned error is nil, the returned Workspace is valid.
func DeleteWorkspace(c client.Client, name string, opts ...client.DeleteOption) (*tenantv1alpha1.Workspace, error) {
wsp := &tenantv1alpha1.Workspace{
ObjectMeta: metav1.ObjectMeta{Name: name},
}
opts = append(opts, &URLOptions)
err := c.Delete(context.TODO(), wsp, opts...)
if err != nil {
return nil, err
}
return wsp, nil
}