|
|
package framework
|
|
|
|
|
|
import (
|
|
|
"context"
|
|
|
"flag"
|
|
|
"fmt"
|
|
|
"io"
|
|
|
"math/rand"
|
|
|
"os"
|
|
|
"regexp"
|
|
|
"strings"
|
|
|
"testing"
|
|
|
"time"
|
|
|
|
|
|
"github.com/onsi/ginkgo"
|
|
|
"github.com/onsi/ginkgo/reporters"
|
|
|
"github.com/onsi/gomega"
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
|
"k8s.io/apimachinery/pkg/api/errors"
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
|
|
"k8s.io/apimachinery/pkg/util/yaml"
|
|
|
"k8s.io/client-go/kubernetes"
|
|
|
"k8s.io/client-go/rest"
|
|
|
)
|
|
|
|
|
|
var DefaultStartTimeout = float64(60 * 60)
|
|
|
var nsRegex = regexp.MustCompile("[^a-z0-9]")
|
|
|
|
|
|
type Framework struct {
|
|
|
Config *Config
|
|
|
ClusterConfig *ClusterConfig
|
|
|
|
|
|
factory Factory // 工厂对象,提供创建provider的方法
|
|
|
provider ClusterProvider // 存储当前 framework对象中实现的provider
|
|
|
client kubernetes.Interface // 用来链接创建的 k8s 集群
|
|
|
|
|
|
configFile string // 配置文件的路径
|
|
|
initTimeout float64 // 启动时候,包括安装集群和依赖及本程序的超时时间
|
|
|
}
|
|
|
|
|
|
func NewFramework() *Framework {
|
|
|
return &Framework{}
|
|
|
}
|
|
|
|
|
|
// Flags 解析命令行参数: --config --timeout
|
|
|
func (f *Framework) Flags() *Framework {
|
|
|
flag.StringVar(&f.configFile, "config", "config", "config file to used")
|
|
|
flag.Float64Var(&f.initTimeout, "startup-timeout", DefaultStartTimeout, "startup timeout")
|
|
|
flag.Parse() // 让配置生效,就是将参数赋值到变量中
|
|
|
return f
|
|
|
}
|
|
|
|
|
|
// LoadConfig 加载配置文件到 fmw
|
|
|
func (f *Framework) LoadConfig(writer io.Writer) *Framework {
|
|
|
// 1. 创建 config 对象
|
|
|
config := NewConfig()
|
|
|
|
|
|
// 2. 加载配置文件内容到 config 对象中
|
|
|
if err := config.Load(f.configFile); err != nil {
|
|
|
panic(err)
|
|
|
}
|
|
|
|
|
|
// 3. 将传入的 writer 应用到 config中
|
|
|
// 4. 将 config 加入到 fmw 中
|
|
|
return f.WithConfig(config.WithWriter(writer))
|
|
|
}
|
|
|
|
|
|
func (f *Framework) SynchronizedBeforeSuite(initFunc func()) *Framework {
|
|
|
if initFunc == nil {
|
|
|
initFunc = func() {
|
|
|
// 1. 安装环境
|
|
|
ginkgo.By("Deploying test environment")
|
|
|
if err := f.DeployTestEnvironment(); err != nil {
|
|
|
panic(err)
|
|
|
}
|
|
|
|
|
|
// 2. 初始化环境访问的授权,也就是创建kubectl 访问需要的config
|
|
|
ginkgo.By("kubectl switch context")
|
|
|
kubectlConfig := NewKubectlConfig(f.Config)
|
|
|
if err := kubectlConfig.SetContext(f.ClusterConfig); err != nil {
|
|
|
panic(err)
|
|
|
}
|
|
|
// 推出前清理context
|
|
|
defer func() {
|
|
|
ginkgo.By("kubectl reverting context")
|
|
|
if !f.Config.Sub("cluster").Sub("kind").GetBool("retain") {
|
|
|
_ = kubectlConfig.DeleteContext(f.ClusterConfig)
|
|
|
}
|
|
|
}()
|
|
|
|
|
|
// 3. 安装依赖和我们的程序
|
|
|
ginkgo.By("Preparing install steps")
|
|
|
installer := NewInstaller(f.Config)
|
|
|
ginkgo.By("Executing install steps")
|
|
|
if err := installer.Install(); err != nil {
|
|
|
panic(err)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
ginkgo.SynchronizedBeforeSuite(func() []byte {
|
|
|
initFunc()
|
|
|
return nil
|
|
|
}, func(_ []byte) {}, f.initTimeout)
|
|
|
return f
|
|
|
}
|
|
|
|
|
|
func (f *Framework) SynchronizedAfterSuite(destroyFunc func()) *Framework {
|
|
|
if destroyFunc == nil {
|
|
|
destroyFunc = func() {
|
|
|
// 回收测试环境
|
|
|
if err := f.DestroyTestEnvironment(); err != nil {
|
|
|
panic(err)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
ginkgo.SynchronizedAfterSuite(func() {}, destroyFunc, f.initTimeout)
|
|
|
return f
|
|
|
}
|
|
|
|
|
|
func (f *Framework) MRun(m *testing.M) {
|
|
|
rand.Seed(time.Now().UnixNano()) // 优化随机数
|
|
|
os.Exit(m.Run()) // 执行真正的 TestMain
|
|
|
}
|
|
|
|
|
|
func (f *Framework) Run(t *testing.T) {
|
|
|
gomega.RegisterFailHandler(ginkgo.Fail)
|
|
|
var r []ginkgo.Reporter
|
|
|
r = append(r, reporters.NewJUnitReporter("e2e.xml"))
|
|
|
ginkgo.RunSpecsWithDefaultAndCustomReporters(t, "e2e", r)
|
|
|
}
|
|
|
|
|
|
func (f *Framework) WithConfig(config *Config) *Framework {
|
|
|
f.Config = config
|
|
|
return f
|
|
|
}
|
|
|
|
|
|
// DeployTestEnvironment 创建测试环境,并且获取访问集群的配置及client
|
|
|
func (f *Framework) DeployTestEnvironment() error {
|
|
|
// 1. 检查 f.config
|
|
|
if f.Config == nil {
|
|
|
return field.Invalid(
|
|
|
field.NewPath("config"),
|
|
|
nil,
|
|
|
"Not inital config object")
|
|
|
}
|
|
|
// 2. 创建provider
|
|
|
ginkgo.By("Getting env provider")
|
|
|
var err error
|
|
|
if f.provider, err = f.factory.Provider(f.Config); err != nil {
|
|
|
return err
|
|
|
}
|
|
|
// 3. 执行 provider 实现的 validate 方法验证 config
|
|
|
ginkgo.By("Validate config for provider")
|
|
|
if err := f.provider.Validate(f.Config); err != nil {
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
// 4. 执行 provider 实现的 deploy 方法,创建集群
|
|
|
ginkgo.By("Deploying test env")
|
|
|
clusterConfig, err := f.provider.Deploy(f.Config)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
f.ClusterConfig = &clusterConfig
|
|
|
|
|
|
// 5. 创建 client,用于执行测试用例的时候使用
|
|
|
if f.client, err = kubernetes.NewForConfig(f.ClusterConfig.Rest); err != nil {
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
// DestroyTestEnvironment 销毁测试环境。此方法要在执行过 DeployTestEnvironment 方法之后执行。
|
|
|
func (f *Framework) DestroyTestEnvironment() error {
|
|
|
// 1. 检查 f.Config
|
|
|
if f.Config == nil {
|
|
|
return field.Invalid(
|
|
|
field.NewPath("config"),
|
|
|
nil,
|
|
|
"Not inital config object")
|
|
|
}
|
|
|
|
|
|
// 2. 检查 provider
|
|
|
if f.provider == nil {
|
|
|
return fmt.Errorf("f.provider is nil")
|
|
|
}
|
|
|
|
|
|
// 3. 执行 provider 的 destroy 方法来销毁环境
|
|
|
ginkgo.By("Destroying test env")
|
|
|
if err := f.provider.Destroy(f.Config); err != nil {
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
// 4. 清空 f.provider,保护销毁函数被多次执行而报错
|
|
|
f.provider = nil
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
// 加载测试文件内容到对象中
|
|
|
func (f *Framework) LoadYamlToUnstructured(ctFilePath string, obj *unstructured.Unstructured) error {
|
|
|
data, err := os.ReadFile(ctFilePath)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
//var o = map[string]interface{}{}
|
|
|
//yaml.Unmarshal(data, &o)
|
|
|
return yaml.Unmarshal(data, &(obj.Object))
|
|
|
}
|
|
|
|
|
|
func (f *Framework) Describe(name string, ctxFunc ContextFunc) bool {
|
|
|
// 整个函数,实际上是调用 ginkgo的Describe
|
|
|
return ginkgo.Describe(name, func() {
|
|
|
// 1. 创建 testcontext
|
|
|
ctx, err := f.createTestContext(name, false)
|
|
|
if err != nil {
|
|
|
ginkgo.Fail("Cannot create test context for " + name)
|
|
|
return
|
|
|
}
|
|
|
|
|
|
// 2. 执行每次测试任务前,来执行一些我们期望的动作,如创建namespace就往在这里
|
|
|
ginkgo.BeforeEach(func() {
|
|
|
ctx2, err := f.createTestContext(name, true)
|
|
|
if err != nil {
|
|
|
ginkgo.Fail("Cannot create testcontext for " + name +
|
|
|
" namespace " + ctx2.Namespace)
|
|
|
return
|
|
|
}
|
|
|
ctx = ctx2
|
|
|
})
|
|
|
|
|
|
// 3. 执行每次测试任务之后要做的事情。这里我们来删除testcontext
|
|
|
ginkgo.AfterEach(func() {
|
|
|
// 回收testcontext
|
|
|
_ = f.deleteTestContext(ctx)
|
|
|
})
|
|
|
// 4. 执行用户的测试函数
|
|
|
ctxFunc(&ctx, f)
|
|
|
})
|
|
|
}
|
|
|
|
|
|
func (f *Framework) createTestContext(name string, nsCreate bool) (TestContext, error) {
|
|
|
// 1. 创建 testcontext对象
|
|
|
tc := TestContext{}
|
|
|
// 2. 检查 f 是否为空
|
|
|
if f.Config == nil || f.ClusterConfig == nil {
|
|
|
return tc, nil
|
|
|
}
|
|
|
|
|
|
// 3. 填充字段
|
|
|
tc.Name = name
|
|
|
tc.Config = rest.CopyConfig(f.ClusterConfig.Rest)
|
|
|
tc.MasterIP = f.ClusterConfig.MasterIP
|
|
|
|
|
|
// 4. 判断参数,时候创建namespace
|
|
|
if nsCreate {
|
|
|
// 4.1 如果创建,使用f.client来创建namespace
|
|
|
// 4.1.1 处理name,将空格或下划线替换为"-"
|
|
|
// 4.1.2 正则检查是否有其他非法字符
|
|
|
// 4.1.3 自动生成namespace的机制
|
|
|
prefix := nsRegex.ReplaceAllString(
|
|
|
strings.ReplaceAll(
|
|
|
strings.ReplaceAll(
|
|
|
strings.ToLower(name),
|
|
|
" ", "-"),
|
|
|
"_", "-"),
|
|
|
"")
|
|
|
if len(prefix) > 30 {
|
|
|
prefix = prefix[:30]
|
|
|
}
|
|
|
ns, err := f.client.CoreV1().Namespaces().Create(
|
|
|
context.TODO(),
|
|
|
&corev1.Namespace{
|
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
|
GenerateName: prefix + "-",
|
|
|
},
|
|
|
},
|
|
|
metav1.CreateOptions{})
|
|
|
if err != nil {
|
|
|
return tc, err
|
|
|
}
|
|
|
tc.Namespace = ns.GetName()
|
|
|
}
|
|
|
|
|
|
// 5 ..... 执行其他的想要做的事情
|
|
|
// 比如我们要创建sa/secret
|
|
|
return tc, nil
|
|
|
}
|
|
|
|
|
|
func (f *Framework) deleteTestContext(ctx TestContext) error {
|
|
|
// 删除创建的资源
|
|
|
// 这里我们之创建了ns,所以只删除它
|
|
|
|
|
|
errs := field.ErrorList{}
|
|
|
if ctx.Namespace != "" {
|
|
|
// 删除ns
|
|
|
if err := f.client.CoreV1().Namespaces().Delete(
|
|
|
context.TODO(),
|
|
|
ctx.Namespace,
|
|
|
metav1.DeleteOptions{}); err != nil &&
|
|
|
!errors.IsNotFound(err) {
|
|
|
errs = append(errs, field.InternalError(field.NewPath("testcontext"), err))
|
|
|
}
|
|
|
}
|
|
|
// 如果我们还创建了更多的资源,需要在下面实现执行删除的操作
|
|
|
// 如果执行过程中出现错误,同样使用errs = append(errs, err)来追加错误
|
|
|
|
|
|
return errs.ToAggregate()
|
|
|
}
|