使用client-go实现自定义控制器

介绍

我们已经知道,Service对集群之外暴露服务的主要方式有两种:NodePort和LoadBalancer,但是这两种方式,都有一定的缺点:

  • NodePort方式的缺点是会占用很多集群机器的端口,那么当集群服务变多的时候,这个缺点就愈发明显。
  • LoadBalancer的缺点是每个Service都需要一个LB,浪费,麻烦,并且需要Kubernetes之外的设备的支持。

基于这种现状,Kubernetes提供了Ingress资源对象,Ingress只需要一个NodePort或者一个LB就可以满足暴露多个Service的需求。

客户端首先对 域名 执行 DNS 解析,得到 Ingress Controller 所在节点的 IP,然后客户端向 Ingress Controller 发送 HTTP 请求,然后根据 Ingress 对象里面的描述匹配域名,找到对应的 Service 对象,并获取关联的 Endpoints 列表,将客户端的请求转发给其中一个 Pod。

本文我们来使用client-go实现一个自定义控制器,通过判断serviceAnnotations属性是否包含ingress/http字段,如果包含则创建ingress,如果不包含则不创建。而且如果存在ingress则进行删除。

具体实现

首先我们创建项目。

$ mkdir ingress-manager && cd ingress-manager
$ go mod init ingress-manager # 由于控制器部分的内容比较多,将它们单独放到pkg目录下
$ mkdir pkg # 最终项目目录结构如下
.
├── go.mod
├── go.sum
├── main.go
└── pkg
└── controller.go

接着我们来实现controller部分:

package pkg

import (
"context"
apiCoreV1 "k8s.io/api/core/v1"
netV1 "k8s.io/api/networking/v1"
"k8s.io/apimachinery/pkg/api/errors"
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
informersCoreV1 "k8s.io/client-go/informers/core/v1"
informersNetV1 "k8s.io/client-go/informers/networking/v1"
"k8s.io/client-go/kubernetes"
coreV1 "k8s.io/client-go/listers/core/v1"
v1 "k8s.io/client-go/listers/networking/v1"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
"reflect"
"time"
) const (
workNum = 5 // 工作的节点数
maxRetry = 10 // 最大重试次数
) // 定义控制器
type Controller struct {
client kubernetes.Interface
ingressLister v1.IngressLister
serviceLister coreV1.ServiceLister
queue workqueue.RateLimitingInterface
} // 初始化控制器
func NewController(client kubernetes.Interface, serviceInformer informersCoreV1.ServiceInformer, ingressInformer informersNetV1.IngressInformer) Controller {
c := Controller{
client: client,
ingressLister: ingressInformer.Lister(),
serviceLister: serviceInformer.Lister(),
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "ingressManager"),
} // 添加事件处理函数
serviceInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: c.addService,
UpdateFunc: c.updateService,
}) ingressInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
DeleteFunc: c.deleteIngress,
})
return c
} // 入队
func (c *Controller) enqueue(obj interface{}) {
key, err := cache.MetaNamespaceKeyFunc(obj)
if err != nil {
runtime.HandleError(err)
}
c.queue.Add(key)
} func (c *Controller) addService(obj interface{}) {
c.enqueue(obj)
} func (c *Controller) updateService(oldObj, newObj interface{}) {
// todo 比较annotation
// 这里只是比较了对象是否相同,如果相同,直接返回
if reflect.DeepEqual(oldObj, newObj) {
return
}
c.enqueue(newObj)
} func (c *Controller) deleteIngress(obj interface{}) {
ingress := obj.(*netV1.Ingress)
ownerReference := metaV1.GetControllerOf(ingress)
if ownerReference == nil {
return
} // 判断是否为真的service
if ownerReference.Kind != "Service" {
return
} c.queue.Add(ingress.Namespace + "/" + ingress.Name)
} // 启动控制器,可以看到开了五个协程,真正干活的是worker
func (c *Controller) Run(stopCh chan struct{}) {
for i := 0; i < workNum; i++ {
go wait.Until(c.worker, time.Minute, stopCh)
}
<-stopCh
} func (c *Controller) worker() {
for c.processNextItem() {
}
} // 业务真正处理的地方
func (c *Controller) processNextItem() bool {
// 获取key
item, shutdown := c.queue.Get()
if shutdown {
return false
}
defer c.queue.Done(item) // 调用业务逻辑
err := c.syncService(item.(string))
if err != nil {
// 对错误进行处理
c.handlerError(item.(string), err)
return false
}
return true
} func (c *Controller) syncService(item string) error {
namespace, name, err := cache.SplitMetaNamespaceKey(item)
if err != nil {
return err
}
// 获取service
service, err := c.serviceLister.Services(namespace).Get(name)
if err != nil {
if errors.IsNotFound(err) {
return nil
}
return err
} // 新增和删除
_, ok := service.GetAnnotations()["ingress/http"]
ingress, err := c.ingressLister.Ingresses(namespace).Get(name)
if err != nil && !errors.IsNotFound(err) {
return err
} if ok && errors.IsNotFound(err) {
// 创建ingress
ig := c.constructIngress(service)
_, err := c.client.NetworkingV1().Ingresses(namespace).Create(context.TODO(), ig, metaV1.CreateOptions{})
if err != nil {
return err
}
} else if !ok && ingress != nil {
// 删除ingress
err := c.client.NetworkingV1().Ingresses(namespace).Delete(context.TODO(), name, metaV1.DeleteOptions{})
if err != nil {
return err
}
}
return nil
} func (c *Controller) handlerError(key string, err error) {
// 如果出现错误,重新加入队列,最大处理10次
if c.queue.NumRequeues(key) <= maxRetry {
c.queue.AddRateLimited(key)
return
}
runtime.HandleError(err)
c.queue.Forget(key)
} func (c *Controller) constructIngress(service *apiCoreV1.Service) *netV1.Ingress {
// 构造ingress
pathType := netV1.PathTypePrefix
ingress := netV1.Ingress{}
ingress.ObjectMeta.OwnerReferences = []metaV1.OwnerReference{
*metaV1.NewControllerRef(service, apiCoreV1.SchemeGroupVersion.WithKind("Service")),
}
ingress.Namespace = service.Namespace
ingress.Name = service.Name
ingress.Spec = netV1.IngressSpec{
Rules: []netV1.IngressRule{
{
Host: "example.com",
IngressRuleValue: netV1.IngressRuleValue{
HTTP: &netV1.HTTPIngressRuleValue{
Paths: []netV1.HTTPIngressPath{
{
Path: "/",
PathType: &pathType,
Backend: netV1.IngressBackend{
Service: &netV1.IngressServiceBackend{
Name: service.Name,
Port: netV1.ServiceBackendPort{
Number: 80,
},
},
},
},
},
},
},
},
},
} return &ingress
}

