关于程序员:还在手写Operator是时候使用Kubebuilder了

5次阅读

共计 19353 个字符,预计需要花费 49 分钟才能阅读完成。

前言

Kubernetes 当初曾经成为了事实的云原生分布式操作系统,其最大的劣势在于扩展性,比方在计算,存储,网络都能够依据使用者的需要进行扩大。另外一个重要扩大就是 Custom Resource 个性,通过 Custom Resource 开发者能够定义本人的资源,而后实现对应的 Operator 来和谐实现本身的管制逻辑。

以前开发 Operator 须要开发者实现资源的监听,资源事件的队列化,以及前面的整套管制逻辑,比拟繁琐,正因为如此,市场上呈现了多款开发 Operator 的脚手架,比拟罕用的有 Operator-SDK 和 Kubebuilder,他们两者其实都是对 Controller Runtime(Kubernetes SIG 官网封装和形象的开发 Operator 的公共库)的封装,Operator-SDK 是 CoreOS 出品,Kubebuilder 则是 Kubernetes-SIG 官网团队原生打造,因而咱们的教程是用 Kubebuilder 来示范开发本人的 Operator。

原理篇

Kubebuilder 脚手架生成 Operator 的代码后,开发者只须要在 Reconciler 外面实现本人的管制逻辑,下图中除 Reconciler 外,其它局部的都是 Kubebuilder 主动生成的。生成的代码底层间接依赖 Controller Runtime 这个 Kubernetes SIG 保护的外围库,然而这个库大家理解的不多,这就让它成为了黑盒,开发者在开发本人的 Operator 的时候往往会心里没底,因而我画出了残缺的原理图,而后依据该图做具体的解释,下图就是整体的原理图:

咱们先把每个外围概念在上面介绍一下:

GVK & GVR

  • GVK = Group + Version + Kind  组合而来的,资源品种形容术语,例如 deployment kind 的 GVK 是 apps /v1/deployments,用来惟一标识某个品种资源
  • GVR = Group + Version + Resource  组合而来的,资源实例形容术语,例如某个 deployment 的 name 是 sample,那么它的 GVR 则是 apps /v1/sample,用来惟一标识某个类型资源的具体对象

Group 是相干 API 性能汇合,每个 Group 领有一个或多个 Version,用于接口的演进,Kind 关联着一个 Package 中定义的 Go Type,比方 apps/v1/deployment 就关联着 Kubernetes 源码外面的 k8s.io/api/apps/v1 package 中的 Deployment 的 struct,天然 GVK 实例化进去的资源对象就是 GVR。

CRD & CR

CRD 即 Custom Resource Definition,是 Kubernetes 提供给开发者自定义类型资源的性能,开发者自定义 CRD 而后实现该 CRD 对应的 Operator 来实现对应的管制逻辑,CRD 是告诉 Kubernetes 平台存在一种新的资源,CR 则是该 CRD 定义的具体的实例对象,CRD 就是某个类型的 GVK,而 CR 则对应 GVR 示意某个具体资源类型的对象。

Scheme

这里存储了 GVK 对应的 Go Type 的映射关系,相同也存储了 Go Type 对应 GVK 的映射关系,也就是说给定 Go Type 就晓得他的 GVK,给定 GVK 就晓得他的 Go Type,上图中 Kubebuilder 生成的代码里就主动生成了 Scheme,该 Scheme 外面存储了 Kubernetes 原生资源和自定义的 CRD 的 GVK 和 Go Type 的映射关系,例如咱们收到 Kubernetes APIServer 的 GVR 的 JSON 数据之后如下:

{    
  "kind": "MyJob",    "apiVersion": "myjob.github.com/v1beta1",   
    ...
}

依据 JSON 数据外面的 kind 和 apiVersion 字段即取得了 GVK,而后就能依据 GVK 取得 Go Type 来反序列化出对应的 GVR。

Manager

Controller Runtime 形象的最外层治理对象,负责管理外部的 Controller,Cache,Client 等对象。

Cache

