分享
  1. 首页
  2. 文章

Go 编程: 从 K8S 资源定义到 YAML 配置框架的实现

JayL · · 1246 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

原文地址:Go 编程: 从 K8S 资源定义到 YAML 配置框架的实现

在 Go 语言中,实现 YAML 配置定义与解析是非常简单的, 以最新YAML 包 gopkg.in/yaml.v3 为例:


import "gopkg.in/yaml.v3"
type StructA struct {
 A string `yaml:"a"`
}
type StructB struct {
 StructA `yaml:",inline"`
 B string `yaml:"b"`
}
var b StructB
err := yaml.Unmarshal([]byte(data), &b)

非常直白,貌似不值一谈。再看看现实, YAML 文件用途之广泛,特别是在Kubernetes 平台上的应用,简直就是不可或缺。几乎所有 Kubernetes 资源都有相应的 YAML 格式定义, 少说也有几十种,这些资源在 Kubernetes 中是如何定义并被解析处理的呢?

何为配置框架

如果这几十种资源全都按篇头的例子进行独立定义是不可想象的。所以,在 Kubernetes 的资源定义上设计了一套基础元数据定义框架。不妨就就简单的 POD 资源定义做个例子分析一下:

apiVersion: v1
kind: Pod
metadata:
 name: myapp-pod
 labels:
 app: myapp
spec:
 containers:
 - name: myapp-container
 image: busybox
 command: ['sh', '-c', 'echo Hello Kubernetes! && sleep 3600']

配置定义中:apiVersionkindmetadataspec这四项一级字段,就是我所说的元数据属性字段。在 Kubernetes 中,所有的资源对象都必须遵守这个基础框架格式。至于具体的资源定义,则通过kind进行区分,通过 spec进行具体定义。

这样定义有什么好处呢?

首先,从配置定义层面看,这种定义方式直接将具体资源配置抽象成通用资源配置,这样实际处理资源配置的过程就可以框架化。什么是框架化,请参考以前的文章.这里简单贴一下框架原理图。

framework.png

框架的好处之一,就是代码复用。同样,对于配置文件,如果配置本身遵循同一套基础框架时,那么,配置文件的处理过程就可以实现框架化,即通过相同的处理流程进行处理,也就实现了框架特性,代码复用

将不同的配置设计成遵循同一套基础框架还有另外一个好处,即延迟实现。如果所有的 Kubernetes 资源都必须在发布时就预先定义好资源属性,想想CRD(用户自定义资源)岂不是束手无策。

YAML 配置框架的实现

原理讲完了,现在我们来简单模拟一下 Kubernetes 资源配置框架的实现。简单定义一个叫做command的资源配置:

apiVersion: v1
kind: command
name: hello #简化 metadata 成简单的 name 
spec:
 command: 
 - echo
 - hello

用最简单的 Go 代码实现以上面的资源的解析过程:

// 基础资源配置框架结构定义
type Resource struct{
 ApiVersion string `yaml:"apiVersion"`
 Kind string `yaml:"kind"`
 Name string `yaml:"name"`
 Spec Command `yaml:"spec"`
}
// command 配置定义
type Command struct {
 Argments []string `yaml:"command"` 
}
// 解析 Resource
var resource Resource
err := yaml.Unmarshal([]byte(data), &resource)

实现配置框架的解析是非常容易的,只需要定义出基础配置项即可。这是已知情况。现在问题是,Kind 类型不仅仅是 command, 还有其它的类型。比如,增加一个 database 的配置资源定义:

apiVersion: v1
kind: database
name: dbconfig #简化 metadata 成简单的 name 
spec:
 host: "127.0.0.1"
 port: "3306"
 database: "demo"
 username: "demo"
 password: "123456"

那么现在,如何通过使用同一套代码实现两个完全不同资源的解析呢?

首先,当然还是要增加 Database 的类型定义,这步少不了。如下:

// database 配置
type Database struct {
 Host string `yaml:"host"`
 Port string `yaml:"port"`
 Database string `yaml:"database"`
 Username string `yaml:"username"`
 Password string `yaml:"password"`
}

