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

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