负责管理 GVK 对应的 Share Informer,GVK 和 Share Informer 是一一对应的,一个 GVK 只会存在对应的一个 Share Informer,外面治理的 Share Informer 只有 Controller Watch 才会创立出 GVK 对应的 Share Informer,而后下层所有的 Controller 依据 GVK 共享该 Share Informer,Share Informer 会负责监听对应 GVK 的 GVR 的创立 / 删除 / 更新操作,而后告诉所有 Watch 该 GVK 的 Controller,Controller 将对应的资源名称增加到 Queue 外面,最终触发开发者的 Reconciler 的和谐。

Client

Reconciler 对资源的创立 / 删除 / 更新操作都是通过该对象去操作,外面分为两种 Client:

  • Read Client  则是对应资源的读操作,该操作不会去拜访 Kubernetes APIServer,而是去拜访 GVK 对应的 Share Informer 对应的本地缓存
  • Write Client  则是对应资源的写操作,该操作则会间接去拜访 Kubernetes APIServer

开发者不必去抉择应用哪种 Client,而是间接去应用从 Manager 对象获取到的 Client 而后应用 Create/Update/Delete 接口去操作对应的 GVR,Client 外面会主动帮你实现对应的操作。

Controller

该对象跟开发者要实现的逻辑 Reconciler 是一一对应的关系,外面有创立的带限速性能的 Queue,以及该 Controller 关注 GVK 的 Watcher,一个 Controller 能够关注很多 GVK,该关注会依据 GVK 到 Cache 外面找到对应的 Share Informer 去 Watch 资源,Watch 到的事件会退出到 Queue 外面,Queue 最终触发开发者的 Reconciler 的和谐。

Reconciler

接管 Controller 发送给本人的 GVR 事件,而后从 Cache 中读取出 GVR 的以后状态,通过本人的管制逻辑,通过 Client 向 Kubernetes APIServer 更新 GVR 资源, 开发者只须要在 Reconciler 实现本人的管制逻辑 ,示意图如下:

咱们以 MyJob CRD 这个 Operator 示例来阐明整个流程:

  1. 初始化 Scheme,将 Kubernetes 的原生资源以及 MyJob 资源的 GVK 和 Go Type 注册进去
  2. 初始化 Manager,会将下面初始结束的 Scheme 传入进去,Manager 外部会初始化 Cache 和 Client
  3. 初始化 Reconciler,同时将该 Reconciler 注册到 Manager,同时会在 Manager 外面初始化一个 Controller 与该 Reconciler 对应
  4. Reconciler Watch MyJob 和 Pod 资源

    1. Watch MyJob 资源,Controller 会从 Cache 外面去获取 MyJob 的 Share Informer,如果没有则创立,而后对该 Share Informer 进行 Watch,将失去的 MyJob 的名字和 Namespace 扔进 Queue
    2. Watch Pod 资源,Controller 会从 Cache 外面去获取 Pod 的 Share Informer,如果没有则创立,而后对该 Share Informer 进行 Watch,将失去的 Pod 资源的 Owner 是 MyJob 的名字和 Namespace 扔进 Queue
  5. 最终 Controller 将所有 Watch 的资源事件扔到 Queue 后,Controller 会将 Queue 里的 MyJob 的名字和 Namespace 去触发 Reconciler 的 Reconcile 接口进行和谐
  6. 开发者只须要在 Reconciler 外面接管到对应 GVR 的事件去实现对应的管制逻辑,下面的步骤则间接由 Kubebuilder 生成的代码主动实现

最初有了以上的外围概念之后,咱们能够总结出一个残缺的 Operator 概念层级图:

实际篇

Kubebuilder 的装置请参考官网教程

1.  初始化我的项目

kubebuilder init --domain github.com

2.  创立 CRD

kubebuilder create api --group myjob --version v1beta1 --kind MyJob

下面命令执行结束后我的项目构造如下:

├── Dockerfile

├── Makefile

├── PROJECT                                                 // Kubebuilder 主动生成的我的项目元数据

├── README.md

├── api

│   └── v1beta1

│       ├── groupversion_info.go                            // GV(GroupVersion) 定义以及 CRD 向 Scheme 注册的办法

│       ├── myjob_types.go                                  // 自定义 CRD 对应的 struct 的中央

│       └── zz_generated.deepcopy.go                        // Kubebuilder 工具主动生成的 GVR DeepCopy 的办法

├── bin

│   └── manager

├── config

│   ├── certmanager

│   │   ├── certificate.yaml

│   │   ├── kustomization.yaml

│   │   └── kustomizeconfig.yaml

