使用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. su 和 sudo的区别

    su是一个命令,可切换其他用户进行操作:而 '-' 号则是代表是否完全切换指定的用户环境信息 sudo是一个服务,可通过/etc/sudoers进行配置文件,让被限制的用户只能执行被授予的命令操作.

  2. 为什么需要域驱动设计DDD?

    我们需要 DDD 的因素 – 微服务面试问题

  3. 移动端H5页面中1px边框的几种解决方法

    问题提出 这是一个比较老的问题了,我第一次注意到的时候,是UI设计师来找我麻烦,emmm那时候我才初入前端职场,啥也不懂啊啊啊啊啊,情形是这样的:设计师拿着手机过来:这些边框都粗了啊,我的设计稿上是1 ...

  4. 2022DASCTF X SU 三月春季挑战赛 Calc

    查看代码 #coding=utf-8 from flask import Flask,render_template,url_for,render_template_string,redirect,r ...

  5. .Net Core:Docker无法拉取mcr.microsoft.com相关镜像解决办法

    今天在教同事Docker简单部署Asp.Net Core项目,pull镜像时突然出现下图中的错误: 因为微软在 2018 年五月之后,只会将相关镜像打包发布到 MCR 上.但是 MCR 对国内用户不太 ...

  6. Java中使用最频繁及最通用的Java工具类

    在Java中,工具类定义了一组公共方法,Java中使用最频繁及最通用的Java工具类. 一. org.apache.commons.io.IOUtils closeQuietly:关闭一个IO流.so ...

  7. JavaScript脚本延迟加载的方式有哪些?

    延迟加载就是等页面加载完成之后再加载 JavaScript 文件. js 延迟加载有助于提高页面加载速度. 一般有以下几种方式: defer 属性: 给 js 脚本添加 defer 属性,这个属性会让 ...

  8. Python入门-程序测试

    1.功能测试 常规测试 #常规测试代码,一个模块写功能,一个模块调用功能 #=============模块1:gongneng_ceshi def func(v1, v2): return v1* v ...

  9. mpvue使用scss

    安装scss 安装命令如下,不带版本号可能会导致报错 npm i sass-loader@7.3.1 -D npm i node-sass@4.14.1 -D 然后修改 build 文件夹下的 web ...

  10. SwitchHosts管理编辑hosts工具

    管理Hosts工具 SwitchHosts 地址: SwitchHosts 开发工程中,针对不同项目设置不同的域名. 办法很多,例如直接编辑hosts文件,通过环境工具提供的功能设置等. 现在要安利一 ...