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 }