│   ├── crd                                                 // 部署 CRD 的相干 Yaml 汇合

│   │   ├── bases

│   │   │   └── myjob.github.com_myjobs.yaml

│   │   ├── kustomization.yaml

│   │   ├── kustomizeconfig.yaml

│   │   └── patches

│   │       ├── cainjection_in_myjobs.yaml

│   │       └── webhook_in_myjobs.yaml

│   ├── default                                             // 应用 Kustomize 部署该 Operator 的一个默认 Yaml 汇合,它以 crd,rbac,manager 为 base,具体的能够去学习 Kustomize 相干的用法

│   │   ├── kustomization.yaml

│   │   ├── manager_auth_proxy_patch.yaml

│   │   ├── manager_webhook_patch.yaml

│   │   └── webhookcainjection_patch.yaml

│   ├── manager                                             // 部署 Operator 的相干 Yaml 汇合

│   │   ├── kustomization.yaml

│   │   └── manager.yaml

│   ├── prometheus                                          // Operator 运行监控相干的 Yaml 汇合

│   │   ├── kustomization.yaml

│   │   └── monitor.yaml

│   ├── rbac                                                // Operator 部署须要的 RBAC 权限相干的 Yaml 汇合

│   │   ├── auth_proxy_client_clusterrole.yaml

│   │   ├── auth_proxy_role.yaml

│   │   ├── auth_proxy_role_binding.yaml

│   │   ├── auth_proxy_service.yaml

│   │   ├── kustomization.yaml

│   │   ├── leader_election_role.yaml

│   │   ├── leader_election_role_binding.yaml

│   │   ├── myjob_editor_role.yaml

│   │   ├── myjob_viewer_role.yaml

│   │   ├── role.yaml

│   │   └── role_binding.yaml

│   ├── samples                                             // 部署一个 CR 示例的 Yaml

│   │   └── myjob_v1beta1_myjob.yaml

│   └── webhook

│       ├── kustomization.yaml

│       ├── kustomizeconfig.yaml

│       └── service.yaml

├── controllers

│   ├── myjob_controller.go                                 // 开发者实现 Reconciler,实现管制逻辑的文件,对应下面原理图的 Reconciler

│   ├── myjob_controller_test.go

│   └── suite_test.go

├── cover.out

├── go.mod

├── go.sum

├── hack

│   └── boilerplate.go.txt

└── main.go                                                 // 该文件是 Kubebuilder 主动生成的,该文件外面会会对应下面原理图中的 Scheme 初始化,以及 Manager 的初始化,而后将 Reconciler 增加到 Manager 中 

3. 定义 CRD

对应 api/v1beta1/myjob_types.go 文件:

package v1beta1

import (   v1
                        "k8s.io/api/core/v1" 
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
       ) 

const (
  // myjob 刚创立的时候默认状态
  MyJobPending = "pending" 
  
  // myjob 治理的 pod 创立后对应的状态
  MyJobRunning = "running" 
  // myjob 治理的 pod 执行实现后对应的状态
  MyJobCompleted = "completed"
) 

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.

// MyJobSpec defines the desired state of MyJobtype MyJobSpec struct {  
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "make" to regenerate code after modifying this file
Template v1.PodTemplateSpec `json:"template" protobuf:"bytes,6,opt,name=template"`
} 

// MyJobStatus defines the observed state of MyJobtype MyJobStatus struct {   
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster 
// Important: Run "make" to regenerate code after modifying this file   
// +optional
Phase string `json:"phase,omitempty"`
}

func (j *MyJobStatus) SetDefault(job *MyJob) bool {
  changed := false 
  
  if job.Status.Phase == "" {
    job.Status.Phase = MyJobPending 
    changed = true
  }
  
  return changed
} 

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// MyJob is the Schema for the myjobs APItype MyJob struct {  
metav1.TypeMeta   `json:",inline"` 
metav1.ObjectMeta `json:"metadata,omitempty"` 

Spec   MyJobSpec   `json:"spec,omitempty"` 
Status MyJobStatus `json:"status,omitempty"`
} 

func (j *MyJob) StatusSetDefault() bool {return j.Status.SetDefault(j)
} 
// +kubebuilder:object:root=true 

