You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

314 lines
8.3 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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