github.com/mccv1r0/cni@v0.7.0-alpha1/pkg/skel/skel.go (about)

     1  // Copyright 2014-2016 CNI authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package skel provides skeleton code for a CNI plugin.
    16  // In particular, it implements argument parsing and validation.
    17  package skel
    18  
    19  import (
    20  	"bytes"
    21  	"encoding/json"
    22  	"fmt"
    23  	"io"
    24  	"io/ioutil"
    25  	"log"
    26  	"os"
    27  
    28  	"github.com/containernetworking/cni/pkg/types"
    29  	"github.com/containernetworking/cni/pkg/version"
    30  )
    31  
    32  // CmdArgs captures all the arguments passed in to the plugin
    33  // via both env vars and stdin
    34  type CmdArgs struct {
    35  	ContainerID string
    36  	Netns       string
    37  	IfName      string
    38  	Args        string
    39  	Path        string
    40  	StdinData   []byte
    41  }
    42  
    43  type dispatcher struct {
    44  	Getenv func(string) string
    45  	Stdin  io.Reader
    46  	Stdout io.Writer
    47  	Stderr io.Writer
    48  
    49  	ConfVersionDecoder version.ConfigDecoder
    50  	VersionReconciler  version.Reconciler
    51  }
    52  
    53  type reqForCmdEntry map[string]bool
    54  
    55  func (t *dispatcher) getCmdArgsFromEnv() (string, *CmdArgs, error) {
    56  	var cmd, contID, netns, ifName, args, path string
    57  
    58  	vars := []struct {
    59  		name      string
    60  		val       *string
    61  		reqForCmd reqForCmdEntry
    62  	}{
    63  		{
    64  			"CNI_COMMAND",
    65  			&cmd,
    66  			reqForCmdEntry{
    67  				"ADD": true,
    68  				"GET": true,
    69  				"DEL": true,
    70  			},
    71  		},
    72  		{
    73  			"CNI_CONTAINERID",
    74  			&contID,
    75  			reqForCmdEntry{
    76  				"ADD": true,
    77  				"GET": true,
    78  				"DEL": true,
    79  			},
    80  		},
    81  		{
    82  			"CNI_NETNS",
    83  			&netns,
    84  			reqForCmdEntry{
    85  				"ADD": true,
    86  				"GET": true,
    87  				"DEL": false,
    88  			},
    89  		},
    90  		{
    91  			"CNI_IFNAME",
    92  			&ifName,
    93  			reqForCmdEntry{
    94  				"ADD": true,
    95  				"GET": true,
    96  				"DEL": true,
    97  			},
    98  		},
    99  		{
   100  			"CNI_ARGS",
   101  			&args,
   102  			reqForCmdEntry{
   103  				"ADD": false,
   104  				"GET": false,
   105  				"DEL": false,
   106  			},
   107  		},
   108  		{
   109  			"CNI_PATH",
   110  			&path,
   111  			reqForCmdEntry{
   112  				"ADD": true,
   113  				"GET": true,
   114  				"DEL": true,
   115  			},
   116  		},
   117  	}
   118  
   119  	argsMissing := false
   120  	for _, v := range vars {
   121  		*v.val = t.Getenv(v.name)
   122  		if *v.val == "" {
   123  			if v.reqForCmd[cmd] || v.name == "CNI_COMMAND" {
   124  				fmt.Fprintf(t.Stderr, "%v env variable missing\n", v.name)
   125  				argsMissing = true
   126  			}
   127  		}
   128  	}
   129  
   130  	if argsMissing {
   131  		return "", nil, fmt.Errorf("required env variables missing")
   132  	}
   133  
   134  	if cmd == "VERSION" {
   135  		t.Stdin = bytes.NewReader(nil)
   136  	}
   137  
   138  	stdinData, err := ioutil.ReadAll(t.Stdin)
   139  	if err != nil {
   140  		return "", nil, fmt.Errorf("error reading from stdin: %v", err)
   141  	}
   142  
   143  	cmdArgs := &CmdArgs{
   144  		ContainerID: contID,
   145  		Netns:       netns,
   146  		IfName:      ifName,
   147  		Args:        args,
   148  		Path:        path,
   149  		StdinData:   stdinData,
   150  	}
   151  	return cmd, cmdArgs, nil
   152  }
   153  
   154  func createTypedError(f string, args ...interface{}) *types.Error {
   155  	return &types.Error{
   156  		Code: 100,
   157  		Msg:  fmt.Sprintf(f, args...),
   158  	}
   159  }
   160  
   161  func (t *dispatcher) checkVersionAndCall(cmdArgs *CmdArgs, pluginVersionInfo version.PluginInfo, toCall func(*CmdArgs) error) error {
   162  	configVersion, err := t.ConfVersionDecoder.Decode(cmdArgs.StdinData)
   163  	if err != nil {
   164  		return err
   165  	}
   166  	verErr := t.VersionReconciler.Check(configVersion, pluginVersionInfo)
   167  	if verErr != nil {
   168  		return &types.Error{
   169  			Code:    types.ErrIncompatibleCNIVersion,
   170  			Msg:     "incompatible CNI versions",
   171  			Details: verErr.Details(),
   172  		}
   173  	}
   174  
   175  	return toCall(cmdArgs)
   176  }
   177  
   178  func validateConfig(jsonBytes []byte) error {
   179  	var conf struct {
   180  		Name string `json:"name"`
   181  	}
   182  	if err := json.Unmarshal(jsonBytes, &conf); err != nil {
   183  		return fmt.Errorf("error reading network config: %s", err)
   184  	}
   185  	if conf.Name == "" {
   186  		return fmt.Errorf("missing network name")
   187  	}
   188  	return nil
   189  }
   190  
   191  func (t *dispatcher) pluginMain(cmdAdd, cmdGet, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo, about string) *types.Error {
   192  	cmd, cmdArgs, err := t.getCmdArgsFromEnv()
   193  	if err != nil {
   194  		// Print the about string to stderr when no command is set
   195  		if t.Getenv("CNI_COMMAND") == "" && about != "" {
   196  			fmt.Fprintln(t.Stderr, about)
   197  		}
   198  		return createTypedError(err.Error())
   199  	}
   200  
   201  	if cmd != "VERSION" {
   202  		err = validateConfig(cmdArgs.StdinData)
   203  		if err != nil {
   204  			return createTypedError(err.Error())
   205  		}
   206  	}
   207  
   208  	switch cmd {
   209  	case "ADD":
   210  		err = t.checkVersionAndCall(cmdArgs, versionInfo, cmdAdd)
   211  	case "GET":
   212  		configVersion, err := t.ConfVersionDecoder.Decode(cmdArgs.StdinData)
   213  		if err != nil {
   214  			return createTypedError(err.Error())
   215  		}
   216  		if gtet, err := version.GreaterThanOrEqualTo(configVersion, "0.4.0"); err != nil {
   217  			return createTypedError(err.Error())
   218  		} else if !gtet {
   219  			return &types.Error{
   220  				Code: types.ErrIncompatibleCNIVersion,
   221  				Msg:  "config version does not allow GET",
   222  			}
   223  		}
   224  		for _, pluginVersion := range versionInfo.SupportedVersions() {
   225  			gtet, err := version.GreaterThanOrEqualTo(pluginVersion, configVersion)
   226  			if err != nil {
   227  				return createTypedError(err.Error())
   228  			} else if gtet {
   229  				if err := t.checkVersionAndCall(cmdArgs, versionInfo, cmdGet); err != nil {
   230  					return createTypedError(err.Error())
   231  				}
   232  				return nil
   233  			}
   234  		}
   235  		return &types.Error{
   236  			Code: types.ErrIncompatibleCNIVersion,
   237  			Msg:  "plugin version does not allow GET",
   238  		}
   239  	case "DEL":
   240  		err = t.checkVersionAndCall(cmdArgs, versionInfo, cmdDel)
   241  	case "VERSION":
   242  		err = versionInfo.Encode(t.Stdout)
   243  	default:
   244  		return createTypedError("unknown CNI_COMMAND: %v", cmd)
   245  	}
   246  
   247  	if err != nil {
   248  		if e, ok := err.(*types.Error); ok {
   249  			// don't wrap Error in Error
   250  			return e
   251  		}
   252  		return createTypedError(err.Error())
   253  	}
   254  	return nil
   255  }
   256  
   257  // PluginMainWithError is the core "main" for a plugin. It accepts
   258  // callback functions for add, get, and del CNI commands and returns an error.
   259  //
   260  // The caller must also specify what CNI spec versions the plugin supports.
   261  //
   262  // It is the responsibility of the caller to check for non-nil error return.
   263  //
   264  // For a plugin to comply with the CNI spec, it must print any error to stdout
   265  // as JSON and then exit with nonzero status code.
   266  //
   267  // To let this package automatically handle errors and call os.Exit(1) for you,
   268  // use PluginMain() instead.
   269  func PluginMainWithError(cmdAdd, cmdGet, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo, about string) *types.Error {
   270  	return (&dispatcher{
   271  		Getenv: os.Getenv,
   272  		Stdin:  os.Stdin,
   273  		Stdout: os.Stdout,
   274  		Stderr: os.Stderr,
   275  	}).pluginMain(cmdAdd, cmdGet, cmdDel, versionInfo, about)
   276  }
   277  
   278  // PluginMain is the core "main" for a plugin which includes automatic error handling.
   279  //
   280  // The caller must also specify what CNI spec versions the plugin supports.
   281  //
   282  // The caller can specify an "about" string, which is printed on stderr
   283  // when no CNI_COMMAND is specified. The reccomended output is "CNI plugin <foo> v<version>"
   284  //
   285  // When an error occurs in either cmdAdd, cmdGet, or cmdDel, PluginMain will print the error
   286  // as JSON to stdout and call os.Exit(1).
   287  //
   288  // To have more control over error handling, use PluginMainWithError() instead.
   289  func PluginMain(cmdAdd, cmdGet, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo, about string) {
   290  	if e := PluginMainWithError(cmdAdd, cmdGet, cmdDel, versionInfo, about); e != nil {
   291  		if err := e.Print(); err != nil {
   292  			log.Print("Error writing error JSON to stdout: ", err)
   293  		}
   294  		os.Exit(1)
   295  	}
   296  }