17、深入探究Kubernetes Operator的测试与开发

深入探究Kubernetes Operator的测试与开发

1. 测试Reconcile函数

1.1 测试工具介绍

为了测试Reconcile函数,我们将使用ginkgo(Go语言的测试框架)和controller - runtime库中的envtest包。envtest包通过启动一个简单的本地控制平面来提供Kubernetes测试环境。

1.2 安装envtest二进制文件

安装步骤如下:
1. 安装setup - envtest工具:

$ go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest
  1. 安装特定Kubernetes版本的二进制文件:
$ setup-envtest use 1.23

输出示例:

Version: 1.23.5
OS/Arch: linux/amd64
Path: /path/to/kubebuilder-envtest/k8s/1.23.5-linux-amd64
  1. 使用默认目录或环境变量:
    • 创建符号链接使用默认目录:
$ sudo mkdir /usr/local/kubebuilder
$ sudo ln -s /path/to/kubebuilder-envtest/k8s/1.23.5-linux-amd64 /usr/local/kubebuilder/bin
- 使用环境变量:
$ source <(setup-envtest use -i -p env 1.23.5)
$ echo $KUBEBUILDER_ASSETS
/path/to/kubebuilder-envtest/k8s/1.23.5-linux-amd64

1.3 使用envtest

控制平面仅运行API Server和etcd,没有控制器。这意味着测试的Operator创建Kubernetes资源时,没有控制器会做出反应。创建测试环境,首先要创建envtest.Environment结构的实例。示例代码如下:

import (
  "path/filepath"
  "sigs.k8s.io/controller-runtime/pkg/envtest"
)
testEnv = &envtest.Environment{
  CRDDirectoryPaths: []string{
    filepath.Join("..", "..", "crd"),
  },
  ErrorIfCRDPathMissing: true,
}

启动和停止环境:

cfg, err := testEnv.Start()
// ...
err := testEnv.Stop()

1.4 定义ginkgo套件

使用Go测试函数启动ginkgo规范:

import (
  "testing"
  . "github.com/onsi/ginkgo/v2"
  . "github.com/onsi/gomega"
)
func TestMyReconciler_Reconcile(t *testing.T) {
  RegisterFailHandler(Fail)
  RunSpecs(t,
    "Controller Suite",
  )
}

BeforeSuite和AfterSuite函数示例:

import (
  "context"
  "path/filepath"
  "testing"
  . "github.com/onsi/ginkgo/v2"
  . "github.com/onsi/gomega"
  appsv1 "k8s.io/api/apps/v1"
  "k8s.io/apimachinery/pkg/runtime"
  clientgoscheme "k8s.io/client-go/kubernetes/scheme"
  "sigs.k8s.io/controller-runtime/pkg/builder"
  "sigs.k8s.io/controller-runtime/pkg/client"
  "sigs.k8s.io/controller-runtime/pkg/envtest"
  "sigs.k8s.io/controller-runtime/pkg/log"
  "sigs.k8s.io/controller-runtime/pkg/log/zap"
  "sigs.k8s.io/controller-runtime/pkg/manager"
  mygroupv1alpha1 "github.com/feloy/myresource-crd/pkg/apis/mygroup.example.com/v1alpha1"
)
var (
  testEnv   *envtest.Environment
  ctx       context.Context
  cancel    context.CancelFunc
  k8sClient client.Client
)
var _ = BeforeSuite(func() {
  log.SetLogger(zap.New(
    zap.WriteTo(GinkgoWriter),
    zap.UseDevMode(true),
  ))
  ctx, cancel = context.WithCancel(
    context.Background(),
  )
  testEnv = &envtest.Environment{
    CRDDirectoryPaths: []string{
      filepath.Join("..", "..", "crd"),
    },
    ErrorIfCRDPathMissing: true,
  }
  var err error
  cfg, err := testEnv.Start()
  Expect(err).NotTo(HaveOccurred())
  Expect(cfg).NotTo(BeNil())
  scheme := runtime.NewScheme()
  err = clientgoscheme.AddToScheme(scheme)
  Expect(err).NotTo(HaveOccurred())
  err = mygroupv1alpha1.AddToScheme(scheme)
  Expect(err).NotTo(HaveOccurred())
  mgr, err := manager.New(cfg, manager.Options{
    Scheme: scheme,
  })
  Expect(err).ToNot(HaveOccurred())
  k8sClient = mgr.GetClient()
  err = builder.
    ControllerManagedBy(mgr).
    Named(Name).
    For(&mygroupv1alpha1.MyResource{}).
    Owns(&appsv1.Deployment{}).
    Complete(&MyReconciler{})
  go func() {
    defer GinkgoRecover()
    err = mgr.Start(ctx)
    Expect(err).ToNot(
      HaveOccurred(),
      "failed to run manager",
    )
  }()
})
var _ = AfterSuite(func() {
  cancel()
  err := testEnv.Stop()
  Expect(err).NotTo(HaveOccurred())
})

1.5 编写测试

测试计划如下:
1. 创建MyResource实例,验证Reconcile函数是否创建预期的“低级”资源。
2. 低级资源创建后,更新其状态,验证MyResource实例的状态是否相应更新。

