概述

每当《CNI, From A Developer’s
Perspective》一平和遇到,我们就针对性CNI有了较为深切之摸底。我们领会,容器网络成效的实现最后是透过CNI插件来好的。每个CNI插件本质上虽是一个可执行文件,而CNI的实施流程无非就是自容器管理连串与部署文件获取配置信息,然后用那个消息为环境变量和规范输入的款式传输给插件,再运行插件完成具体的器皿网络部署,最后用部署结果经注明输出重回。

以我们本着CNI的各样插件做了一个先河的浏览之后,我们会发现,尽管各样CNI插件实现容器网络的艺术是各式各个的,不过其编写的覆辙基本是同一的。其中必会设有三单函数:main(),cmdAdd(),cmdDel()。接着我们回顾一下《CNI,
From A Developer’s
Perspective》一温情境遇的讲述,CNI其实就唯有区区个基本操作ADD和DEL,前者用于在容器网络,后者用于从容器网络被剥离。由此,通过上述两只函数,再加上有些靠边之联想,大家啊即一蹴而就勾勒出插件的尽流程了。当CNI插件被调用时,首先登main函数,main函数会对环境变量和专业输入被之布局音讯举行解析,接着冲分析得到的操作方法(ADD或DEL),转入具体的施行函数完成网络的配备工作。如即便ADD操作,则调用cmdAdd()函数,反之,假设是DEL操作,则调用cmdDel()函数。从总角度来拘禁,CNI插件的落实框架是不怕是这般简单清晰。下边咱们不怕因CNI官方插件库底bridge插件为例,深远上述三独函数的源码,来更为求证CNI插件应什么促成的。

(bridge插件源码链接:https://github.com/containernetworking/plugins/tree/master/plugins/main/bridge)

 

main函数

1、main函数相当简单,仅仅只是调用了skel.PluginMain这些函数,并且以函数cmdAdd和cmdDel以及帮忙插件襄助之CNI版本作为参数传递给它们。

func main() {
    skel.PluginMain(cmdAdd, cmdDel, version.All)
}

  

2、PluginMain函数是一个封装函数,它一向指向PluginMainWithError举行调用,当起错误发生的时,会用左为json的花样出口到标准输出,并退出插件的行。

func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo) {
    if e := PluginMainWithError(cmdAdd, cmdDel, versionInfo); e != nil {
        if err := e.Print(); err != nil {
            log.Print("Error writing error JSON to stdout: ", err)
        }
        os.Exit(1)
    }
}

  

3、PluginMainWithError函数也异常简单,其实即便是用环境变量,标准输入输出构造了一个dispatcher结构,再实践中的pluginMain方法而已。

func PluginMainWithError(cmdAdd, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo) *types.Error {
    return (&dispatcher{
        Getenv: os.Getenv,
        Stdin:  os.Stdin,
        Stdout: os.Stdout,
        Stderr: os.Stderr,
    }).pluginMain(cmdAdd, cmdDel, versionInfo)
}

  

dispatcher结构如下所示:

type dispatcher struct {
    Getenv func(string) string
    Stdin  io.Reader
    Stdout io.Writer
    Stderr io.Writer

    ConfVersionDecoder version.ConfigDecoder
    VersionReconciler  version.Reconciler
}

  

4、接着dispatcher结构的pluginMain方法执行实际的操作。该函数的操作分为如下两步:

  • 首先调用cmd, cmdArgs, err :=
    t.getCmdArgsFromEnv()从环境变量和业内输入被剖析出操作音讯cmd和部署信息cmdArgs
  • 随之冲操作新闻cmd的两样,调用checkVersionAndCall(),该函数会首先由正式输入被得配置信息被的CNI版本,再与事先main函数中指定的插件协理之CNI版本音信举行相比较对。假如版本匹配,则调用相应的回调函数cmdAdd或cmdDel并盖cmdArgs作为参数,否则,再次来到错误

    func (t dispatcher) pluginMain(cmdAdd, cmdDel func(_ CmdArgs) error, versionInfo version.PluginInfo) *types.Error {

    cmd, cmdArgs, err := t.getCmdArgsFromEnv()
        .....
    switch cmd {
    case "ADD":
        err = t.checkVersionAndCall(cmdArgs, versionInfo, cmdAdd)
    case "DEL":
                ......
    }
        ......
    

    }

  

5、下边大家来探dispatcher的getCmdArgsFromEnv()方法是什么样由环境变量和业内输入被得配置消息的。首先来拘禁一下cmdArgs的现实协会:

