CNCF CNI プラグインでCNIのサンプルコードをBashで用意したが、実際にCNIプラグインを書き始めたいという人でbashスクリプトで書く人はいないと思うのでGoのサンプルコードも用意した。
公式のサンプルコード
CNI公式のリポジトリpluginsの中にsampleコードがある。
package main
import (
"encoding/json"
"fmt"
"net"
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/types/current"
"github.com/containernetworking/cni/pkg/version"
)
// PluginConf is whatever you expect your configuration json to be. This is whatever
// is passed in on stdin. Your plugin may wish to expose its functionality via
// runtime args, see CONVENTIONS.md in the CNI spec.
type PluginConf struct {
types.NetConf // You may wish to not nest this type
RuntimeConfig *struct {
SampleConfig map[string]interface{} `json:"sample"`
} `json:"runtimeConfig"`
// This is the previous result, when called in the context of a chained
// plugin. Because this plugin supports multiple versions, we'll have to
// parse this in two passes. If your plugin is not chained, this can be
// removed (though you may wish to error if a non-chainable plugin is
// chained.
// If you need to modify the result before returning it, you will need
// to actually convert it to a concrete versioned struct.
RawPrevResult *map[string]interface{} `json:"prevResult"`
PrevResult *current.Result `json:"-"`
// Add plugin-specifc flags here
MyAwesomeFlag bool `json:"myAwesomeFlag"`
AnotherAwesomeArg string `json:"anotherAwesomeArg"`
}
// parseConfig parses the supplied configuration (and prevResult) from stdin.
func parseConfig(stdin []byte) (*PluginConf, error) {
conf := PluginConf{}
if err := json.Unmarshal(stdin, &conf); err != nil {
return nil, fmt.Errorf("failed to parse network configuration: %v", err)
}
// Parse previous result. Remove this if your plugin is not chained.
if conf.RawPrevResult != nil {
resultBytes, err := json.Marshal(conf.RawPrevResult)
if err != nil {
return nil, fmt.Errorf("could not serialize prevResult: %v", err)
}
res, err := version.NewResult(conf.CNIVersion, resultBytes)
if err != nil {
return nil, fmt.Errorf("could not parse prevResult: %v", err)
}
conf.RawPrevResult = nil
conf.PrevResult, err = current.NewResultFromResult(res)
if err != nil {
return nil, fmt.Errorf("could not convert result to current version: %v", err)
}
}
// End previous result parsing
// Do any validation here
if conf.AnotherAwesomeArg == "" {
return nil, fmt.Errorf("anotherAwesomeArg must be specified")
}
return &conf, nil
}
// cmdAdd is called for ADD requests
func cmdAdd(args *skel.CmdArgs) error {
conf, err := parseConfig(args.StdinData)
if err != nil {
return err
}
if conf.PrevResult == nil {
return fmt.Errorf("must be called as chained plugin")
}
// This is some sample code to generate the list of container-side IPs.
// We're casting the prevResult to a 0.3.0 response, which can also include
// host-side IPs (but doesn't when converted from a 0.2.0 response).
containerIPs := make([]net.IP, 0, len(conf.PrevResult.IPs))
if conf.CNIVersion != "0.3.0" {
for _, ip := range conf.PrevResult.IPs {
containerIPs = append(containerIPs, ip.Address.IP)
}
} else {
for _, ip := range conf.PrevResult.IPs {
if ip.Interface == nil {
continue
}
intIdx := *ip.Interface
// Every IP is indexed in to the interfaces array, with "-1" standing
// for an unknown interface (which we'll assume to be Container-side
// Skip all IPs we know belong to an interface with the wrong name.
if intIdx >= 0 && intIdx < len(conf.PrevResult.Interfaces) && conf.PrevResult.Interfaces[intIdx].Name != args.IfName {
continue
}
containerIPs = append(containerIPs, ip.Address.IP)
}
}
if len(containerIPs) == 0 {
return fmt.Errorf("got no container IPs")
}
// Pass through the result for the next plugin
return types.PrintResult(conf.PrevResult, conf.CNIVersion)
}
// cmdDel is called for DELETE requests
func cmdDel(args *skel.CmdArgs) error {
conf, err := parseConfig(args.StdinData)
if err != nil {
return err
}
_ = conf
// Do your delete here
return nil
}
func main() {
// TODO: implement plugin version
skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.All, "TODO")
}
func cmdGet(args *skel.CmdArgs) error {
// TODO: implement
return fmt.Errorf("not implemented")
}
上記はシンプルだが、Chainプラグインの2番目以降のプラグインとして呼び出されることを前提としてコードになっている。prevResult関連の処理やCNIバージョンに依存したコンテナIPの処理などが少し複雑。
Goで書いた最もシンプルなCNIサンプルコード
cnisampleリポジトリに実際に動くサンプルコードを置いた。
package main
import (
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/types/current"
"github.com/containernetworking/cni/pkg/version"
)
// cmdAdd is called for ADD requests
func cmdAdd(args *skel.CmdArgs) error {
return types.PrintResult(¤t.Result{}, "0.4.0")
}
// cmdDel is called for DELETE requests
func cmdDel(args *skel.CmdArgs) error {
return nil
}
func main() {
skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.All, "")
}
func cmdGet(args *skel.CmdArgs) error {
return types.PrintResult(¤t.Result{}, "0.4.0")
}
見てわかる通り特に何もしない。CNIオペレーションのためのコマンド(ADD, DEL, GET)しかCNI設定を活用していないし、ネットワーク設定も読み込むことすらしていない。ちゃんとしたものを書くなら前章の公式のコードを改良していくほうが良いかもしれない。上記のコード上でcniリポジトリのパッケージをいくつも利用しているが大したことはしてない、ただここから膨らませていくなら便利ではあるのでサンプルコードからそのまま流用した。cmdAdd
で処理を実装したらcurrent.Result{}
に処理結果を入れてPrintResult
に渡す。
$ go get github.com/containernetworking/cni/cnitool
$ sudo ip netns add testing
$ git clone https://github.com/hichihara/cnisample
$ cd cnisample/go/noop
$ mkdir bin
$ go build -o ./bin/noop
$ sudo CNI_PATH=./bin NETCONFPATH=./netconf cnitool add mysample /var/run/netns/testing
{
"cniVersion": "0.4.0",
"dns": {}
}
Cleanup
$ sudo ip netns del testing
次回は実際に利用価値のありそうなプラグインを試しに書いてみて記事にしたい(アイデアはもうある)。