github.com/jk-he/cni@v0.8.1/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  	"strings"
    28  
    29  	"github.com/containernetworking/cni/pkg/types"
    30  	"github.com/containernetworking/cni/pkg/utils"
    31  	"github.com/containernetworking/cni/pkg/version"
    32  )
    33  
    34  // CmdArgs captures all the arguments passed in to the plugin
    35  // via both env vars and stdin
    36  type CmdArgs struct {
    37  	ContainerID string
    38  	Netns       string
    39  	IfName      string
    40  	Args        string
    41  	Path        string
    42  	StdinData   []byte
    43  }
    44  
    45  type dispatcher struct {
    46  	Getenv func(string) string
    47  	Stdin  io.Reader
    48  	Stdout io.Writer
    49  	Stderr io.Writer
    50  
    51  	ConfVersionDecoder version.ConfigDecoder
    52  	VersionReconciler  version.Reconciler
    53  }
    54  
    55  type reqForCmdEntry map[string]bool
    56  
    57  func (t *dispatcher) getCmdArgsFromEnv() (string, *CmdArgs, *types.Error) {
    58  	var cmd, contID, netns, ifName, args, path string
    59  
    60  	vars := []struct {
    61  		name      string
    62  		val       *string
    63  		reqForCmd reqForCmdEntry
    64  	}{
    65  		{
    66  			"CNI_COMMAND",
    67  			&cmd,
    68  			reqForCmdEntry{
    69  				"ADD":   true,
    70  				"CHECK": true,
    71  				"DEL":   true,
    72  			},
    73  		},
    74  		{
    75  			"CNI_CONTAINERID",
    76  			&contID,
    77  			reqForCmdEntry{
    78  				"ADD":   true,
    79  				"CHECK": true,
    80  				"DEL":   true,
    81  			},
    82  		},
    83  		{
    84  			"CNI_NETNS",
    85  			&netns,
    86  			reqForCmdEntry{
    87  				"ADD":   true,
    88  				"CHECK": true,
    89  				"DEL":   false,
    90  			},
    91  		},
    92  		{
    93  			"CNI_IFNAME",
    94  			&ifName,
    95  			reqForCmdEntry{
    96  				"ADD":   true,
    97  				"CHECK": true,
    98  				"DEL":   true,
    99  			},
   100  		},
   101  		{
   102  			"CNI_ARGS",
   103  			&args,
   104  			reqForCmdEntry{
   105  				"ADD":   false,
   106  				"CHECK": false,
   107  				"DEL":   false,
   108  			},
   109  		},
   110  		{
   111  			"CNI_PATH",
   112  			&path,
   113  			reqForCmdEntry{
   114  				"ADD":   true,
   115  				"CHECK": true,
   116  				"DEL":   true,
   117  			},
   118  		},
   119  	}
   120  
   121  	argsMissing := make([]string, 0)
   122  	for _, v := range vars {
   123  		*v.val = t.Getenv(v.name)
   124  		if *v.val == "" {
   125  			if v.reqForCmd[cmd] || v.name == "CNI_COMMAND" {
   126  				argsMissing = append(argsMissing, v.name)
   127  			}
   128  		}
   129  	}
   130  
   131  	if len(argsMissing) > 0 {
   132  		joined := strings.Join(argsMissing, ",")
   133  		return "", nil, types.NewError(types.ErrInvalidEnvironmentVariables, fmt.Sprintf("required env variables [%s] missing", joined), "")
   134  	}
   135  
   136  	if cmd == "VERSION" {
   137  		t.Stdin = bytes.NewReader(nil)
   138  	}
   139  
   140  	stdinData, err := ioutil.ReadAll(t.Stdin)
   141  	if err != nil {
   142  		return "", nil, types.NewError(types.ErrIOFailure, fmt.Sprintf("error reading from stdin: %v", err), "")
   143  	}
   144  
   145  	cmdArgs := &CmdArgs{
   146  		ContainerID: contID,
   147  		Netns:       netns,
   148  		IfName:      ifName,
   149  		Args:        args,
   150  		Path:        path,
   151  		StdinData:   stdinData,
   152  	}
   153  	return cmd, cmdArgs, nil
   154  }
   155  
   156  func (t *dispatcher) checkVersionAndCall(cmdArgs *CmdArgs, pluginVersionInfo version.PluginInfo, toCall func(*CmdArgs) error) *types.Error {
   157  	configVersion, err := t.ConfVersionDecoder.Decode(cmdArgs.StdinData)
   158  	if err != nil {
   159  		return types.NewError(types.ErrDecodingFailure, err.Error(), "")
   160  	}
   161  	verErr := t.VersionReconciler.Check(configVersion, pluginVersionInfo)
   162  	if verErr != nil {
   163  		return types.NewError(types.ErrIncompatibleCNIVersion, "incompatible CNI versions", verErr.Details())
   164  	}
   165  
   166  	if err = toCall(cmdArgs); err != nil {
   167  		if e, ok := err.(*types.Error); ok {
   168  			// don't wrap Error in Error
   169  			return e
   170  		}
   171  		return types.NewError(types.ErrInternal, err.Error(), "")
   172  	}
   173  
   174  	return nil
   175  }
   176  
   177  func validateConfig(jsonBytes []byte) *types.Error {
   178  	var conf struct {
   179  		Name string `json:"name"`
   180  	}
   181  	if err := json.Unmarshal(jsonBytes, &conf); err != nil {
   182  		return types.NewError(types.ErrDecodingFailure, fmt.Sprintf("error unmarshall network config: %v", err), "")
   183  	}
   184  	if conf.Name == "" {
   185  		return types.NewError(types.ErrInvalidNetworkConfig, "missing network name", "")
   186  	}
   187  	if err := utils.ValidateNetworkName(conf.Name); err != nil {
   188  		return err
   189  	}
   190  	return nil
   191  }
   192  
   193  func (t *dispatcher) pluginMain(cmdAdd, cmdCheck, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo, about string) *types.Error {
   194  	cmd, cmdArgs, err := t.getCmdArgsFromEnv()
   195  	if err != nil {
   196  		// Print the about string to stderr when no command is set
   197  		if err.Code == types.ErrInvalidEnvironmentVariables && t.Getenv("CNI_COMMAND") == "" && about != "" {
   198  			_, _ = fmt.Fprintln(t.Stderr, about)
   199  			return nil
   200  		}
   201  		return err
   202  	}
   203  
   204  	if cmd != "VERSION" {
   205  		if err = validateConfig(cmdArgs.StdinData); err != nil {
   206  			return err
   207  		}
   208  		if err = utils.ValidateContainerID(cmdArgs.ContainerID); err != nil {
   209  			return err
   210  		}
   211  		if err = utils.ValidateInterfaceName(cmdArgs.IfName); err != nil {
   212  			return err
   213  		}
   214  	}
   215  
   216  	switch cmd {
   217  	case "ADD":
   218  		err = t.checkVersionAndCall(cmdArgs, versionInfo, cmdAdd)
   219  	case "CHECK":
   220  		configVersion, err := t.ConfVersionDecoder.Decode(cmdArgs.StdinData)
   221  		if err != nil {
   222  			return types.NewError(types.ErrDecodingFailure, err.Error(), "")
   223  		}
   224  		if gtet, err := version.GreaterThanOrEqualTo(configVersion, "0.4.0"); err != nil {
   225  			return types.NewError(types.ErrDecodingFailure, err.Error(), "")
   226  		} else if !gtet {
   227  			return types.NewError(types.ErrIncompatibleCNIVersion, "config version does not allow CHECK", "")
   228  		}
   229  		for _, pluginVersion := range versionInfo.SupportedVersions() {
   230  			gtet, err := version.GreaterThanOrEqualTo(pluginVersion, configVersion)
   231  			if err != nil {
   232  				return types.NewError(types.ErrDecodingFailure, err.Error(), "")
   233  			} else if gtet {
   234  				if err := t.checkVersionAndCall(cmdArgs, versionInfo, cmdCheck); err != nil {
   235  					return err
   236  				}
   237  				return nil
   238  			}
   239  		}
   240  		return types.NewError(types.ErrIncompatibleCNIVersion, "plugin version does not allow CHECK", "")
   241  	case "DEL":
   242  		err = t.checkVersionAndCall(cmdArgs, versionInfo, cmdDel)
   243  	case "VERSION":
   244  		if err := versionInfo.Encode(t.Stdout); err != nil {
   245  			return types.NewError(types.ErrIOFailure, err.Error(), "")
   246  		}
   247  	default:
   248  		return types.NewError(types.ErrInvalidEnvironmentVariables, fmt.Sprintf("unknown CNI_COMMAND: %v", cmd), "")
   249  	}
   250  
   251  	if err != nil {
   252  		return err
   253  	}
   254  	return nil
   255  }
   256  
   257  // PluginMainWithError is the core "main" for a plugin. It accepts
   258  // callback functions for add, check, 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, cmdCheck, 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, cmdCheck, 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 recommended output is "CNI plugin <foo> v<version>"
   284  //
   285  // When an error occurs in either cmdAdd, cmdCheck, 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, cmdCheck, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo, about string) {
   290  	if e := PluginMainWithError(cmdAdd, cmdCheck, 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  }