// MyJobList contains a list of MyJobtype MyJobList struct {
metav1.TypeMeta `json:",inline"` 
metav1.ListMeta `json:"metadata,omitempty"`
Items           []MyJob `json:"items"`} 

func init() {SchemeBuilder.Register(&MyJob{}, &MyJobList{})
}

MyJob 的逻辑咱们以简略为主,次要阐明整个的开发流程,每个 MyJob 都会只创立一个与本人名字和 Namespace 截然不同的 Pod,MyJob 初始状态为 Pending,当对应的 Pod 创立进去,则 MyJob 的状态变成 Running,当 Pod 执行结束变成 Succeeded 或者 Failed 或者 正在被删除后,则 MyJob 的状态变成 Completed 状态。

4.  开发控制器逻辑

该 Operator 须要创立 Pod,因而须要给该 Operator 创立 Pod 的权限,Kubebuilder 反对主动生成 Operator 的 RBAC,然而须要开发者在管制逻辑加上标识,此处咱们加上对 Pod 有读写的权限的标识:

// +kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch;create;update;patch;delete

标识的具体参考  Kubebuilder 标记教程,这样最初在部署的时候会依据开发者增加的这些标识由工具主动生成对应的 RBAC Yaml 文件。

对应 controllers/myjob_controller.go 文件:

 package controllers

import (   
  "context"
  
  "github.com/go-logr/logr" 
  corev1 "k8s.io/api/core/v1"
  "k8s.io/apimachinery/pkg/api/errors"
  metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  "k8s.io/apimachinery/pkg/runtime"
  ctrl "sigs.k8s.io/controller-runtime" 
  "sigs.k8s.io/controller-runtime/pkg/client"
  "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
  "sigs.k8s.io/controller-runtime/pkg/handler"
  "sigs.k8s.io/controller-runtime/pkg/source"
  
  myjobv1beta1 "github.com/sky-big/myjob-operator/api/v1beta1")

// MyJobReconciler reconciles a MyJob object
type MyJobReconciler struct { 
  client.Client
  Log    logr.Logger
  Scheme *runtime.Scheme
}

// +kubebuilder:rbac:groups=myjob.github.com,resources=myjobs,verbs=get;list;watch;create;update;patch;delete

// +kubebuilder:rbac:groups=myjob.github.com,resources=myjobs/status,verbs=get;update;patch

// 打上该控制器须要 Pod 所有权限标识,Kubebuilder 在生成 RBAC 的时候会读取该标识而后生成对 Pod 的权限 // +kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch;create;update;patch;delete

func (r *MyJobReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) 
{ctx := context.Background()
  logger := r.Log.WithValues("myjob", req.NamespacedName)
  
  // your logic here
  j := &myjobv1beta1.MyJob{}
  if err := r.Get(ctx, req.NamespacedName, j); err != nil {return ctrl.Result{}, client.IgnoreNotFound(err) 
  }
 
  // 设置 MyJob 的 Status 的默认值
  if j.StatusSetDefault() {if err := r.Status().Update(ctx, j); err != nil {return ctrl.Result{}, err 
    }      return ctrl.Result{Requeue: true}, nil 
  }
  
  // Pod 不存在则创立 Pod,如果存在查看 Pod 的状态
  p := &corev1.Pod{}
  err := r.Get(ctx, req.NamespacedName, p)
  if err == nil {
    // MyJob 的状态还是 Pending,然而对应的 Pod 曾经创立,则将 MyJob 的状态置为 Running
    if !isPodCompleted(p) && myjobv1beta1.MyJobRunning != j.Status.Phase {j.Status.Phase = myjobv1beta1.MyJobRunning         if err := r.Status().Update(ctx, j); err != nil {return ctrl.Result{}, err         }         logger.Info("myjob phase changed", "Phase", myjobv1beta1.MyJobRunning)      }
      // MyJob 对应的 Pod 曾经执行结束,则将 MyJob 的状态置为 Completed
    if isPodCompleted(p) && myjobv1beta1.MyJobRunning == j.Status.Phase 
    { 
      j.Status.Phase = myjobv1beta1.MyJobCompleted
      if err := r.Status().Update(ctx, j); err != nil {return ctrl.Result{}, err 
      }
      logger.Info("myjob phase changed", "Phase", myjobv1beta1.MyJobCompleted) 
    }
   } else if err != nil && errors.IsNotFound(err) { 
     
     // 创立 MyJob 对应的 Pod
     pod := makePodByMyJob(j)
     if err := controllerutil.SetControllerReference(j, pod, r.Scheme); err != nil {return ctrl.Result{}, err 
     }
      if err := r.Create(ctx, pod); err != nil && !errors.IsAlreadyExists(err) {return ctrl.Result{}, err
      } 
     logger.Info("myjob create pod success")
   } 
  else {return ctrl.Result{}, err 
  }
  
  return ctrl.Result{}, nil}