有了CommandDatabase的定义,现在的问题是,如何让Resource同时支持这两种类型,当然,可能还会有更多种类型。做为一名资深 Go 程序员,第一反应一定是使用接口。不错!但是如何在配置框架中使用接口呢?

不妨回头看看,我前面讲到的,延迟实现。在解析配置时,对于不确定的配置节点,就可以采取延迟实现的方式来处理。在最新gopkg.in/yaml.v3包中,对于延后处理的节点可以定义成yaml.Node类型。那么,现在 Resource 定义就可以更新成这样:

// 基础资源配置框架结构定义
type Resource struct{
 ApiVersion string `yaml:"apiVersion"`
 Kind string `yaml:"kind"`
 Name string `yaml:"name"`
 //延迟实现解析
 Spec yaml.Node `yaml:"spec"`
}

那么现在即使完全不知道CommandDatabase的定义我们也可以开始解析Resource了。写一个简单文件解析函数的实现:

//伪代码,谨慎使用
func Parse(filename string) ([]*Resource, error) {
 fd, err := os.Open(filename)
 if err != nil {
 return nil, err
 }
 rs := []*Resource{}
 dec := yaml.NewDecoder(fd)
 for {
 r := Resource{}
 err := dec.Decode(&r)
 if err != nil {
 if err == io.EOF {
 break
 }
 return err
 }
 rs = append(rs, r)
 }
 return rs, nil
}

如果写到这就完了,那前面我所说的什么框架啊、配置框架也就白瞎了。既然要写配置框架,当然处理框架配置以外,还需要框架处理流程。我们还有很关键的事没有完成。不妨,先看看在 Kubernetes 平台上,配置资源是如何流转的。

k8s-config-flow.png

在 Kubernetes 平台上, YAML 格式的资源配置主要过程是:1、 2 两个过程。这两步的关键实现就在 ApiServer 上。当然,这里并不是要将 ApiServer 摊开来一步一步的做源码分析。我们只需要知道1、 2 两个过程就可以了,即在 ApiServer 内部实现上,所有的资源定义配置经过 ApiServer 时,第一步肯定是解析配置,然后就是第二步处理配置

同理,回到我们自己的例子中来。不论是 command 还是 database, 解析完成后,后面一定还有一个后续操作,至于具体什么操作不重要,关键是要有,哪怕是一个空操作。所以,我们就可以定义这样一个操作执行接口:

type Executor func(context.Context) error

除了这个执行接口以外,其实我们还少一个关键步骤,即规范Kind资源类型。如何规范Kind资源类型呢,答案当然是接口。Kind 资源如何在配置框架中实现处理,这个处理过程我们就可以抽象成下面的接口, 即将延迟处理的 yaml.Node 解析成目标执行Executor接口. 这就是不同 Kind 类型资源,必须实现的接口。

//Kind Spec Interface
type KindSpec interface{
 Decode(node *yaml.Node) (Executor, error)
}

有了这样一个 KindSpec 的接口类型定义,我们就可以将不同的 Kind 类型资源通过注册的方式注入到配置框架中,写一个简单的配置框架代码类供大家参考:

//伪代码,谨慎使用
type ConfigFramework struct{
 kinds map[string]KindSpec
}
func New() *ConfigFramework{
 return &ConfigFramework{kinds: make(map[string]KindSpec)}
}
//注册 Kind 类型资源
func (cf *ConfigFramework) Register(kind string, spec KindSpec) {
 cf.kinds[kind] = spec
}
func (cf *ConfigFramework) Execute(ctx context.Context, filename string) error {
 fd, err := os.Open(filename)
 if err != nil {
 return err
 }
 dec := yaml.NewDecoder(fd)
 for {
 r := Resource{}
 err := dec.Decode(&r)
 if err != nil {
 if err == io.EOF {
 break
 }
 return err
 }
 if spec, ok := cf.kinds[r.Kind]; ok {
 if exec, err := spec.Decode(r.Spec); err != nil {
 return err
 }
 if exec != nil {
 if err := exec.Execute(ctx); err != nil {
 return err
 }
 }
 }
 }
 return nil
}

以上,就完成了一个简单配置框架,即将配置的解析与处理过程框架化。


有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
1246 次点击
被以下专栏收入,发现更多相似内容
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