接下来我们来实现main:

package main

import (
"ingress-manager/pkg"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
) func main() {
// 获取config
// 先尝试从集群外部获取,获取不到则从集群内部获取
var config, err = clientcmd.BuildConfigFromFlags("", clientcmd.RecommendedHomeFile)
if err != nil {
clusterConfig, err := rest.InClusterConfig()
if err != nil {
panic(err)
}
config = clusterConfig
} // 通过config创建 clientSet
clientSet, err := kubernetes.NewForConfig(config)
if err != nil {
panic(err)
} // 通过 client 创建 informer,添加事件处理函数
factory := informers.NewSharedInformerFactory(clientSet, 0)
serviceInformer := factory.Core().V1().Services()
ingressInformer := factory.Networking().V1().Ingresses()
newController := pkg.NewController(clientSet, serviceInformer, ingressInformer) // 启动 informer
stopCh := make(chan struct{})
factory.Start(stopCh)
factory.WaitForCacheSync(stopCh)
newController.Run(stopCh)
}

测试

首先创建deploy和service:

apiVersion: apps/v1
kind: Deployment
metadata:
name: my-nginx
spec:
selector:
matchLabels:
app: my-nginx
template:
metadata:
labels:
app: my-nginx
spec:
containers:
- name: my-nginx
image: nginx:1.17.1
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: my-nginx
labels:
app: my-nginx
spec:
ports:
- port: 80
protocol: TCP
name: http
selector:
app: my-nginx

创建完成后进行查看:

$ kubectl get deploy,service,ingress
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/my-nginx 1/1 1 1 7m NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 78d
service/my-nginx ClusterIP 10.105.32.46 <none> 80/TCP 7m

上面的命令我分别获取deploy,service,ingress,但是只获取到了deployservice,这符合我们的预期。接着我们给service/m-nginx中的annotations添加ingress/http: nginx

$ kubectl edit service/my-nginx

apiVersion: v1
kind: Service
metadata:
annotations:
ingress/http: nginx
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"labels":{"app":"my-nginx"},"name":"my-nginx","namespace":"default"},"spec":{"ports":[{"name":"http","port":80,"protocol":"TCP"}],"selector":{"app":"my-nginx"}}}
...... service/my-nginx edited

重新进行查看:

$ kubectl get deploy,service,ingress
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/demo-deployment 1/1 1 1 41d
deployment.apps/my-nginx 1/1 1 1 11m NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 78d
service/my-nginx ClusterIP 10.105.32.46 <none> 80/TCP 11m NAME CLASS HOSTS ADDRESS PORTS AGE
ingress.networking.k8s.io/my-nginx <none> example.com 80 19s

接着我们再来测试下,将ingress/http: nginx注释掉,看看ingress是否会自动删除:

$ kubectl get deploy,service,ingress
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/demo-deployment 1/1 1 1 41d
deployment.apps/my-nginx 1/1 1 1 19m NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 78d
service/my-nginx ClusterIP 10.105.32.46 <none> 80/TCP 19m

我们发现和我们预期的效果一样。