func (r *MyJobReconciler) SetupWithManager(mgr ctrl.Manager) error {c := ctrl.NewControllerManagedBy(mgr)
 
  // 监督拥有者是 MyJob 类型的 Pod,同时将 Pod 的拥有者 MyJob 扔进解决队列中,对 MyJob 进行和谐
  
  c.Watches(&source.Kind{Type: &corev1.Pod{}}, &handler.EnqueueRequestForOwner{ 
    IsController: true,  
      OwnerType:    &myjobv1beta1.MyJob{},})

  
 return c.For(&myjobv1beta1.MyJob{}).
 Complete(r)
}

func isPodCompleted(pod *corev1.Pod) bool {
  if corev1.PodSucceeded == pod.Status.Phase ||
    corev1.PodFailed == pod.Status.Phase || 
    pod.DeletionTimestamp != nil {return true} 
  return false
}
func makePodByMyJob(j *myjobv1beta1.MyJob) *corev1.Pod {
  return &corev1.Pod{ 
    ObjectMeta: metav1.ObjectMeta{
      Name:      j.Name, 
        Namespace: j.Namespace,
    },      Spec: *j.Spec.Template.Spec.DeepCopy(),}
}

5. 编写集成测试

Kubebuilder 应用了 Controller-Runtime 提供的  envtest  来帮忙开发者来写集成测试,这个包会帮忙你独自启动 Kubernetes APIServer 以及 Etcd 服务(留神 Kubebuilder 官网安装包外面会蕴含这两个服务的可执行文件,如果开发者是本人编译部署装置的 Kubebuilder,则开发者须要独自装置这两个服务的可执行文件),这两个过程专门用来帮忙你进行集成测试,请留神这两个服务是实在启动在你的开发机器上的,因而要留神 APIServer 以及 Etcd 对应的端口不要被占用,同时你要启动下面原理图的 Manager 以及你的 Reconciler,Manager 的监控服务会占用 8080 端口也须要特地留神不被占用或者本人指定其它端口,咱们依照以下步骤来实现集成测试的开发:

1) Kubebuilder 在生成的代码外面会在 controllers 目录下生成文件 suite_test.go 文件,外面曾经帮忙你启动了 envtest,然而咱们还要在此文件里增加启动咱们本人的 Manager 以及 Reconciler,代码如下:

package controllers

import ( 
  "path/filepath"   "testing"  
  . "github.com/onsi/ginkgo"  
  . "github.com/onsi/gomega" 
  "k8s.io/client-go/kubernetes/scheme"
  "k8s.io/client-go/rest" 
  ctrl "sigs.k8s.io/controller-runtime"
  "sigs.k8s.io/controller-runtime/pkg/client"
  "sigs.k8s.io/controller-runtime/pkg/envtest"
  "sigs.k8s.io/controller-runtime/pkg/envtest/printer"
  logf "sigs.k8s.io/controller-runtime/pkg/log"
  "sigs.k8s.io/controller-runtime/pkg/log/zap"
  
  myjobv1beta1 "github.com/sky-big/myjob-operator/api/v1beta1"
  // +kubebuilder:scaffold:imports
)
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
var cfg *rest.Config
var k8sClient client.Clientvar testEnv *en
vtest.Environment