type CmdArgs struct {
    ContainerID string
    Netns       string
    IfName      string
    Args        string
    Path        string
    StdinData   []byte
}

  

解析了上述组织下大家可以发现CmdArgs中的情节与《CNI, From A Developer’s
Perspective》中讲述的从容器管理体系面临得之周转时系统基本是一样的,而都领悟这个参数是通过环境变量传递给插件的。由此,不难想象,getCmdArgsFromEnv()所做的办事就是从环境变量中提议布局音讯用于填充CmdArgs,再将容器网络的配置信息,也不怕是正规输入被之始末,存入StdinData字段。具体代码如下所示:

func (t *dispatcher) getCmdArgsFromEnv() (string, *CmdArgs, error) {
    var cmd, contID, netns, ifName, args, path string

    vars := []struct {
        name      string
        val       *string
        reqForCmd reqForCmdEntry
    }{
        {
            "CNI_COMMAND",
            &cmd,
            reqForCmdEntry{
                "ADD": true,
                "DEL": true,
            },
        },
                ....
        {
            "CNI_NETNS",
            &netns,
            reqForCmdEntry{
                "ADD": true,
                "DEL": false,
            },
        },
                ....
    }

    argsMissing := false
    for _, v := range vars {
        *v.val = t.Getenv(v.name)
        if *v.val == "" {
            if v.reqForCmd[cmd] || v.name == "CNI_COMMAND" {
                fmt.Fprintf(t.Stderr, "%v env variable missing\n", v.name)
                argsMissing = true
            }
        }
    }

    if argsMissing {
        return "", nil, fmt.Errorf("required env variables missing")
    }

    stdinData, err := ioutil.ReadAll(t.Stdin)
    if err != nil {
        return "", nil, fmt.Errorf("error reading from stdin: %v", err)
    }

    cmdArgs := &CmdArgs{
        ContainerID: contID,
        Netns:       netns,
        IfName:      ifName,
        Args:        args,
        Path:        path,
        StdinData:   stdinData,
    }
    return cmd, cmdArgs, nil
}

  

虽说getCmdArgsFromEnv()要水到渠成的行事异常简单,但细心分析代码之后,大家可窥见它们的实现充裕细。首先,它定义了一致雨后春笋回想要取的参数,例如cmd,contID,netns等等。之后再一次定义了一个匿名结构的数组,匿名结构被蕴藏了环境变量的名字,一个字符串指针(把欠环境变量对应的参数与给它们,例如cmd对许CNI_COMMAND)以及一个reqForCmdEntry类型的分子reqForCmd。类型reqForCmdEntry其实是一个map,它于此的用意是概念该环境变量是否为相应操作所不可不的。例如,上文中的环境变量”CNI_NETNS”,对于”ADD”操作为true,而对”DEL”操作则也false,这讲明以”ADD”操作时,该环境变量不能也空,否则会报错,可是在”DEL”操作时虽无所谓。最后,遍历该数组举办参数的取即可。

暨是截止,main函数的职责就。总的来说它举办了三桩业务:1、CNI版本检查,2、提取配置参数构建cmdArgs,3、调用对应的回调函数,cmdAdd或者cmdDel。

 

cmdAdd函数

1、如下所示cmdAdd函数一般分为三单步骤执行:

  • 先是调用函数conf, err :=
    loadNetConf(args.StdinData)(注:loadNetConf是插件自定义的,各类插件都非同等),从标准输入,也便是参数args.StdinData中取得容器网络布局信息
  • 随后冲现实的布置音讯举办网络的配置工作
  • 最后,调用函数types.PrintResult(result, conf.CNIVersion)输出配置结果

    func cmdAdd(args *skel.CmdArgs) error {

    n, cniVersion, err := loadNetConf(args.StdinData)
        ......
        return PrintResult(result, cniVersion)
    

    }

  

2、接着我们针对loadNetConf函数举办辨析。因为每个CNI插件配置容器网络的主意各有不同,因而它们所急需的布信息一般为是殊的,除了我们共有的消息为含有在types.NetConf结构面临,每个插件还定义了好所用的字段。例如,对于bridge插件,它用于存储配置音信之构造如下所示:

type NetConf struct {
    types.NetConf
    BrName       string `json:"bridge"`
    IsGW         bool   `json:"isGateway"`
    IsDefaultGW  bool   `json:"isDefaultGateway"`
    ForceAddress bool   `json:"forceAddress"`
    IPMasq       bool   `json:"ipMasq"`
    MTU          int    `json:"mtu"`
    HairpinMode  bool   `json:"hairpinMode"`
    PromiscMode  bool   `json:"promiscMode"`
}

  