测试代码示例:

import (
  "fmt"
  "math/rand"
  . "github.com/onsi/ginkgo/v2"
  . "github.com/onsi/gomega"
  appsv1 "k8s.io/api/apps/v1"
  metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  "k8s.io/apimachinery/pkg/types"
  "sigs.k8s.io/controller-runtime/pkg/client"
  mygroupv1alpha1 "github.com/feloy/myresource-crd/pkg/apis/mygroup.example.com/v1alpha1"
)
var _ = Describe("MyResource controller", func() {
  When("When creating a MyResource instance", func() {
    var (
      myres      mygroupv1alpha1.MyResource
      ownerref   *metav1.OwnerReference
      name       string
      namespace  = "default"
      deployName string
      image      string
    )
    BeforeEach(func() {
      image = fmt.Sprintf("myimage%d", rand.Intn(1000))
      myres = mygroupv1alpha1.MyResource{
        Spec: mygroupv1alpha1.MyResourceSpec{
          Image: image,
        },
      }
      name = fmt.Sprintf("myres%d", rand.Intn(1000))
      myres.SetName(name)
      myres.SetNamespace(namespace)
      err := k8sClient.Create(ctx, &myres)
      Expect(err).NotTo(HaveOccurred())
      ownerref = metav1.NewControllerRef(
        &myres,
        mygroupv1alpha1.SchemeGroupVersion.
          WithKind("MyResource"),
      )
      deployName = fmt.Sprintf("%s-deployment", name)
    })
    AfterEach(func() {
      k8sClient.Delete(ctx, &myres)
    })
    It("should create a deployment", func() {
      var dep appsv1.Deployment
      Eventually(
        deploymentExists(deployName, namespace, &dep),
        10, 1
      ).Should(BeTrue())
    })
    When("deployment is found", func() {
      var dep appsv1.Deployment
      BeforeEach(func() {
        Eventually(
          deploymentExists(deployName, namespace, &dep),
          10, 1,
        ).Should(BeTrue())
      })
      It("should be owned by the MyResource instance", func() {
        Expect(dep.GetOwnerReferences()).
          To(ContainElement(*ownerref))
      })
      It("should use the image specified in MyResource instance", func() {
        Expect(
          dep.Spec.Template.Spec.Containers[0].Image,
        ).To(Equal(image))
      })
      When("deployment ReadyReplicas is 1", func() {
        BeforeEach(func() {
          dep.Status.Replicas = 1
          dep.Status.ReadyReplicas = 1
          err := k8sClient.Status().Update(ctx, &dep)
          Expect(err).NotTo(HaveOccurred())
        })
        It("should set status ready for MyResource instance", func() {
          Eventually(
            getMyResourceState(name, namespace), 10, 1,
          ).Should(Equal("Ready"))
        })
      })
    })
  })
})
func deploymentExists(
  name, namespace string, dep *appsv1.Deployment,
) func() bool {
  return func() bool {
    err := k8sClient.Get(ctx, client.ObjectKey{
      Namespace: namespace,
      Name:      name,
    }, dep)
    return err == nil
  }
}
func getMyResourceState(
  name, namespace string,
) func() (string, error) {
  return func() (string, error) {
    myres := mygroupv1alpha1.MyResource{}
    err := k8sClient.Get(ctx, types.NamespacedName{
      Namespace: namespace,
      Name:      name,
    }, &myres)
    if err != nil {
      return "", err
    }
    return myres.Status.State, nil
  }
}

1.6 测试执行流程

测试编号 测试条件 前置操作 测试内容 后置操作
1 创建MyResource实例 创建MyResource实例 检查是否创建Deployment 删除MyResource实例
2 创建MyResource实例,Deployment已找到 创建MyResource实例,等待Deployment创建 检查Deployment是否由MyResource实例拥有 删除MyResource实例
3 创建MyResource实例,Deployment已找到 创建MyResource实例,等待Deployment创建 检查Deployment是否使用MyResource实例指定的镜像 删除MyResource实例
4 创建MyResource实例,Deployment已找到,Deployment ReadyReplicas为1 创建MyResource实例,等待Deployment创建,更新Deployment状态 检查MyResource实例状态是否变为Ready 删除MyResource实例

1.7 测试流程mermaid图

graph LR
    classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
    classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px

    A([开始]):::startend --> B(创建MyResource实例):::process
    B --> C{Deployment是否创建}:::decision
    C -- 是 --> D(检查Deployment所有者):::process
    D --> E(检查Deployment镜像):::process
    E --> F{Deployment ReadyReplicas是否为1}:::decision
    F -- 是 --> G(检查MyResource状态):::process
    C -- 否 --> H(等待Deployment创建):::process
    H --> C
    F -- 否 --> I(更新Deployment状态):::process
    I --> F
    G --> J(删除MyResource实例):::process
    J --> K([结束]):::startend

2. 使用Kubebuilder创建Operator

2.1 Kubebuilder简介

Kubebuilder SDK可帮助创建新资源及其相关的Operator。它提供命令来初始化项目、添加资源和控制器,还能构建和部署自定义资源定义(CRD)和Manager到集群。