func TestAPIs(t *testing.T) {RegisterFailHandler(Fail)
  
  RunSpecsWithDefaultAndCustomReporters(t, 
                                        "Controller Suite",
                                        []Reporter{printer.NewlineReporter{}})
} 
var _ = BeforeSuite(func(done Done) {logf.SetLogger(zap.LoggerTo(GinkgoWriter, true))
By("bootstrapping test environment") 
testEnv = &envtest.Environment{CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, 
} 

var err error
cfg, err = testEnv.Start() 
Expect(err).ToNot(HaveOccurred())
Expect(cfg).ToNot(BeNil())

err = myjobv1beta1.AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())

// +kubebuilder:scaffold:scheme

k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
Expect(err).ToNot(HaveOccurred())
Expect(k8sClient).ToNot(BeNil()) 

// * 号两头这块代码是咱们在 Kubebuilder 生成的代码上增加的代码,增加的代码逻辑次要是启动 Manager 以及咱们本人的 Reconciler 控制器 
// ******************************************
// 创立 manager
k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{
                                   Scheme:             scheme.Scheme,
                                   MetricsBindAddress: ":8082",
                                   })
Expect(err).ToNot(HaveOccurred())

// 启动 myjob reconciler
err = (&MyJobReconciler{Client: k8sManager.GetClient(),
  Log:    ctrl.Log.WithName("controllers").WithName("MyJob"),
    Scheme: k8sManager.GetScheme(),}).SetupWithManager(k8sManager)
Expect(err).ToNot(HaveOccurred()) 

// 启动 manager 
go func() {err = k8sManager.Start(ctrl.SetupSignalHandler()) 
  Expect(err).ToNot(HaveOccurred())
}()

k8sClient = k8sManager.GetClient()
Expect(k8sClient).ToNot(BeNil())
// ****************************************** 
close(done)
}, 60) 

var _ = AfterSuite(func() {By("tearing down the test environment")
err := testEnv.Stop()
Expect(err).ToNot(HaveOccurred())
})

2) 下面的步骤实现后,咱们其实能够晓得,目前 Kubernetes APIServer,Etcd 曾经启动结束,同时咱们本人的 Manager,Reconciler 启动实现,这样咱们就能够编写对应的测试用例来测试咱们的 Operator 了,咱们在 controllers 目录下创立 myjob_controller_test.go 文件来编写具体的测试用例的文件,上面的测试用例流程是『创立 myjob → 验证 myjob 创立胜利 → 验证 myjob 对应的 pod 创立胜利 → 验证 myjob 的状态是否 running → Mock 对应的 pod 执行结束 → 验证 myjob 的状态变成 completed 状态』,代码如下:

package controllers

import (
  "context"
  "time" 
  
  . "github.com/onsi/ginkgo"
  . "github.com/onsi/gomega" 
  v1 "k8s.io/api/core/v1" 
  metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  "k8s.io/apimachinery/pkg/types"
  
  myjobv1beta1 "github.com/sky-big/myjob-operator/api/v1beta1") 

var _ = Describe("MyJob controller", func() {
                 const ( 
                 MyjobName      = "test-myjob"
                 MyjobNamespace = "default"
                 
                 timeout  = time.Second * 10 
                 duration = time.Second * 10 
                 interval = time.Millisecond * 250 
                )

Context("When creating MyJob", func() {It("Should be success", func() {By("By creating a new MyJob") 
  ctx := context.Background()
  
  // 0. 创立 myjob 
  cronJob := &myjobv1beta1.MyJob{
    TypeMeta: metav1.TypeMeta{ 
      APIVersion: "myjob.github.com/v1beta1",  
        Kind:       "MyJob", 
    }, 
      ObjectMeta: metav1.ObjectMeta{
        Name:      MyjobName,
          Namespace: MyjobNamespace,
      },  
        Spec: myjobv1beta1.MyJobSpec{ 
          Template: v1.PodTemplateSpec{  
            Spec: v1.PodSpec{Containers: []v1.Container{
                v1.Container{ 
                  Name:    "pi", 
                    Image:   "perl", 
                      Command: []string{"perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"},  
                }, 
              },    
            }, 
          }, 
        },  
  }  
  Expect(k8sClient.Create(ctx, cronJob)).Should(Succeed())
  
  myjobKey := types.NamespacedName{Name: MyjobName, Namespace: MyjobNamespace}
  createdMyjob := &myjobv1beta1.MyJob{} 
  
  // 1. 验证 myjob 创立胜利
  Eventually(func() bool {err := k8sClient.Get(ctx, myjobKey, createdMyjob)
  if err != nil {return false} 
  return true
}, timeout, interval).Should(BeTrue())
Expect(createdMyjob.Name).Should(Equal(MyjobName))

// 2. 验证 myjob 创立 pod
myPodKey := types.NamespacedName{Name: MyjobName, Namespace: MyjobNamespace}
myPod := &v1.Pod{} 
Consistently(func() (string, error) {err := k8sClient.Get(ctx, myPodKey, myPod) 
if err != nil {return "", err} 
return myPod.Name, nil
}, duration, interval).Should(Equal(MyjobName))

// 3. 验证 myjob 状态变为 Running
runningMyjob := &myjobv1beta1.MyJob{} 
Consistently(func() bool {err := k8sClient.Get(ctx, myjobKey, runningMyjob)
if err != nil {return false}  
return runningMyjob.Status.Phase == myjobv1beta1.MyJobRunning
}, duration, interval).Should(BeTrue())

// 4. Mock Pod 工作实现 
mockPod := &v1.Pod{}  
Consistently(func() bool {err := k8sClient.Get(ctx, myPodKey, mockPod)
if err != nil {return false} 
copy := mockPod.DeepCopy() 
copy.Status.Phase = v1.PodSucceeded
err = k8sClient.Status().Update(context.TODO(), copy)

if err != nil {return false} 
return true
}, duration, interval).Should(BeTrue())

// 5. 验证 myjob 状态变为 Completed
completedMyjob := &myjobv1beta1.MyJob{}
Consistently(func() bool {err := k8sClient.Get(ctx, myjobKey, completedMyjob)
if err != nil {return false} 
return completedMyjob.Status.Phase == myjobv1beta1.MyJobCompleted
}, duration, interval).Should(BeTrue())
})
})
})