假定loadNetConf函数所召开的操作也非凡简单,就是调用json.Unmarshal(bytes,
n)函数将配备音讯于业内输入的字节流中解码到一个NetConf结构,具体代码如下:

func loadNetConf(bytes []byte) (*NetConf, string, error) {
    n := &NetConf{
        BrName: defaultBrName,
    }
    if err := json.Unmarshal(bytes, n); err != nil {
        return nil, "", fmt.Errorf("failed to load netconf: %v", err)
    }
    return n, n.CNIVersion, nil
}

  

3、最终,我们对部署结果的出口进行辨析。由于不同的CNI版本要求的输出结果的始末是勿极端一致的,由此就有的情其实是相比复杂的。下边我们固然进去PrintResult函数一钻探竟。

func PrintResult(result Result, version string) error {
    newResult, err := result.GetAsVersion(version)
    if err != nil {
        return err
    }
    return newResult.Print()
}

  

自地点的代码中我们得望,该函数就做了零星桩事,一桩是调用newResult, err
:=
result.GetAsVersion(version),按照指定的版本音信,举办结果音信的版转换。第二码就是调用newResult.Print()将结果信息输出到正规输出。

其实,Result如下所示,是一个interface类型。每个版本的CNI都是概念了自己的Result结构的,而那一个社团都是满意Result接口的。

// Result is an interface that provides the result of plugin execution
type Result interface {
    // The highest CNI specification result verison the result supports
    // without having to convert
    Version() string

    // Returns the result converted into the requested CNI specification
    // result version, or an error if conversion failed
    GetAsVersion(version string) (Result, error)

    // Prints the result in JSON format to stdout
    Print() error

    // Returns a JSON string representation of the result
    String() string
}

  

若其中的GetAsVersion()方法则用来将眼前本的CNI
Result信息转化到相应的CNI
Result音信。我们来推举个实际的例子,应该就是非常明显了。

func (r *Result) GetAsVersion(version string) (types.Result, error) {
    switch version {
    case "0.3.0", ImplementedSpecVersion:
        r.CNIVersion = version
        return r, nil
    case types020.SupportedVersions[0], types020.SupportedVersions[1], types020.SupportedVersions[2]:
        return r.convertTo020()
    }
    return nil, fmt.Errorf("cannot convert version 0.3.x to %q", version)
}

  

一旦现在大家的result的版0.3.0,
然而插件要求再次来到的result版本是0.2.0之,依照达文中的代码,显著那我们会调用r.convertTo020()函数进行换,如下所示:

// Convert to the older 0.2.0 CNI spec Result type
func (r *Result) convertTo020() (*types020.Result, error) {
    oldResult := &types020.Result{
        CNIVersion: types020.ImplementedSpecVersion,
        DNS:        r.DNS,
    }

    for _, ip := range r.IPs {
        // Only convert the first IP address of each version as 0.2.0
        // and earlier cannot handle multiple IP addresses
               ......
    }

    for _, route := range r.Routes {
               ......
    }
        ......
    return oldResult, nil
}

  

该函数所开的操作,一句话来说,就是概念了相应版本具体的Result结构,然后据此时本的Result结构面临的音讯举行填充,从而做到Result版本的转会。

万一Print方法对各个版本的Result都是一样的,都是用Result举办json编码后,输出到标准输出而已。

至这为止,cmdAdd函数操作就。

 

cmdDel函数

cmdDel和cmdAdd的执行社团是近似的,而且貌似比cmdAdd还略有。同样,cmdDel先由args.Stdin中收获网络的布置新闻,接着再开展对应的清理工作。最终,与cmdAdd不同之凡,cmdDel不需要针对结果开展输出,直接回错误音信即可。

盖cmdDel和cmdAdd从构造层面来拘禁是类似的,因而即使不再赘述了。

 

结语

上文对CNI插件的履行框架实行了相比深刻的剖析。总的来说,一般插件的实施就是三组成部分内容:1、解析配置音讯,2、执行实际的网络布局ADD或DEL,3、对于ADD操作还欲出口结果。全部来说,架构仍旧要命简洁清晰的。

如若您生出任何新的器皿网络方案,希望通过本文的读能够为您速地修出相应的CNI插件。

相关文章

网站地图xml地图