2.2 安装Kubebuilder

可从 此处 获取相关安装资源。

2.3 创建项目

创建项目的步骤如下:
1. 创建一个空目录并进入:

$ mkdir myresource-kb
$ cd myresource-kb
  1. 执行 kubebuilder init 命令:
$ kubebuilder init --domain myid.dev --repo github.com/myid/myresource
- `--domain`:域名,用作GVK组名的后缀。
- `--repo`:生成的Go代码的模块名。

创建项目后,可查看Makefile的可用命令:

$ make help

构建Manager二进制文件:

$ make build

本地运行Manager:

$ make run

或者直接执行二进制文件:

$ ./bin/manager

建议此时初始化版本控制项目(如Git)并提交初始版本:

$ git init
$ git commit -am 'kubebuilder init --domain myid.dev --repo github.com/myid/myresource'

2.4 向项目添加自定义资源

当前Manager没有管理任何控制器,需要执行 kubebuilder create api 命令添加自定义资源和相关控制器:

$ kubebuilder create api --group mygroup --version v1alpha1 --kind MyResource

执行命令后会询问是否创建资源和控制器,都回复 y

Create Resource [y/n]
y
Create Controller [y/n]
y

执行后可通过 git status 查看文件变化:

On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
    modified:   PROJECT
    modified:   go.mod
    modified:   go.sum
    modified:   main.go
Untracked files:
  (use "git add <file>..." to include in what will be committed)
    api/
    config/crd/
    config/rbac/myresource_editor_role.yaml
    config/rbac/myresource_viewer_role.yaml
    config/samples/
    controllers/
no changes added to commit (use "git add" and/or "git commit -a")

此命令的影响如下:
- PROJECT 文件:包含项目定义,新增了第一个资源的定义。
- main.go controllers 目录:定义了自定义资源的控制器。
- api/v1alpha1 目录:使用Go结构定义自定义资源,包含 deepcopy - gen 生成的代码和 AddToScheme 函数。
- config/samples 目录:包含一个新的YAML文件,定义了自定义资源的实例。
- config/rbac 目录:包含两个新的 ClusterRole 资源文件,分别用于查看和编辑 MyResource 实例。
- config/crd 目录:包含用于构建CRD的kustomize文件。

2.5 项目结构变化表格

目录/文件 变化说明
PROJECT 新增资源定义
go.mod, go.sum 文件被修改
main.go 文件被修改
api/v1alpha1 新增自定义资源定义及相关代码
config/samples 新增自定义资源实例YAML文件
config/rbac 新增两个ClusterRole资源文件
config/crd 包含构建CRD的kustomize文件
controllers 新增控制器相关代码

2.6 Kubebuilder项目创建流程mermaid图

graph LR
    classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
    classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px

    A([开始]):::startend --> B(创建空目录):::process
    B --> C(进入目录):::process
    C --> D(执行kubebuilder init):::process
    D --> E(查看Makefile命令):::process
    E --> F(构建Manager二进制文件):::process
    F --> G(本地运行Manager):::process
    G --> H(初始化Git项目):::process
    H --> I(提交初始版本):::process
    I --> J(执行kubebuilder create api):::process
    J --> K{是否创建资源和控制器}:::decision
    K -- 是 --> L(完成资源和控制器添加):::process
    K -- 否 --> M(结束操作):::process
    L --> N([结束]):::startend
    M --> N

综上所述,我们介绍了Kubernetes Operator的测试方法,包括使用ginkgo和envtest进行测试,以及如何使用Kubebuilder SDK创建Operator项目和添加自定义资源。这些方法和工具能帮助开发者更高效地开发和测试Kubernetes Operator。

根据原作 https://pan.quark.cn/s/0ed355622f0f 的源码改编 野火IM解决方案 野火IM是专业级即时通讯和实时音视频整体解决方案,由北京野火无限网络科技有限公司维护和支持。 主要特性有:私有部署安全可靠,性能强大,功能齐全,全平台支持,开源率高,部署运维简单,二次开发友好,方便第三方系统对接或者嵌入现有系统中。 详细情况请参考在线文档。 主要包括一下项目: 野火IM Vue Electron Demo,演示如何将野火IM的能力集成到Vue Electron项目。 前置说明 本项目所使用的是需要付费的,价格请参考费用详情 支持试用,具体请看试用说明 本项目默认只能连接到官方服务,购买或申请试用之后,替换,即可连到自行部署的服务 分支说明 :基于开发,是未来的开发重心 :基于开发,进入维护模式,不再开发新功能,鉴于已经终止支持且不再维护,建议客户升级到版本 环境依赖 mac系统 最新版本的Xcode nodejs v18.19.0 npm v10.2.3 python 2.7.x git npm install -g node-gyp@8.3.0 windows系统 nodejs v18.19.0 python 2.7.x git npm 6.14.15 npm install --global --vs2019 --production windows-build-tools 本步安装windows开发环境的安装内容较多,如果网络情况不好可能需要等较长时间,选择早上网络较好时安装是个好的选择 或参考手动安装 windows-build-tools进行安装 npm install -g node-gyp@8.3.0 linux系统 nodej...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值