如果service被删除了,ingress肯定也是不会存在的。这个这里就不多演示了。有兴趣可以自行测试下。

使用client-go实现自定义控制器的更多相关文章

  1. 使用jQuery.FileUpload和Backload自定义控制器上传多个文件

    当需要在控制器中处理除了文件的其他表单字段,执行控制器独有的业务逻辑......等等,这时候我们可以自定义控制器. 通过继承BackloadController □ 思路 BackloadContro ...

  2. 自定义控制器的View(loadView)及其注意点

    *:first-child { margin-top: 0 !important; } body > *:last-child { margin-bottom: 0 !important; } ...

  3. 1.自定义控制器切换<一>

    一.自定义控制器切换:在同一个控制器上,展示不同的控制器,类似于tabbar一样 二.怎么做?(问题解决步骤) 1.创建若干控制器:OneViewController TwoViewControlle ...

  4. beego 自定义控制器与路由

    框架浅析 这是之前使用bee创建的webapp目录层级结构: ├── conf 配置文件 │ └── app.conf ├── controllers 控制器 │ └── default.go ├── ...

  5. SAP CRM 自定义控制器与数据绑定

    当用户从视图离开时,视图将失去它的数据.解决这个问题,需要引入自定义控制器(Custom Controller)(译者注:SAP CRM自定义端中,不同地方的Custom Controller会翻译为 ...

  6. MVC文件上传06-使用客户端jQuery-File-Upload插件和服务端Backload组件自定义控制器上传多个文件

    当需要在控制器中处理除了文件的其他表单字段,执行控制器独有的业务逻辑......等等,这时候我们可以自定义控制器. MVC文件上传相关兄弟篇: MVC文件上传01-使用jquery异步上传并客户端验证 ...

  7. 不准使用xib自定义控制器view的大小

    1.AppDelegate.m // // 文 件 名:AppDelegate.m // // 版权所有:Copyright © 2018年 leLight. All rights reserved. ...

  8. Yii2 利用controllerMap自定义控制器类

    版权声明:本文为博主原创文章,未经博主允许不得转载. Yii2框架为我们自定义好的  controllers,Models,views,标准的MVC结构框架,但是有些时候我们写接口希望结构更加清晰而不 ...

  9. iOS开发之自定义控制器切换

    iOS8以后, 苹果公司推出了UIPresentationController, 通过其(presentedController 和 presentingController)来控制modal控制器操 ...

随机推荐

  1. mybatis-数据库类型的对应关系

  2. 说出 5 个 JDK 1.8 引入的新特性?

    Java 8 在 Java 历史上是一个开创新的版本,下面 JDK 8 中 5 个主要的特性: Lambda 表达式,允许像对象一样传递匿名函数 Stream API,充分利用现代多核 CPU,可以写 ...

  3. 全方位讲解 Nebula Graph 索引原理和使用

    本文首发于 Nebula Graph Community 公众号 index not found?找不到索引?为什么我要创建 Nebula Graph 索引?什么时候要用到 Nebula Graph ...

  4. ros工作空间中文件夹结构

    ROS 编译系统 catkin 详解 ros系统学习之Catkin编译系统 ROS--catkin编译系统.package.xml和CMakeList.txt文件 1.build:编译空间 存放CMa ...

  5. 【HTML5版】导出Table数据并保存为Excel

    首发我的博客 http://blog.meathill.com/tech/js/export-table-data-into-a-excel-file.html 最近接到这么个需求,要把<tab ...

  6. 《CSS 揭秘》作者Lea Verou:我喜欢分享开源的行业文化

    本文仅用于学习和交流,不用于商业目的.非商业转载请注明作译者.出处,并保留本文的原始链接:http://www.ituring.com.cn/art... 访谈嘉宾: Lea VerouW3C CSS ...

  7. 可想实现一个自己的简单jQuery库?(九)

    Lesson-8 事件机制 在讲事件机制之前呢,我们有一个很重要的东西要先讲,那就是如何实现事件委托(代理). 只有必须先明白了如何实现一个事件委托,我们才能更好的去实现on和off.在我看来,on和 ...

  8. Chrome 已经原生支持截图功能,还可以给节点截图!

    昨天 Chrome62 稳定版释出,除了常规修复各种安全问题外,还增加很多功能上的支持,比如说今天要介绍的强大的截图功能. 直接截图 打开开发者工具页面,选择左上角的元素选择按钮(Inspect) W ...

  9. C#枚举-通过值获取名字,通过名称获取值

    public enum ProtoType { Move = 1, Enter = 2, Leave = 3, Attack, Die, } print("ProtoType.Move:&q ...

  10. linux-RHEL7.0 —— 《Linux就该这么学》阅读笔记

    目录 linux-RHEL7.0 安装部署 修改root密码 RPM(红帽软件包管理器) YUM(软件仓库) Systemd初始化进程 总结 linux命令 帮助命令 man 系统工作命令 echo ...