上述的测试用例中应用了 Ginkgo 以及 Gomega 测试相干的辅助包,具体应用办法参见官网文档。

  1. 执行集成测试 & 编译

===============

$ make test
go test ./... -coverprofile cover.out
  ?       github.com/sky-big/myjob-operator   [no test files]
  ?       github.com/sky-big/myjob-operator/api/v1beta1   [no test files]
ok      github.com/sky-big/myjob-operator/controllers   46.381s coverage: 81.6% of statements
最终后果测试胜利,控制器逻辑代码覆盖率达到 81.6% 
  
  # 编译
$ make
go fmt ./...
go vet ./...
go build -o bin/manager main.go
编译胜利,可执行文件存在 bin 目录上面 

编译过程会主动装置 controller-gen,而后应用它依据 api/v1beta1/myjob_types.go 来生成深度拷贝等通用代码。

7. 打包上传 Docker 镜像

打包镜像对应根目录下的 Dockerfile 文件因为国内网络的问题,须要批改两处:

  • 在 Run go mod download 后面增加一行设置 GOPROXY:RUN go env -w GOPROXY=https://goproxy.cn
FROM golang:1.13 as builder

 

WORKDIR /workspace

# Copy the Go Modules manifests

COPY go.mod go.mod

COPY go.sum go.sum

# cache deps before building and copying source so that we don't need to re-download as much

# and so that source changes don't invalidate our downloaded layer

RUN go env -w GOPROXY=https://goproxy.cn

RUN go mod download 

  • 将 FROM gcr.io/distroless/static:nonroot 换成国内的镜像源,我在 dockerhub 上找了一个下载量较多的源:kubeimages/distroless-static:latest
# FROM gcr.io/distroless/static:nonroot
FROM kubeimages/distroless-static:latest
WORKDIR /
  COPY --from=builder /workspace/manager .
  USER nonroot:nonroot
  
  ENTRYPOINT ["/manager"]

上传镜像的时候须要批改根目录下的 Makefile 文件的第一行,指定镜像的存储的仓库地址以及镜像名称和 Tag,上面填的是我本人的 dockerhub 的仓库地址,在 Push 之前开发者须要登录本人的仓库。

# Image URL to use all building/pushing image targets
IMG ?= skybig/myjob-operator:latest

批改结束后,执行以下命令即可:

# 打包镜像
$ make docker-build
# 上传镜像
$ make docker-push

Operator 打包成镜像后,通过该镜像启动容器后,会启动咱们的控制器。

  1. 部署

