Fabric8 Kubernetes 和 Openshift Java DSL

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

Fabric8 v2 的第一个版本一直在使用基于 JAX-RS 的 Kubernetes 客户端,该客户端使用 Apache CXF 。客户端很棒,但我们一直想提供更薄、依赖更少的东西 (以便更容易采用) 。我们还想给它一个 fecelift 并围绕它构建一个 DSL,以便它变得更易于使用和阅读。

新客户端目前位于:https: //github.com/fabric8io/kubernetes-client ,它提供以下模块:

  1. Kubernetes 客户端。
  2. 一个 Openshift 客户端。
  3. 以上所有的模拟框架(基于 EasyMock

初见客户

让我们快速浏览一下如何使用客户端创建、列出和删除内容:


 //Instantiate the client
        KubernetesClient client = new DefaultKubernetesClient();
    //Create a service
    Service myservice = ...;
    client.services().inNamespace("fabric8").create(myservice);
    
    //Create a service inline
    Service jenkins = client.services().inNamespace("fabric8").createNew()
            .withNewMetadata()
                .withName("jenkins")
                .addToLabels("component", "jenkins")
            .endMetadata()
            .done();

    //List services
    ServiceList serviceList = client.services().inNamespace("fabric8").list();
    
    //Watch services
    client.services().inNamespace("fabric8").watch(new Watcher<Service>() {
            @Override
            public void eventReceived(Action action, Service resource) {
              logger.info("{}: {}", action, resource);
            }
    });

    //Delete by label
    Boolean deleted = client.services().withLabel("component", "jenkins").delete();
    
    //Close client
    client.close();

上面的片段几乎是不言自明的 (这就是使用 DSL 的好处), 但我还有一篇博文要填写,所以我会提供尽可能多的细节。

客户端域模型

您可以将客户端视为两件事的结合:

  1. Kubernetes 域模型。
  2. 围绕模型的 DSL。

域模型是一组对象,代表在客户端和 Kubernetes / Openshift 之间交换的数据。数据的原始格式是 JSON。这些 JSON 对象非常复杂且结构非常严格,因此手工制作它们并非易事。

我们需要有一种方法在 Java 中操作这些 JSON 对象 (并能够利用代码完成等), 但也尽可能接近原始格式。使用 JSON 对象的 POJO 表示可用于操作,但它不太像 JSON,而且对于具有深层嵌套的 JSON 也不是真正有用的。因此,我们决定在那些使用与原始 JSON 完全相同的结构的 POJO 之上生成流畅的构建器。

例如,这里是 Kubernetes 服务的 JSON 对象:


 //Instantiate the client
        KubernetesClient client = new DefaultKubernetesClient();
    //Create a service
    Service myservice = ...;
    client.services().inNamespace("fabric8").create(myservice);
    
    //Create a service inline
    Service jenkins = client.services().inNamespace("fabric8").createNew()
            .withNewMetadata()
                .withName("jenkins")
                .addToLabels("component", "jenkins")
            .endMetadata()
            .done();

    //List services
    ServiceList serviceList = client.services().inNamespace("fabric8").list();
    
    //Watch services
    client.services().inNamespace("fabric8").watch(new Watcher<Service>() {
            @Override
            public void eventReceived(Action action, Service resource) {
              logger.info("{}: {}", action, resource);
            }
    });

    //Delete by label
    Boolean deleted = client.services().withLabel("component", "jenkins").delete();
    
    //Close client
    client.close();

使用 Fluent Builders 的 Java 等价物可能是:


 //Instantiate the client
        KubernetesClient client = new DefaultKubernetesClient();
    //Create a service
    Service myservice = ...;
    client.services().inNamespace("fabric8").create(myservice);
    
    //Create a service inline
    Service jenkins = client.services().inNamespace("fabric8").createNew()
            .withNewMetadata()
                .withName("jenkins")
                .addToLabels("component", "jenkins")
            .endMetadata()
            .done();

    //List services
    ServiceList serviceList = client.services().inNamespace("fabric8").list();
    
    //Watch services
    client.services().inNamespace("fabric8").watch(new Watcher<Service>() {
            @Override
            public void eventReceived(Action action, Service resource) {
              logger.info("{}: {}", action, resource);
            }
    });

    //Delete by label
    Boolean deleted = client.services().withLabel("component", "jenkins").delete();
    
    //Close client
    client.close();

域模型存在于它自己的项目中: Fabric8 的 Kubernetes 模型 。该模型是经过一个漫长的过程从 Kubernetes Openshift 代码生成的:

  1. Go 源码转换 JSON schema
  2. JSON 模式转换 POJO
  3. 一代流利的建设者

Fluent builder 是由一个名为 sundrio 的小项目生成的,我将在以后的帖子中介绍它。

获取客户端实例

获取 默认客户端 实例的实例非常简单,因为提供了一个空的构造函数。当使用空构造函数时,客户端将使用默认设置:

  • Kubernetes 网址
    1. 系统属性“ kubernetes.master
    2. 环境变量“ KUBERNETES_MASTER
    3. 来自用户主目录中的“ .kube/config ”文件。
    4. 使用 DNS:“ https://kubernetes.default.svc
  • 服务帐户路径“ /var/run/secrets/kubernetes.io/serviceaccount/

可以通过传递 Config 对象的实例来提供更细粒度的配置。


 //Instantiate the client
        KubernetesClient client = new DefaultKubernetesClient();
    //Create a service
    Service myservice = ...;
    client.services().inNamespace("fabric8").create(myservice);
    
    //Create a service inline
    Service jenkins = client.services().inNamespace("fabric8").createNew()
            .withNewMetadata()
                .withName("jenkins")
                .addToLabels("component", "jenkins")
            .endMetadata()
            .done();

    //List services
    ServiceList serviceList = client.services().inNamespace("fabric8").list();
    
    //Watch services
    client.services().inNamespace("fabric8").watch(new Watcher<Service>() {
            @Override
            public void eventReceived(Action action, Service resource) {
              logger.info("{}: {}", action, resource);
            }
    });

    //Delete by label
    Boolean deleted = client.services().withLabel("component", "jenkins").delete();
    
    //Close client
    client.close();

客户端扩展和适配器

为了支持 Kubernetes 扩展(例如 Openshift ),客户端使用 Extension Adapter 的概念。这个想法很简单。扩展客户端扩展 默认客户端 并实现 扩展 。只要能通过Java的 ServiceLoader 找到一个 Adapter ,每个client实例都可以适配 Extension (原谅我爹)。

下面是一个示例,说明如何使客户端的任何实例适应 OpenshiftClient 的实例:


 //Instantiate the client
        KubernetesClient client = new DefaultKubernetesClient();
    //Create a service
    Service myservice = ...;
    client.services().inNamespace("fabric8").create(myservice);
    
    //Create a service inline
    Service jenkins = client.services().inNamespace("fabric8").createNew()
            .withNewMetadata()
                .withName("jenkins")
                .addToLabels("component", "jenkins")
            .endMetadata()
            .done();

    //List services
    ServiceList serviceList = client.services().inNamespace("fabric8").list();
    
    //Watch services
    client.services().inNamespace("fabric8").watch(new Watcher<Service>() {
            @Override
            public void eventReceived(Action action, Service resource) {
              logger.info("{}: {}", action, resource);
            }
    });

    //Delete by label
    Boolean deleted = client.services().withLabel("component", "jenkins").delete();
    
    //Close client
    client.close();

只有当 /oapi 存在于 Kubernetes 客户端返回的根路径列表中时,上面的代码才有效(即客户端指向一个开放式轮班安装)。如果不是,它将抛出 IllegalArugementException。

如果用户正在编写绑定到 Openshift 的代码,他总是可以直接实例化 默认 openshift 客户端 的实例。


 //Instantiate the client
        KubernetesClient client = new DefaultKubernetesClient();
    //Create a service
    Service myservice = ...;
    client.services().inNamespace("fabric8").create(myservice);
    
    //Create a service inline
    Service jenkins = client.services().inNamespace("fabric8").createNew()
            .withNewMetadata()
                .withName("jenkins")
                .addToLabels("component", "jenkins")
            .endMetadata()
            .done();

    //List services
    ServiceList serviceList = client.services().inNamespace("fabric8").list();
    
    //Watch services
    client.services().inNamespace("fabric8").watch(new Watcher<Service>() {
            @Override
            public void eventReceived(Action action, Service resource) {
              logger.info("{}: {}", action, resource);
            }
    });

    //Delete by label
    Boolean deleted = client.services().withLabel("component", "jenkins").delete();
    
    //Close client
    client.close();

测试和模拟

模拟正在与外部系统对话的客户端是一种很常见的情况。当客户端是扁平的 (不支持方法链) 时,模拟是微不足道的,并且有大量的框架可以用于这项工作。但是,当使用 DSL 时,事情变得更加复杂,需要大量样板代码将各个部分连接在一起。如果原因不明显,我们就说使用模拟,您定义每次方法调用的模拟行为。与等效的 Flat 对象相比,DSL 往往具有更多的方法(具有更少的参数)。仅此一项就增加了定义行为所需的工作。此外,这些方法通过返回中间对象链接在一起,这意味着它们也需要被模拟,这进一步增加了工作量和复杂性。

为了删除所有样板文件并使模拟客户端变得非常简单,我们将客户端的 DSL 与模拟框架的 DSL 相结合: EasyMock 。这意味着此 DSL 的入口点是 Kubernetes 客户端 DSL 本身,但终端方法已被修改,以便它们返回“Expectation Setters”。一个例子应该使这更容易理解。


 //Instantiate the client
        KubernetesClient client = new DefaultKubernetesClient();
    //Create a service
    Service myservice = ...;
    client.services().inNamespace("fabric8").create(myservice);
    
    //Create a service inline
    Service jenkins = client.services().inNamespace("fabric8").createNew()
            .withNewMetadata()
                .withName("jenkins")
                .addToLabels("component", "jenkins")
            .endMetadata()
            .done();

    //List services
    ServiceList serviceList = client.services().inNamespace("fabric8").list();
    
    //Watch services
    client.services().inNamespace("fabric8").watch(new Watcher<Service>() {
            @Override
            public void eventReceived(Action action, Service resource) {
              logger.info("{}: {}", action, resource);
            }
    });

    //Delete by label
    Boolean deleted = client.services().withLabel("component", "jenkins").delete();
    
    //Close client
    client.close();

模拟框架可以轻松地与其他 Fabric8 组件结合使用,例如 CDI 扩展 。您只需要创建返回模拟的 @Produces 方法。