Dan Williams 0c2a034f01 api,libcni: add network config list-based plugin chaining
Using a new ".configlist" file format that allows specifying
a list of CNI network configurations to run, add new libcni
helper functions to call each plugin in the list, injecting
the overall name, CNI version, and previous plugin's Result
structure into the configuration of the next plugin.
2017-01-19 22:28:21 -06:00

201 lines
4.8 KiB
Go

// Copyright 2016 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/*
Noop plugin is a CNI plugin designed for use as a test-double.
When calling, set the CNI_ARGS env var equal to the path of a file containing
the JSON encoding of a Debug.
*/
package main
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"strings"
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/version"
noop_debug "github.com/containernetworking/cni/plugins/test/noop/debug"
)
type NetConf struct {
types.NetConf
DebugFile string `json:"debugFile"`
PrevResult *types.Result `json:"prevResult,omitempty"`
}
func loadConf(bytes []byte) (*NetConf, error) {
n := &NetConf{}
if err := json.Unmarshal(bytes, n); err != nil {
return nil, fmt.Errorf("failed to load netconf: %v %q", err, string(bytes))
}
return n, nil
}
// parse extra args i.e. FOO=BAR;ABC=123
func parseExtraArgs(args string) (map[string]string, error) {
m := make(map[string]string)
if len(args) == 0 {
return m, nil
}
items := strings.Split(args, ";")
for _, item := range items {
kv := strings.Split(item, "=")
if len(kv) != 2 {
return nil, fmt.Errorf("CNI_ARGS invalid key/value pair: %s\n", kv)
}
m[kv[0]] = kv[1]
}
return m, nil
}
func getConfig(stdinData []byte, args string) (string, *NetConf, error) {
netConf, err := loadConf(stdinData)
if err != nil {
return "", nil, err
}
extraArgs, err := parseExtraArgs(args)
if err != nil {
return "", nil, err
}
debugFilePath, ok := extraArgs["DEBUG"]
if !ok {
debugFilePath = netConf.DebugFile
}
return debugFilePath, netConf, nil
}
func debugBehavior(args *skel.CmdArgs, command string) error {
debugFilePath, netConf, err := getConfig(args.StdinData, args.Args)
if err != nil {
return err
}
if debugFilePath == "" {
fmt.Printf(`{}`)
os.Stderr.WriteString("CNI_ARGS or config empty, no debug behavior\n")
return nil
}
debug, err := noop_debug.ReadDebug(debugFilePath)
if err != nil {
return err
}
debug.CmdArgs = *args
debug.Command = command
if debug.ReportResult == "" {
debug.ReportResult = fmt.Sprintf(` { "result": %q }`, noop_debug.EmptyReportResultMessage)
}
err = debug.WriteDebug(debugFilePath)
if err != nil {
return err
}
os.Stderr.WriteString(debug.ReportStderr)
if debug.ReportError != "" {
return errors.New(debug.ReportError)
} else if debug.ReportResult == "PASSTHROUGH" || debug.ReportResult == "INJECT-DNS" {
if debug.ReportResult == "INJECT-DNS" {
netConf.PrevResult.DNS.Nameservers = []string{"1.2.3.4"}
}
newResult, err := json.Marshal(netConf.PrevResult)
if err != nil {
return fmt.Errorf("failed to marshal new result: %v", err)
}
os.Stdout.WriteString(string(newResult))
} else {
os.Stdout.WriteString(debug.ReportResult)
}
return nil
}
func debugGetSupportedVersions(stdinData []byte) []string {
vers := []string{"0.-42.0", "0.1.0", "0.2.0"}
cniArgs := os.Getenv("CNI_ARGS")
if cniArgs == "" {
return vers
}
debugFilePath, _, err := getConfig(stdinData, cniArgs)
if err != nil {
panic("test setup error: unable to get debug file path: " + err.Error())
}
debug, err := noop_debug.ReadDebug(debugFilePath)
if err != nil {
panic("test setup error: unable to read debug file: " + err.Error())
}
if debug.ReportVersionSupport == nil {
return vers
}
return debug.ReportVersionSupport
}
func cmdAdd(args *skel.CmdArgs) error {
return debugBehavior(args, "ADD")
}
func cmdDel(args *skel.CmdArgs) error {
return debugBehavior(args, "DEL")
}
func saveStdin() ([]byte, error) {
// Read original stdin
stdinData, err := ioutil.ReadAll(os.Stdin)
if err != nil {
return nil, err
}
// Make a new pipe for stdin, and write original stdin data to it
r, w, err := os.Pipe()
if err != nil {
return nil, err
}
if _, err := w.Write(stdinData); err != nil {
return nil, err
}
if err := w.Close(); err != nil {
return nil, err
}
os.Stdin = r
return stdinData, nil
}
func main() {
// Grab and read stdin before pkg/skel gets it
stdinData, err := saveStdin()
if err != nil {
panic("test setup error: unable to read stdin: " + err.Error())
}
supportedVersions := debugGetSupportedVersions(stdinData)
skel.PluginMain(cmdAdd, cmdDel, version.PluginSupports(supportedVersions...))
}