目前 Kubebuilder 最新版本不反对 Kubernetes 1.18 版本,对应的 BUG 曾经修复,然而还没有公布到最新版本的 Kubebuilder,这是对应的  PR(https://github.com/kubernetes…),Kubernetes 1.18 以下版本应用没有问题,这是须要留神的点。

我增加了一个卸载的 Makefile Target 在 Makefile 文件中,不便测试。

# Deploy controller in the configured Kubernetes cluster in ~/.kube/config
deploy: manifests kustomize
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
$(KUSTOMIZE) build config/default | kubectl apply -f -
  
  undeploy: manifests kustomize
  cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
$(KUSTOMIZE) build config/default | kubectl delete -f -

Kubebuilder yaml 的治理都是通过 kustomize 进行治理的,该工具在这里就就不细说了,它是  Kubernetes 原生概念帮忙用户创作并复用申明式配置,在 kustomize 呈现之前,Kubernetes 治理利用的形式次要是通过 Helm 或者下层 Paas 来实现。

批改结束后,执行以下命令即可:

# 将 Operator 部署在 Kubernetes 集群中
$ make deploy
# 查看 Operator 对应的 Pod 的状态
$ kubectl get pods -A
NAMESPACE               NAME
READY   STATUS      RESTARTS   AGEmyjob-operator-system
myjob-operator-controller-manager-65489c68c8-md2w7 
2/2     Running     0          143m

9. 测试

测试的 yaml 对应在 config/sample/myjob_v1beta1_myjob.yaml 文件中,我依据最新定义的 CRD 批改了一下:

apiVersion: myjob.github.com/v1beta1
kind: MyJob
metadata:
name: myjob-sample
spec: 
# Add fields here
template:
metadata:  
name: pi
spec: 
containers:
- name: pi
image: perl 
command: ["perl",  "-Mbignum=bpi", "-wle", "print bpi(2000)" ] 
restartPolicy: Never

而后在我的项目根目录下执行命令进行测试:

# 部署测试 MyJob
$ kubectl apply -f config/sample/myjob_v1beta1_myjob.yaml
# 查看 MyJob 对应的 Pod 执行状态
$ kubectl get pods -A
NAMESPACE               NAME
READY   STATUS      RESTARTS   AGE
default                 myjob-sample 
0/1     Completed   0          143m
myjob-operator-system   myjob-operator-controller-manager-65489c68c8-md2w7
2/2     Running     0          143m
# 查看 MyJob 的状态
$ kubectl get myjobs -A
NAMESPACE   NAME           AGE
default     myjob-sample   146m
$ kubectl describe myjob myjob-sample
Name:         myjob-sample
Namespace:    default
Labels:       <none>
  Annotations:  API Version:  myjob.github.com/v1beta1
Kind:         MyJob
Metadata:
Creation Timestamp:  2020-11-16T08:58:31Z
Generation:          1
Resource Version:    652546
Self Link:           /apis/myjob.github.com/v1beta1/namespaces/default/myjobs/myjob-sample
UID:                 1ae6e8b0-931b-4630-b3ea-bf60e94bf2d0
Spec:
Template: 
Metadata: 
Name:  pi 
Spec:
Containers:
Command: 
perl
  -Mbignum=bpi
  -wle
print bpi(2000)
Image:         perl
Name:          pi
Restart Policy:  Never
Status: 
Phase:  completed
Events:   <none

能够看到 MyJob 的状态变成了 Completed 实现状态,咱们开发的 MyJob Operator 也就从零开始到当初完满完结了。

=

举荐浏览

=====

为什么阿里巴巴的程序员成长速度这么快,看完他们的内部资料我懂了

字节跳动总结的设计模式 PDF 火了,完整版凋谢下载

刷 Github 时发现了一本阿里大神的算法笔记!标星 70.5K

程序员 50W 年薪的常识体系与成长路线。

月薪在 30K 以下的 Java 程序员,可能听不懂这个我的项目;

字节跳动总结的设计模式 PDF 火了,完整版凋谢分享

对于【暴力递归算法】你所不晓得的思路

开拓鸿蒙,谁做零碎,聊聊华为微内核

 
=

看完三件事❤️

如果你感觉这篇内容对你还蛮有帮忙,我想邀请你帮我三个小忙:

点赞,转发,有你们的『点赞和评论』,才是我发明的能源。

关注公众号『Java 斗帝』,不定期分享原创常识。

同时能够期待后续文章 ing????

正文完
 0