Add 'pkg/' from commit 'a11be4d7596203874b742b6597caf255204c56c3'
git-subtree-dir: pkg git-subtree-mainline: dcf7368eeab15e2affc6256f0bb1e84dd46a34de git-subtree-split: a11be4d7596203874b742b6597caf255204c56c3
This commit is contained in:
commit
8b43e90949
79
pkg/invoke/args.go
Normal file
79
pkg/invoke/args.go
Normal file
@ -0,0 +1,79 @@
|
||||
// Copyright 2015 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.
|
||||
|
||||
package invoke
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type CNIArgs interface {
|
||||
// For use with os/exec; i.e., return nil to inherit the
|
||||
// environment from this process
|
||||
AsEnv() []string
|
||||
}
|
||||
|
||||
type inherited struct{}
|
||||
|
||||
var inheritArgsFromEnv inherited
|
||||
|
||||
func (_ *inherited) AsEnv() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ArgsFromEnv() CNIArgs {
|
||||
return &inheritArgsFromEnv
|
||||
}
|
||||
|
||||
type Args struct {
|
||||
Command string
|
||||
ContainerID string
|
||||
NetNS string
|
||||
PluginArgs [][2]string
|
||||
PluginArgsStr string
|
||||
IfName string
|
||||
Path string
|
||||
}
|
||||
|
||||
// Args implements the CNIArgs interface
|
||||
var _ CNIArgs = &Args{}
|
||||
|
||||
func (args *Args) AsEnv() []string {
|
||||
env := os.Environ()
|
||||
pluginArgsStr := args.PluginArgsStr
|
||||
if pluginArgsStr == "" {
|
||||
pluginArgsStr = stringify(args.PluginArgs)
|
||||
}
|
||||
|
||||
env = append(env,
|
||||
"CNI_COMMAND="+args.Command,
|
||||
"CNI_CONTAINERID="+args.ContainerID,
|
||||
"CNI_NETNS="+args.NetNS,
|
||||
"CNI_ARGS="+pluginArgsStr,
|
||||
"CNI_IFNAME="+args.IfName,
|
||||
"CNI_PATH="+args.Path)
|
||||
return env
|
||||
}
|
||||
|
||||
// taken from rkt/networking/net_plugin.go
|
||||
func stringify(pluginArgs [][2]string) string {
|
||||
entries := make([]string, len(pluginArgs))
|
||||
|
||||
for i, kv := range pluginArgs {
|
||||
entries[i] = strings.Join(kv[:], "=")
|
||||
}
|
||||
|
||||
return strings.Join(entries, ";")
|
||||
}
|
53
pkg/invoke/delegate.go
Normal file
53
pkg/invoke/delegate.go
Normal file
@ -0,0 +1,53 @@
|
||||
// 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.
|
||||
|
||||
package invoke
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
)
|
||||
|
||||
func DelegateAdd(delegatePlugin string, netconf []byte) (types.Result, error) {
|
||||
if os.Getenv("CNI_COMMAND") != "ADD" {
|
||||
return nil, fmt.Errorf("CNI_COMMAND is not ADD")
|
||||
}
|
||||
|
||||
paths := filepath.SplitList(os.Getenv("CNI_PATH"))
|
||||
|
||||
pluginPath, err := FindInPath(delegatePlugin, paths)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ExecPluginWithResult(pluginPath, netconf, ArgsFromEnv())
|
||||
}
|
||||
|
||||
func DelegateDel(delegatePlugin string, netconf []byte) error {
|
||||
if os.Getenv("CNI_COMMAND") != "DEL" {
|
||||
return fmt.Errorf("CNI_COMMAND is not DEL")
|
||||
}
|
||||
|
||||
paths := filepath.SplitList(os.Getenv("CNI_PATH"))
|
||||
|
||||
pluginPath, err := FindInPath(delegatePlugin, paths)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ExecPluginWithoutResult(pluginPath, netconf, ArgsFromEnv())
|
||||
}
|
95
pkg/invoke/exec.go
Normal file
95
pkg/invoke/exec.go
Normal file
@ -0,0 +1,95 @@
|
||||
// Copyright 2015 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.
|
||||
|
||||
package invoke
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
)
|
||||
|
||||
func ExecPluginWithResult(pluginPath string, netconf []byte, args CNIArgs) (types.Result, error) {
|
||||
return defaultPluginExec.WithResult(pluginPath, netconf, args)
|
||||
}
|
||||
|
||||
func ExecPluginWithoutResult(pluginPath string, netconf []byte, args CNIArgs) error {
|
||||
return defaultPluginExec.WithoutResult(pluginPath, netconf, args)
|
||||
}
|
||||
|
||||
func GetVersionInfo(pluginPath string) (version.PluginInfo, error) {
|
||||
return defaultPluginExec.GetVersionInfo(pluginPath)
|
||||
}
|
||||
|
||||
var defaultPluginExec = &PluginExec{
|
||||
RawExec: &RawExec{Stderr: os.Stderr},
|
||||
VersionDecoder: &version.PluginDecoder{},
|
||||
}
|
||||
|
||||
type PluginExec struct {
|
||||
RawExec interface {
|
||||
ExecPlugin(pluginPath string, stdinData []byte, environ []string) ([]byte, error)
|
||||
}
|
||||
VersionDecoder interface {
|
||||
Decode(jsonBytes []byte) (version.PluginInfo, error)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *PluginExec) WithResult(pluginPath string, netconf []byte, args CNIArgs) (types.Result, error) {
|
||||
stdoutBytes, err := e.RawExec.ExecPlugin(pluginPath, netconf, args.AsEnv())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Plugin must return result in same version as specified in netconf
|
||||
versionDecoder := &version.ConfigDecoder{}
|
||||
confVersion, err := versionDecoder.Decode(netconf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return version.NewResult(confVersion, stdoutBytes)
|
||||
}
|
||||
|
||||
func (e *PluginExec) WithoutResult(pluginPath string, netconf []byte, args CNIArgs) error {
|
||||
_, err := e.RawExec.ExecPlugin(pluginPath, netconf, args.AsEnv())
|
||||
return err
|
||||
}
|
||||
|
||||
// GetVersionInfo returns the version information available about the plugin.
|
||||
// For recent-enough plugins, it uses the information returned by the VERSION
|
||||
// command. For older plugins which do not recognize that command, it reports
|
||||
// version 0.1.0
|
||||
func (e *PluginExec) GetVersionInfo(pluginPath string) (version.PluginInfo, error) {
|
||||
args := &Args{
|
||||
Command: "VERSION",
|
||||
|
||||
// set fake values required by plugins built against an older version of skel
|
||||
NetNS: "dummy",
|
||||
IfName: "dummy",
|
||||
Path: "dummy",
|
||||
}
|
||||
stdin := []byte(fmt.Sprintf(`{"cniVersion":%q}`, version.Current()))
|
||||
stdoutBytes, err := e.RawExec.ExecPlugin(pluginPath, stdin, args.AsEnv())
|
||||
if err != nil {
|
||||
if err.Error() == "unknown CNI_COMMAND: VERSION" {
|
||||
return version.PluginSupports("0.1.0"), nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return e.VersionDecoder.Decode(stdoutBytes)
|
||||
}
|
157
pkg/invoke/exec_test.go
Normal file
157
pkg/invoke/exec_test.go
Normal file
@ -0,0 +1,157 @@
|
||||
// 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.
|
||||
|
||||
package invoke_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/invoke"
|
||||
"github.com/containernetworking/cni/pkg/invoke/fakes"
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Executing a plugin, unit tests", func() {
|
||||
var (
|
||||
pluginExec *invoke.PluginExec
|
||||
rawExec *fakes.RawExec
|
||||
versionDecoder *fakes.VersionDecoder
|
||||
|
||||
pluginPath string
|
||||
netconf []byte
|
||||
cniargs *fakes.CNIArgs
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
rawExec = &fakes.RawExec{}
|
||||
rawExec.ExecPluginCall.Returns.ResultBytes = []byte(`{ "ips": [ { "version": "4", "address": "1.2.3.4/24" } ] }`)
|
||||
|
||||
versionDecoder = &fakes.VersionDecoder{}
|
||||
versionDecoder.DecodeCall.Returns.PluginInfo = version.PluginSupports("0.42.0")
|
||||
|
||||
pluginExec = &invoke.PluginExec{
|
||||
RawExec: rawExec,
|
||||
VersionDecoder: versionDecoder,
|
||||
}
|
||||
pluginPath = "/some/plugin/path"
|
||||
netconf = []byte(`{ "some": "stdin", "cniVersion": "0.3.1" }`)
|
||||
cniargs = &fakes.CNIArgs{}
|
||||
cniargs.AsEnvCall.Returns.Env = []string{"SOME=ENV"}
|
||||
})
|
||||
|
||||
Describe("returning a result", func() {
|
||||
It("unmarshals the result bytes into the Result type", func() {
|
||||
r, err := pluginExec.WithResult(pluginPath, netconf, cniargs)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
result, err := current.GetResult(r)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(len(result.IPs)).To(Equal(1))
|
||||
Expect(result.IPs[0].Address.IP.String()).To(Equal("1.2.3.4"))
|
||||
})
|
||||
|
||||
It("passes its arguments through to the rawExec", func() {
|
||||
pluginExec.WithResult(pluginPath, netconf, cniargs)
|
||||
Expect(rawExec.ExecPluginCall.Received.PluginPath).To(Equal(pluginPath))
|
||||
Expect(rawExec.ExecPluginCall.Received.StdinData).To(Equal(netconf))
|
||||
Expect(rawExec.ExecPluginCall.Received.Environ).To(Equal([]string{"SOME=ENV"}))
|
||||
})
|
||||
|
||||
Context("when the rawExec fails", func() {
|
||||
BeforeEach(func() {
|
||||
rawExec.ExecPluginCall.Returns.Error = errors.New("banana")
|
||||
})
|
||||
It("returns the error", func() {
|
||||
_, err := pluginExec.WithResult(pluginPath, netconf, cniargs)
|
||||
Expect(err).To(MatchError("banana"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("without returning a result", func() {
|
||||
It("passes its arguments through to the rawExec", func() {
|
||||
pluginExec.WithoutResult(pluginPath, netconf, cniargs)
|
||||
Expect(rawExec.ExecPluginCall.Received.PluginPath).To(Equal(pluginPath))
|
||||
Expect(rawExec.ExecPluginCall.Received.StdinData).To(Equal(netconf))
|
||||
Expect(rawExec.ExecPluginCall.Received.Environ).To(Equal([]string{"SOME=ENV"}))
|
||||
})
|
||||
|
||||
Context("when the rawExec fails", func() {
|
||||
BeforeEach(func() {
|
||||
rawExec.ExecPluginCall.Returns.Error = errors.New("banana")
|
||||
})
|
||||
It("returns the error", func() {
|
||||
err := pluginExec.WithoutResult(pluginPath, netconf, cniargs)
|
||||
Expect(err).To(MatchError("banana"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("discovering the plugin version", func() {
|
||||
BeforeEach(func() {
|
||||
rawExec.ExecPluginCall.Returns.ResultBytes = []byte(`{ "some": "version-info" }`)
|
||||
})
|
||||
|
||||
It("execs the plugin with the command VERSION", func() {
|
||||
pluginExec.GetVersionInfo(pluginPath)
|
||||
Expect(rawExec.ExecPluginCall.Received.PluginPath).To(Equal(pluginPath))
|
||||
Expect(rawExec.ExecPluginCall.Received.Environ).To(ContainElement("CNI_COMMAND=VERSION"))
|
||||
expectedStdin, _ := json.Marshal(map[string]string{"cniVersion": version.Current()})
|
||||
Expect(rawExec.ExecPluginCall.Received.StdinData).To(MatchJSON(expectedStdin))
|
||||
})
|
||||
|
||||
It("decodes and returns the version info", func() {
|
||||
versionInfo, err := pluginExec.GetVersionInfo(pluginPath)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(versionInfo.SupportedVersions()).To(Equal([]string{"0.42.0"}))
|
||||
Expect(versionDecoder.DecodeCall.Received.JSONBytes).To(MatchJSON(`{ "some": "version-info" }`))
|
||||
})
|
||||
|
||||
Context("when the rawExec fails", func() {
|
||||
BeforeEach(func() {
|
||||
rawExec.ExecPluginCall.Returns.Error = errors.New("banana")
|
||||
})
|
||||
It("returns the error", func() {
|
||||
_, err := pluginExec.GetVersionInfo(pluginPath)
|
||||
Expect(err).To(MatchError("banana"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when the plugin is too old to recognize the VERSION command", func() {
|
||||
BeforeEach(func() {
|
||||
rawExec.ExecPluginCall.Returns.Error = errors.New("unknown CNI_COMMAND: VERSION")
|
||||
})
|
||||
|
||||
It("interprets the error as a 0.1.0 version", func() {
|
||||
versionInfo, err := pluginExec.GetVersionInfo(pluginPath)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(versionInfo.SupportedVersions()).To(ConsistOf("0.1.0"))
|
||||
})
|
||||
|
||||
It("sets dummy values for env vars required by very old plugins", func() {
|
||||
pluginExec.GetVersionInfo(pluginPath)
|
||||
|
||||
env := rawExec.ExecPluginCall.Received.Environ
|
||||
Expect(env).To(ContainElement("CNI_NETNS=dummy"))
|
||||
Expect(env).To(ContainElement("CNI_IFNAME=dummy"))
|
||||
Expect(env).To(ContainElement("CNI_PATH=dummy"))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
27
pkg/invoke/fakes/cni_args.go
Normal file
27
pkg/invoke/fakes/cni_args.go
Normal file
@ -0,0 +1,27 @@
|
||||
// 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.
|
||||
|
||||
package fakes
|
||||
|
||||
type CNIArgs struct {
|
||||
AsEnvCall struct {
|
||||
Returns struct {
|
||||
Env []string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *CNIArgs) AsEnv() []string {
|
||||
return a.AsEnvCall.Returns.Env
|
||||
}
|
36
pkg/invoke/fakes/raw_exec.go
Normal file
36
pkg/invoke/fakes/raw_exec.go
Normal file
@ -0,0 +1,36 @@
|
||||
// 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.
|
||||
|
||||
package fakes
|
||||
|
||||
type RawExec struct {
|
||||
ExecPluginCall struct {
|
||||
Received struct {
|
||||
PluginPath string
|
||||
StdinData []byte
|
||||
Environ []string
|
||||
}
|
||||
Returns struct {
|
||||
ResultBytes []byte
|
||||
Error error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *RawExec) ExecPlugin(pluginPath string, stdinData []byte, environ []string) ([]byte, error) {
|
||||
e.ExecPluginCall.Received.PluginPath = pluginPath
|
||||
e.ExecPluginCall.Received.StdinData = stdinData
|
||||
e.ExecPluginCall.Received.Environ = environ
|
||||
return e.ExecPluginCall.Returns.ResultBytes, e.ExecPluginCall.Returns.Error
|
||||
}
|
34
pkg/invoke/fakes/version_decoder.go
Normal file
34
pkg/invoke/fakes/version_decoder.go
Normal file
@ -0,0 +1,34 @@
|
||||
// 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.
|
||||
|
||||
package fakes
|
||||
|
||||
import "github.com/containernetworking/cni/pkg/version"
|
||||
|
||||
type VersionDecoder struct {
|
||||
DecodeCall struct {
|
||||
Received struct {
|
||||
JSONBytes []byte
|
||||
}
|
||||
Returns struct {
|
||||
PluginInfo version.PluginInfo
|
||||
Error error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *VersionDecoder) Decode(jsonData []byte) (version.PluginInfo, error) {
|
||||
e.DecodeCall.Received.JSONBytes = jsonData
|
||||
return e.DecodeCall.Returns.PluginInfo, e.DecodeCall.Returns.Error
|
||||
}
|
43
pkg/invoke/find.go
Normal file
43
pkg/invoke/find.go
Normal file
@ -0,0 +1,43 @@
|
||||
// Copyright 2015 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.
|
||||
|
||||
package invoke
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// FindInPath returns the full path of the plugin by searching in the provided path
|
||||
func FindInPath(plugin string, paths []string) (string, error) {
|
||||
if plugin == "" {
|
||||
return "", fmt.Errorf("no plugin name provided")
|
||||
}
|
||||
|
||||
if len(paths) == 0 {
|
||||
return "", fmt.Errorf("no paths provided")
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
for _, fe := range ExecutableFileExtensions {
|
||||
fullpath := filepath.Join(path, plugin) + fe
|
||||
if fi, err := os.Stat(fullpath); err == nil && fi.Mode().IsRegular() {
|
||||
return fullpath, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("failed to find plugin %q in path %s", plugin, paths)
|
||||
}
|
103
pkg/invoke/find_test.go
Normal file
103
pkg/invoke/find_test.go
Normal file
@ -0,0 +1,103 @@
|
||||
// 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.
|
||||
|
||||
package invoke_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/invoke"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("FindInPath", func() {
|
||||
var (
|
||||
multiplePaths []string
|
||||
pluginName string
|
||||
plugin2NameWithExt string
|
||||
plugin2NameWithoutExt string
|
||||
pluginDir string
|
||||
anotherTempDir string
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
tempDir, err := ioutil.TempDir("", "cni-find")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
plugin, err := ioutil.TempFile(tempDir, "a-cni-plugin")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
plugin2Name := "a-plugin-with-extension" + invoke.ExecutableFileExtensions[0]
|
||||
plugin2, err := os.Create(filepath.Join(tempDir, plugin2Name))
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
anotherTempDir, err = ioutil.TempDir("", "nothing-here")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
multiplePaths = []string{anotherTempDir, tempDir}
|
||||
pluginDir, pluginName = filepath.Split(plugin.Name())
|
||||
_, plugin2NameWithExt = filepath.Split(plugin2.Name())
|
||||
plugin2NameWithoutExt = strings.Split(plugin2NameWithExt, ".")[0]
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
os.RemoveAll(pluginDir)
|
||||
os.RemoveAll(anotherTempDir)
|
||||
})
|
||||
|
||||
Context("when multiple paths are provided", func() {
|
||||
It("returns only the path to the plugin", func() {
|
||||
pluginPath, err := invoke.FindInPath(pluginName, multiplePaths)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(pluginPath).To(Equal(filepath.Join(pluginDir, pluginName)))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when a plugin name without its file name extension is provided", func() {
|
||||
It("returns the path to the plugin, including its extension", func() {
|
||||
pluginPath, err := invoke.FindInPath(plugin2NameWithoutExt, multiplePaths)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(pluginPath).To(Equal(filepath.Join(pluginDir, plugin2NameWithExt)))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when an error occurs", func() {
|
||||
Context("when no paths are provided", func() {
|
||||
It("returns an error noting no paths were provided", func() {
|
||||
_, err := invoke.FindInPath(pluginName, []string{})
|
||||
Expect(err).To(MatchError("no paths provided"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when no plugin is provided", func() {
|
||||
It("returns an error noting the plugin name wasn't found", func() {
|
||||
_, err := invoke.FindInPath("", multiplePaths)
|
||||
Expect(err).To(MatchError("no plugin name provided"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when the plugin cannot be found", func() {
|
||||
It("returns an error noting the path", func() {
|
||||
pathsWithNothing := []string{anotherTempDir}
|
||||
_, err := invoke.FindInPath(pluginName, pathsWithNothing)
|
||||
Expect(err).To(MatchError(fmt.Sprintf("failed to find plugin %q in path %s", pluginName, pathsWithNothing)))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
107
pkg/invoke/get_version_integration_test.go
Normal file
107
pkg/invoke/get_version_integration_test.go
Normal file
@ -0,0 +1,107 @@
|
||||
// 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.
|
||||
|
||||
package invoke_test
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/invoke"
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
"github.com/containernetworking/cni/pkg/version/testhelpers"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/ginkgo/extensions/table"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("GetVersion, integration tests", func() {
|
||||
var (
|
||||
pluginDir string
|
||||
pluginPath string
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
pluginDir, err := ioutil.TempDir("", "plugins")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
pluginPath = filepath.Join(pluginDir, "test-plugin")
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
Expect(os.RemoveAll(pluginDir)).To(Succeed())
|
||||
})
|
||||
|
||||
DescribeTable("correctly reporting plugin versions",
|
||||
func(gitRef string, pluginSource string, expectedVersions version.PluginInfo) {
|
||||
Expect(testhelpers.BuildAt([]byte(pluginSource), gitRef, pluginPath)).To(Succeed())
|
||||
versionInfo, err := invoke.GetVersionInfo(pluginPath)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(versionInfo.SupportedVersions()).To(ConsistOf(expectedVersions.SupportedVersions()))
|
||||
},
|
||||
|
||||
Entry("historical: before VERSION was introduced",
|
||||
git_ref_v010, plugin_source_no_custom_versions,
|
||||
version.PluginSupports("0.1.0"),
|
||||
),
|
||||
|
||||
Entry("historical: when VERSION was introduced but plugins couldn't customize it",
|
||||
git_ref_v020_no_custom_versions, plugin_source_no_custom_versions,
|
||||
version.PluginSupports("0.1.0", "0.2.0"),
|
||||
),
|
||||
|
||||
Entry("historical: when plugins started reporting their own version list",
|
||||
git_ref_v020_custom_versions, plugin_source_v020_custom_versions,
|
||||
version.PluginSupports("0.2.0", "0.999.0"),
|
||||
),
|
||||
|
||||
// this entry tracks the current behavior. Before you change it, ensure
|
||||
// that its previous behavior is captured in the most recent "historical" entry
|
||||
Entry("current",
|
||||
"HEAD", plugin_source_v020_custom_versions,
|
||||
version.PluginSupports("0.2.0", "0.999.0"),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
// a 0.2.0 plugin that can report its own versions
|
||||
const plugin_source_v020_custom_versions = `package main
|
||||
|
||||
import (
|
||||
"github.com/containernetworking/cni/pkg/skel"
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func c(_ *skel.CmdArgs) error { fmt.Println("{}"); return nil }
|
||||
|
||||
func main() { skel.PluginMain(c, c, version.PluginSupports("0.2.0", "0.999.0")) }
|
||||
`
|
||||
const git_ref_v020_custom_versions = "bf31ed15"
|
||||
|
||||
// a minimal 0.1.0 / 0.2.0 plugin that cannot report it's own version support
|
||||
const plugin_source_no_custom_versions = `package main
|
||||
|
||||
import "github.com/containernetworking/cni/pkg/skel"
|
||||
import "fmt"
|
||||
|
||||
func c(_ *skel.CmdArgs) error { fmt.Println("{}"); return nil }
|
||||
|
||||
func main() { skel.PluginMain(c, c) }
|
||||
`
|
||||
|
||||
const git_ref_v010 = "2c482f4"
|
||||
const git_ref_v020_no_custom_versions = "349d66d"
|
45
pkg/invoke/invoke_suite_test.go
Normal file
45
pkg/invoke/invoke_suite_test.go
Normal file
@ -0,0 +1,45 @@
|
||||
// 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.
|
||||
|
||||
package invoke_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gexec"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInvoke(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Invoke Suite")
|
||||
}
|
||||
|
||||
const packagePath = "github.com/containernetworking/cni/plugins/test/noop"
|
||||
|
||||
var pathToPlugin string
|
||||
|
||||
var _ = SynchronizedBeforeSuite(func() []byte {
|
||||
var err error
|
||||
pathToPlugin, err = gexec.Build(packagePath)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return []byte(pathToPlugin)
|
||||
}, func(crossNodeData []byte) {
|
||||
pathToPlugin = string(crossNodeData)
|
||||
})
|
||||
|
||||
var _ = SynchronizedAfterSuite(func() {}, func() {
|
||||
gexec.CleanupBuildArtifacts()
|
||||
})
|
20
pkg/invoke/os_unix.go
Normal file
20
pkg/invoke/os_unix.go
Normal file
@ -0,0 +1,20 @@
|
||||
// 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.
|
||||
|
||||
// +build darwin dragonfly freebsd linux netbsd opensbd solaris
|
||||
|
||||
package invoke
|
||||
|
||||
// Valid file extensions for plugin executables.
|
||||
var ExecutableFileExtensions = []string{""}
|
18
pkg/invoke/os_windows.go
Normal file
18
pkg/invoke/os_windows.go
Normal file
@ -0,0 +1,18 @@
|
||||
// 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.
|
||||
|
||||
package invoke
|
||||
|
||||
// Valid file extensions for plugin executables.
|
||||
var ExecutableFileExtensions = []string{".exe", ""}
|
63
pkg/invoke/raw_exec.go
Normal file
63
pkg/invoke/raw_exec.go
Normal file
@ -0,0 +1,63 @@
|
||||
// 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.
|
||||
|
||||
package invoke
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
)
|
||||
|
||||
type RawExec struct {
|
||||
Stderr io.Writer
|
||||
}
|
||||
|
||||
func (e *RawExec) ExecPlugin(pluginPath string, stdinData []byte, environ []string) ([]byte, error) {
|
||||
stdout := &bytes.Buffer{}
|
||||
|
||||
c := exec.Cmd{
|
||||
Env: environ,
|
||||
Path: pluginPath,
|
||||
Args: []string{pluginPath},
|
||||
Stdin: bytes.NewBuffer(stdinData),
|
||||
Stdout: stdout,
|
||||
Stderr: e.Stderr,
|
||||
}
|
||||
if err := c.Run(); err != nil {
|
||||
return nil, pluginErr(err, stdout.Bytes())
|
||||
}
|
||||
|
||||
return stdout.Bytes(), nil
|
||||
}
|
||||
|
||||
func pluginErr(err error, output []byte) error {
|
||||
if _, ok := err.(*exec.ExitError); ok {
|
||||
emsg := types.Error{}
|
||||
if perr := json.Unmarshal(output, &emsg); perr != nil {
|
||||
return fmt.Errorf("netplugin failed but error parsing its diagnostic message %q: %v", string(output), perr)
|
||||
}
|
||||
details := ""
|
||||
if emsg.Details != "" {
|
||||
details = fmt.Sprintf("; %v", emsg.Details)
|
||||
}
|
||||
return fmt.Errorf("%v%v", emsg.Msg, details)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
123
pkg/invoke/raw_exec_test.go
Normal file
123
pkg/invoke/raw_exec_test.go
Normal file
@ -0,0 +1,123 @@
|
||||
// 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.
|
||||
|
||||
package invoke_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/invoke"
|
||||
|
||||
noop_debug "github.com/containernetworking/cni/plugins/test/noop/debug"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("RawExec", func() {
|
||||
var (
|
||||
debugFileName string
|
||||
debug *noop_debug.Debug
|
||||
environ []string
|
||||
stdin []byte
|
||||
execer *invoke.RawExec
|
||||
)
|
||||
|
||||
const reportResult = `{ "some": "result" }`
|
||||
|
||||
BeforeEach(func() {
|
||||
debugFile, err := ioutil.TempFile("", "cni_debug")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(debugFile.Close()).To(Succeed())
|
||||
debugFileName = debugFile.Name()
|
||||
|
||||
debug = &noop_debug.Debug{
|
||||
ReportResult: reportResult,
|
||||
ReportStderr: "some stderr message",
|
||||
}
|
||||
Expect(debug.WriteDebug(debugFileName)).To(Succeed())
|
||||
|
||||
environ = []string{
|
||||
"CNI_COMMAND=ADD",
|
||||
"CNI_CONTAINERID=some-container-id",
|
||||
"CNI_ARGS=DEBUG=" + debugFileName,
|
||||
"CNI_NETNS=/some/netns/path",
|
||||
"CNI_PATH=/some/bin/path",
|
||||
"CNI_IFNAME=some-eth0",
|
||||
}
|
||||
stdin = []byte(`{"some":"stdin-json", "cniVersion": "0.3.1"}`)
|
||||
execer = &invoke.RawExec{}
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
Expect(os.Remove(debugFileName)).To(Succeed())
|
||||
})
|
||||
|
||||
It("runs the plugin with the given stdin and environment", func() {
|
||||
_, err := execer.ExecPlugin(pathToPlugin, stdin, environ)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
debug, err := noop_debug.ReadDebug(debugFileName)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(debug.Command).To(Equal("ADD"))
|
||||
Expect(debug.CmdArgs.StdinData).To(Equal(stdin))
|
||||
Expect(debug.CmdArgs.Netns).To(Equal("/some/netns/path"))
|
||||
})
|
||||
|
||||
It("returns the resulting stdout as bytes", func() {
|
||||
resultBytes, err := execer.ExecPlugin(pathToPlugin, stdin, environ)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(resultBytes).To(BeEquivalentTo(reportResult))
|
||||
})
|
||||
|
||||
Context("when the Stderr writer is set", func() {
|
||||
var stderrBuffer *bytes.Buffer
|
||||
|
||||
BeforeEach(func() {
|
||||
stderrBuffer = &bytes.Buffer{}
|
||||
execer.Stderr = stderrBuffer
|
||||
})
|
||||
|
||||
It("forwards any stderr bytes to the Stderr writer", func() {
|
||||
_, err := execer.ExecPlugin(pathToPlugin, stdin, environ)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(stderrBuffer.String()).To(Equal("some stderr message"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when the plugin errors", func() {
|
||||
BeforeEach(func() {
|
||||
debug.ReportError = "banana"
|
||||
Expect(debug.WriteDebug(debugFileName)).To(Succeed())
|
||||
})
|
||||
|
||||
It("wraps and returns the error", func() {
|
||||
_, err := execer.ExecPlugin(pathToPlugin, stdin, environ)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err).To(MatchError("banana"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when the system is unable to execute the plugin", func() {
|
||||
It("returns the error", func() {
|
||||
_, err := execer.ExecPlugin("/tmp/some/invalid/plugin/path", stdin, environ)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err).To(MatchError(ContainSubstring("/tmp/some/invalid/plugin/path")))
|
||||
})
|
||||
})
|
||||
})
|
51
pkg/ip/cidr.go
Normal file
51
pkg/ip/cidr.go
Normal file
@ -0,0 +1,51 @@
|
||||
// Copyright 2015 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.
|
||||
|
||||
package ip
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"net"
|
||||
)
|
||||
|
||||
// NextIP returns IP incremented by 1
|
||||
func NextIP(ip net.IP) net.IP {
|
||||
i := ipToInt(ip)
|
||||
return intToIP(i.Add(i, big.NewInt(1)))
|
||||
}
|
||||
|
||||
// PrevIP returns IP decremented by 1
|
||||
func PrevIP(ip net.IP) net.IP {
|
||||
i := ipToInt(ip)
|
||||
return intToIP(i.Sub(i, big.NewInt(1)))
|
||||
}
|
||||
|
||||
func ipToInt(ip net.IP) *big.Int {
|
||||
if v := ip.To4(); v != nil {
|
||||
return big.NewInt(0).SetBytes(v)
|
||||
}
|
||||
return big.NewInt(0).SetBytes(ip.To16())
|
||||
}
|
||||
|
||||
func intToIP(i *big.Int) net.IP {
|
||||
return net.IP(i.Bytes())
|
||||
}
|
||||
|
||||
// Network masks off the host portion of the IP
|
||||
func Network(ipn *net.IPNet) *net.IPNet {
|
||||
return &net.IPNet{
|
||||
IP: ipn.IP.Mask(ipn.Mask),
|
||||
Mask: ipn.Mask,
|
||||
}
|
||||
}
|
27
pkg/ip/ip_suite_test.go
Normal file
27
pkg/ip/ip_suite_test.go
Normal file
@ -0,0 +1,27 @@
|
||||
// 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.
|
||||
|
||||
package ip_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIp(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Ip Suite")
|
||||
}
|
31
pkg/ip/ipforward.go
Normal file
31
pkg/ip/ipforward.go
Normal file
@ -0,0 +1,31 @@
|
||||
// Copyright 2015 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.
|
||||
|
||||
package ip
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
func EnableIP4Forward() error {
|
||||
return echo1("/proc/sys/net/ipv4/ip_forward")
|
||||
}
|
||||
|
||||
func EnableIP6Forward() error {
|
||||
return echo1("/proc/sys/net/ipv6/conf/all/forwarding")
|
||||
}
|
||||
|
||||
func echo1(f string) error {
|
||||
return ioutil.WriteFile(f, []byte("1"), 0644)
|
||||
}
|
66
pkg/ip/ipmasq.go
Normal file
66
pkg/ip/ipmasq.go
Normal file
@ -0,0 +1,66 @@
|
||||
// Copyright 2015 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.
|
||||
|
||||
package ip
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/coreos/go-iptables/iptables"
|
||||
)
|
||||
|
||||
// SetupIPMasq installs iptables rules to masquerade traffic
|
||||
// coming from ipn and going outside of it
|
||||
func SetupIPMasq(ipn *net.IPNet, chain string, comment string) error {
|
||||
ipt, err := iptables.New()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to locate iptables: %v", err)
|
||||
}
|
||||
|
||||
if err = ipt.NewChain("nat", chain); err != nil {
|
||||
if err.(*iptables.Error).ExitStatus() != 1 {
|
||||
// TODO(eyakubovich): assumes exit status 1 implies chain exists
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err = ipt.AppendUnique("nat", chain, "-d", ipn.String(), "-j", "ACCEPT", "-m", "comment", "--comment", comment); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = ipt.AppendUnique("nat", chain, "!", "-d", "224.0.0.0/4", "-j", "MASQUERADE", "-m", "comment", "--comment", comment); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ipt.AppendUnique("nat", "POSTROUTING", "-s", ipn.String(), "-j", chain, "-m", "comment", "--comment", comment)
|
||||
}
|
||||
|
||||
// TeardownIPMasq undoes the effects of SetupIPMasq
|
||||
func TeardownIPMasq(ipn *net.IPNet, chain string, comment string) error {
|
||||
ipt, err := iptables.New()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to locate iptables: %v", err)
|
||||
}
|
||||
|
||||
if err = ipt.Delete("nat", "POSTROUTING", "-s", ipn.String(), "-j", chain, "-m", "comment", "--comment", comment); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = ipt.ClearChain("nat", chain); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ipt.DeleteChain("nat", chain)
|
||||
}
|
219
pkg/ip/link.go
Normal file
219
pkg/ip/link.go
Normal file
@ -0,0 +1,219 @@
|
||||
// Copyright 2015 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.
|
||||
|
||||
package ip
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/ns"
|
||||
"github.com/containernetworking/cni/pkg/utils/hwaddr"
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrLinkNotFound = errors.New("link not found")
|
||||
)
|
||||
|
||||
func makeVethPair(name, peer string, mtu int) (netlink.Link, error) {
|
||||
veth := &netlink.Veth{
|
||||
LinkAttrs: netlink.LinkAttrs{
|
||||
Name: name,
|
||||
Flags: net.FlagUp,
|
||||
MTU: mtu,
|
||||
},
|
||||
PeerName: peer,
|
||||
}
|
||||
if err := netlink.LinkAdd(veth); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return veth, nil
|
||||
}
|
||||
|
||||
func peerExists(name string) bool {
|
||||
if _, err := netlink.LinkByName(name); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func makeVeth(name string, mtu int) (peerName string, veth netlink.Link, err error) {
|
||||
for i := 0; i < 10; i++ {
|
||||
peerName, err = RandomVethName()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
veth, err = makeVethPair(name, peerName, mtu)
|
||||
switch {
|
||||
case err == nil:
|
||||
return
|
||||
|
||||
case os.IsExist(err):
|
||||
if peerExists(peerName) {
|
||||
continue
|
||||
}
|
||||
err = fmt.Errorf("container veth name provided (%v) already exists", name)
|
||||
return
|
||||
|
||||
default:
|
||||
err = fmt.Errorf("failed to make veth pair: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// should really never be hit
|
||||
err = fmt.Errorf("failed to find a unique veth name")
|
||||
return
|
||||
}
|
||||
|
||||
// RandomVethName returns string "veth" with random prefix (hashed from entropy)
|
||||
func RandomVethName() (string, error) {
|
||||
entropy := make([]byte, 4)
|
||||
_, err := rand.Reader.Read(entropy)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate random veth name: %v", err)
|
||||
}
|
||||
|
||||
// NetworkManager (recent versions) will ignore veth devices that start with "veth"
|
||||
return fmt.Sprintf("veth%x", entropy), nil
|
||||
}
|
||||
|
||||
func RenameLink(curName, newName string) error {
|
||||
link, err := netlink.LinkByName(curName)
|
||||
if err == nil {
|
||||
err = netlink.LinkSetName(link, newName)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func ifaceFromNetlinkLink(l netlink.Link) net.Interface {
|
||||
a := l.Attrs()
|
||||
return net.Interface{
|
||||
Index: a.Index,
|
||||
MTU: a.MTU,
|
||||
Name: a.Name,
|
||||
HardwareAddr: a.HardwareAddr,
|
||||
Flags: a.Flags,
|
||||
}
|
||||
}
|
||||
|
||||
// SetupVeth sets up a pair of virtual ethernet devices.
|
||||
// Call SetupVeth from inside the container netns. It will create both veth
|
||||
// devices and move the host-side veth into the provided hostNS namespace.
|
||||
// On success, SetupVeth returns (hostVeth, containerVeth, nil)
|
||||
func SetupVeth(contVethName string, mtu int, hostNS ns.NetNS) (net.Interface, net.Interface, error) {
|
||||
hostVethName, contVeth, err := makeVeth(contVethName, mtu)
|
||||
if err != nil {
|
||||
return net.Interface{}, net.Interface{}, err
|
||||
}
|
||||
|
||||
if err = netlink.LinkSetUp(contVeth); err != nil {
|
||||
return net.Interface{}, net.Interface{}, fmt.Errorf("failed to set %q up: %v", contVethName, err)
|
||||
}
|
||||
|
||||
hostVeth, err := netlink.LinkByName(hostVethName)
|
||||
if err != nil {
|
||||
return net.Interface{}, net.Interface{}, fmt.Errorf("failed to lookup %q: %v", hostVethName, err)
|
||||
}
|
||||
|
||||
if err = netlink.LinkSetNsFd(hostVeth, int(hostNS.Fd())); err != nil {
|
||||
return net.Interface{}, net.Interface{}, fmt.Errorf("failed to move veth to host netns: %v", err)
|
||||
}
|
||||
|
||||
err = hostNS.Do(func(_ ns.NetNS) error {
|
||||
hostVeth, err = netlink.LinkByName(hostVethName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to lookup %q in %q: %v", hostVethName, hostNS.Path(), err)
|
||||
}
|
||||
|
||||
if err = netlink.LinkSetUp(hostVeth); err != nil {
|
||||
return fmt.Errorf("failed to set %q up: %v", hostVethName, err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return net.Interface{}, net.Interface{}, err
|
||||
}
|
||||
return ifaceFromNetlinkLink(hostVeth), ifaceFromNetlinkLink(contVeth), nil
|
||||
}
|
||||
|
||||
// DelLinkByName removes an interface link.
|
||||
func DelLinkByName(ifName string) error {
|
||||
iface, err := netlink.LinkByName(ifName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to lookup %q: %v", ifName, err)
|
||||
}
|
||||
|
||||
if err = netlink.LinkDel(iface); err != nil {
|
||||
return fmt.Errorf("failed to delete %q: %v", ifName, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelLinkByNameAddr remove an interface returns its IP address
|
||||
// of the specified family
|
||||
func DelLinkByNameAddr(ifName string, family int) (*net.IPNet, error) {
|
||||
iface, err := netlink.LinkByName(ifName)
|
||||
if err != nil {
|
||||
if err != nil && err.Error() == "Link not found" {
|
||||
return nil, ErrLinkNotFound
|
||||
}
|
||||
return nil, fmt.Errorf("failed to lookup %q: %v", ifName, err)
|
||||
}
|
||||
|
||||
addrs, err := netlink.AddrList(iface, family)
|
||||
if err != nil || len(addrs) == 0 {
|
||||
return nil, fmt.Errorf("failed to get IP addresses for %q: %v", ifName, err)
|
||||
}
|
||||
|
||||
if err = netlink.LinkDel(iface); err != nil {
|
||||
return nil, fmt.Errorf("failed to delete %q: %v", ifName, err)
|
||||
}
|
||||
|
||||
return addrs[0].IPNet, nil
|
||||
}
|
||||
|
||||
func SetHWAddrByIP(ifName string, ip4 net.IP, ip6 net.IP) error {
|
||||
iface, err := netlink.LinkByName(ifName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to lookup %q: %v", ifName, err)
|
||||
}
|
||||
|
||||
switch {
|
||||
case ip4 == nil && ip6 == nil:
|
||||
return fmt.Errorf("neither ip4 or ip6 specified")
|
||||
|
||||
case ip4 != nil:
|
||||
{
|
||||
hwAddr, err := hwaddr.GenerateHardwareAddr4(ip4, hwaddr.PrivateMACPrefix)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate hardware addr: %v", err)
|
||||
}
|
||||
if err = netlink.LinkSetHardwareAddr(iface, hwAddr); err != nil {
|
||||
return fmt.Errorf("failed to add hardware addr to %q: %v", ifName, err)
|
||||
}
|
||||
}
|
||||
case ip6 != nil:
|
||||
// TODO: IPv6
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
273
pkg/ip/link_test.go
Normal file
273
pkg/ip/link_test.go
Normal file
@ -0,0 +1,273 @@
|
||||
// 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.
|
||||
|
||||
package ip_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/ip"
|
||||
"github.com/containernetworking/cni/pkg/ns"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
"github.com/vishvananda/netlink/nl"
|
||||
)
|
||||
|
||||
func getHwAddr(linkname string) string {
|
||||
veth, err := netlink.LinkByName(linkname)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return fmt.Sprintf("%s", veth.Attrs().HardwareAddr)
|
||||
}
|
||||
|
||||
var _ = Describe("Link", func() {
|
||||
const (
|
||||
ifaceFormatString string = "i%d"
|
||||
mtu int = 1400
|
||||
ip4onehwaddr = "0a:58:01:01:01:01"
|
||||
)
|
||||
var (
|
||||
hostNetNS ns.NetNS
|
||||
containerNetNS ns.NetNS
|
||||
ifaceCounter int = 0
|
||||
hostVeth net.Interface
|
||||
containerVeth net.Interface
|
||||
hostVethName string
|
||||
containerVethName string
|
||||
|
||||
ip4one = net.ParseIP("1.1.1.1")
|
||||
ip4two = net.ParseIP("1.1.1.2")
|
||||
originalRandReader = rand.Reader
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
|
||||
hostNetNS, err = ns.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
containerNetNS, err = ns.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
fakeBytes := make([]byte, 20)
|
||||
//to be reset in AfterEach block
|
||||
rand.Reader = bytes.NewReader(fakeBytes)
|
||||
|
||||
_ = containerNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
hostVeth, containerVeth, err = ip.SetupVeth(fmt.Sprintf(ifaceFormatString, ifaceCounter), mtu, hostNetNS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
hostVethName = hostVeth.Name
|
||||
containerVethName = containerVeth.Name
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
Expect(containerNetNS.Close()).To(Succeed())
|
||||
Expect(hostNetNS.Close()).To(Succeed())
|
||||
ifaceCounter++
|
||||
rand.Reader = originalRandReader
|
||||
})
|
||||
|
||||
It("SetupVeth must put the veth endpoints into the separate namespaces", func() {
|
||||
_ = containerNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
containerVethFromName, err := netlink.LinkByName(containerVethName)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(containerVethFromName.Attrs().Index).To(Equal(containerVeth.Index))
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
_ = hostNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
hostVethFromName, err := netlink.LinkByName(hostVethName)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(hostVethFromName.Attrs().Index).To(Equal(hostVeth.Index))
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
Context("when container already has an interface with the same name", func() {
|
||||
It("returns useful error", func() {
|
||||
_ = containerNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
_, _, err := ip.SetupVeth(containerVethName, mtu, hostNetNS)
|
||||
Expect(err.Error()).To(Equal(fmt.Sprintf("container veth name provided (%s) already exists", containerVethName)))
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Context("deleting an non-existent device", func() {
|
||||
It("returns known error", func() {
|
||||
_ = containerNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
// This string should match the expected error codes in the cmdDel functions of some of the plugins
|
||||
_, err := ip.DelLinkByNameAddr("THIS_DONT_EXIST", netlink.FAMILY_V4)
|
||||
Expect(err).To(Equal(ip.ErrLinkNotFound))
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Context("when there is no name available for the host-side", func() {
|
||||
BeforeEach(func() {
|
||||
//adding different interface to container ns
|
||||
containerVethName += "0"
|
||||
})
|
||||
It("returns useful error", func() {
|
||||
_ = containerNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
_, _, err := ip.SetupVeth(containerVethName, mtu, hostNetNS)
|
||||
Expect(err.Error()).To(Equal("failed to move veth to host netns: file exists"))
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Context("when there is no name conflict for the host or container interfaces", func() {
|
||||
BeforeEach(func() {
|
||||
//adding different interface to container and host ns
|
||||
containerVethName += "0"
|
||||
rand.Reader = originalRandReader
|
||||
})
|
||||
It("successfully creates the second veth pair", func() {
|
||||
_ = containerNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
hostVeth, _, err := ip.SetupVeth(containerVethName, mtu, hostNetNS)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
hostVethName = hostVeth.Name
|
||||
return nil
|
||||
})
|
||||
|
||||
//verify veths are in different namespaces
|
||||
_ = containerNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
_, err := netlink.LinkByName(containerVethName)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
_ = hostNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
_, err := netlink.LinkByName(hostVethName)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
It("DelLinkByName must delete the veth endpoints", func() {
|
||||
_ = containerNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
// this will delete the host endpoint too
|
||||
err := ip.DelLinkByName(containerVethName)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
_, err = netlink.LinkByName(containerVethName)
|
||||
Expect(err).To(HaveOccurred())
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
_ = hostNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
_, err := netlink.LinkByName(hostVethName)
|
||||
Expect(err).To(HaveOccurred())
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
It("DelLinkByNameAddr must throw an error for configured interfaces", func() {
|
||||
_ = containerNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
// this will delete the host endpoint too
|
||||
addr, err := ip.DelLinkByNameAddr(containerVethName, nl.FAMILY_V4)
|
||||
Expect(err).To(HaveOccurred())
|
||||
|
||||
var ipNetNil *net.IPNet
|
||||
Expect(addr).To(Equal(ipNetNil))
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
It("SetHWAddrByIP must change the interface hwaddr and be predictable", func() {
|
||||
|
||||
_ = containerNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
var err error
|
||||
hwaddrBefore := getHwAddr(containerVethName)
|
||||
|
||||
err = ip.SetHWAddrByIP(containerVethName, ip4one, nil)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
hwaddrAfter1 := getHwAddr(containerVethName)
|
||||
|
||||
Expect(hwaddrBefore).NotTo(Equal(hwaddrAfter1))
|
||||
Expect(hwaddrAfter1).To(Equal(ip4onehwaddr))
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
It("SetHWAddrByIP must be injective", func() {
|
||||
|
||||
_ = containerNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
err := ip.SetHWAddrByIP(containerVethName, ip4one, nil)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
hwaddrAfter1 := getHwAddr(containerVethName)
|
||||
|
||||
err = ip.SetHWAddrByIP(containerVethName, ip4two, nil)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
hwaddrAfter2 := getHwAddr(containerVethName)
|
||||
|
||||
Expect(hwaddrAfter1).NotTo(Equal(hwaddrAfter2))
|
||||
return nil
|
||||
})
|
||||
})
|
||||
})
|
27
pkg/ip/route.go
Normal file
27
pkg/ip/route.go
Normal file
@ -0,0 +1,27 @@
|
||||
// Copyright 2015 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.
|
||||
|
||||
package ip
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
// AddDefaultRoute sets the default route on the given gateway.
|
||||
func AddDefaultRoute(gw net.IP, dev netlink.Link) error {
|
||||
_, defNet, _ := net.ParseCIDR("0.0.0.0/0")
|
||||
return AddRoute(defNet, gw, dev)
|
||||
}
|
41
pkg/ip/route_linux.go
Normal file
41
pkg/ip/route_linux.go
Normal file
@ -0,0 +1,41 @@
|
||||
// Copyright 2015-2017 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.
|
||||
|
||||
package ip
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
// AddRoute adds a universally-scoped route to a device.
|
||||
func AddRoute(ipn *net.IPNet, gw net.IP, dev netlink.Link) error {
|
||||
return netlink.RouteAdd(&netlink.Route{
|
||||
LinkIndex: dev.Attrs().Index,
|
||||
Scope: netlink.SCOPE_UNIVERSE,
|
||||
Dst: ipn,
|
||||
Gw: gw,
|
||||
})
|
||||
}
|
||||
|
||||
// AddHostRoute adds a host-scoped route to a device.
|
||||
func AddHostRoute(ipn *net.IPNet, gw net.IP, dev netlink.Link) error {
|
||||
return netlink.RouteAdd(&netlink.Route{
|
||||
LinkIndex: dev.Attrs().Index,
|
||||
Scope: netlink.SCOPE_HOST,
|
||||
Dst: ipn,
|
||||
Gw: gw,
|
||||
})
|
||||
}
|
34
pkg/ip/route_unspecified.go
Normal file
34
pkg/ip/route_unspecified.go
Normal file
@ -0,0 +1,34 @@
|
||||
// Copyright 2015-2017 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.
|
||||
|
||||
// +build !linux
|
||||
|
||||
package ip
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
// AddRoute adds a universally-scoped route to a device.
|
||||
func AddRoute(ipn *net.IPNet, gw net.IP, dev netlink.Link) error {
|
||||
return types.NotImplementedError
|
||||
}
|
||||
|
||||
// AddHostRoute adds a host-scoped route to a device.
|
||||
func AddHostRoute(ipn *net.IPNet, gw net.IP, dev netlink.Link) error {
|
||||
return types.NotImplementedError
|
||||
}
|
93
pkg/ipam/ipam.go
Normal file
93
pkg/ipam/ipam.go
Normal file
@ -0,0 +1,93 @@
|
||||
// Copyright 2015 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.
|
||||
|
||||
package ipam
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/invoke"
|
||||
"github.com/containernetworking/cni/pkg/ip"
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
func ExecAdd(plugin string, netconf []byte) (types.Result, error) {
|
||||
return invoke.DelegateAdd(plugin, netconf)
|
||||
}
|
||||
|
||||
func ExecDel(plugin string, netconf []byte) error {
|
||||
return invoke.DelegateDel(plugin, netconf)
|
||||
}
|
||||
|
||||
// ConfigureIface takes the result of IPAM plugin and
|
||||
// applies to the ifName interface
|
||||
func ConfigureIface(ifName string, res *current.Result) error {
|
||||
if len(res.Interfaces) == 0 {
|
||||
return fmt.Errorf("no interfaces to configure")
|
||||
}
|
||||
|
||||
link, err := netlink.LinkByName(ifName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to lookup %q: %v", ifName, err)
|
||||
}
|
||||
|
||||
if err := netlink.LinkSetUp(link); err != nil {
|
||||
return fmt.Errorf("failed to set %q UP: %v", ifName, err)
|
||||
}
|
||||
|
||||
var v4gw, v6gw net.IP
|
||||
for _, ipc := range res.IPs {
|
||||
if int(ipc.Interface) >= len(res.Interfaces) || res.Interfaces[ipc.Interface].Name != ifName {
|
||||
// IP address is for a different interface
|
||||
return fmt.Errorf("failed to add IP addr %v to %q: invalid interface index", ipc, ifName)
|
||||
}
|
||||
|
||||
addr := &netlink.Addr{IPNet: &ipc.Address, Label: ""}
|
||||
if err = netlink.AddrAdd(link, addr); err != nil {
|
||||
return fmt.Errorf("failed to add IP addr %v to %q: %v", ipc, ifName, err)
|
||||
}
|
||||
|
||||
gwIsV4 := ipc.Gateway.To4() != nil
|
||||
if gwIsV4 && v4gw == nil {
|
||||
v4gw = ipc.Gateway
|
||||
} else if !gwIsV4 && v6gw == nil {
|
||||
v6gw = ipc.Gateway
|
||||
}
|
||||
}
|
||||
|
||||
for _, r := range res.Routes {
|
||||
routeIsV4 := r.Dst.IP.To4() != nil
|
||||
gw := r.GW
|
||||
if gw == nil {
|
||||
if routeIsV4 && v4gw != nil {
|
||||
gw = v4gw
|
||||
} else if !routeIsV4 && v6gw != nil {
|
||||
gw = v6gw
|
||||
}
|
||||
}
|
||||
if err = ip.AddRoute(&r.Dst, gw, link); err != nil {
|
||||
// we skip over duplicate routes as we assume the first one wins
|
||||
if !os.IsExist(err) {
|
||||
return fmt.Errorf("failed to add route '%v via %v dev %v': %v", r.Dst, gw, ifName, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
27
pkg/ipam/ipam_suite_test.go
Normal file
27
pkg/ipam/ipam_suite_test.go
Normal file
@ -0,0 +1,27 @@
|
||||
// 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.
|
||||
|
||||
package ipam_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIpam(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Ipam Suite")
|
||||
}
|
258
pkg/ipam/ipam_test.go
Normal file
258
pkg/ipam/ipam_test.go
Normal file
@ -0,0 +1,258 @@
|
||||
// Copyright 2015 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.
|
||||
|
||||
package ipam
|
||||
|
||||
import (
|
||||
"net"
|
||||
"syscall"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/ns"
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
const LINK_NAME = "eth0"
|
||||
|
||||
func ipNetEqual(a, b *net.IPNet) bool {
|
||||
aPrefix, aBits := a.Mask.Size()
|
||||
bPrefix, bBits := b.Mask.Size()
|
||||
if aPrefix != bPrefix || aBits != bBits {
|
||||
return false
|
||||
}
|
||||
return a.IP.Equal(b.IP)
|
||||
}
|
||||
|
||||
var _ = Describe("IPAM Operations", func() {
|
||||
var originalNS ns.NetNS
|
||||
var ipv4, ipv6, routev4, routev6 *net.IPNet
|
||||
var ipgw4, ipgw6, routegwv4, routegwv6 net.IP
|
||||
var result *current.Result
|
||||
|
||||
BeforeEach(func() {
|
||||
// Create a new NetNS so we don't modify the host
|
||||
var err error
|
||||
originalNS, err = ns.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
// Add master
|
||||
err = netlink.LinkAdd(&netlink.Dummy{
|
||||
LinkAttrs: netlink.LinkAttrs{
|
||||
Name: LINK_NAME,
|
||||
},
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
_, err = netlink.LinkByName(LINK_NAME)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
ipv4, err = types.ParseCIDR("1.2.3.30/24")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(ipv4).NotTo(BeNil())
|
||||
|
||||
_, routev4, err = net.ParseCIDR("15.5.6.8/24")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(routev4).NotTo(BeNil())
|
||||
routegwv4 = net.ParseIP("1.2.3.5")
|
||||
Expect(routegwv4).NotTo(BeNil())
|
||||
|
||||
ipgw4 = net.ParseIP("1.2.3.1")
|
||||
Expect(ipgw4).NotTo(BeNil())
|
||||
|
||||
ipv6, err = types.ParseCIDR("abcd:1234:ffff::cdde/64")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(ipv6).NotTo(BeNil())
|
||||
|
||||
_, routev6, err = net.ParseCIDR("1111:dddd::aaaa/80")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(routev6).NotTo(BeNil())
|
||||
routegwv6 = net.ParseIP("abcd:1234:ffff::10")
|
||||
Expect(routegwv6).NotTo(BeNil())
|
||||
|
||||
ipgw6 = net.ParseIP("abcd:1234:ffff::1")
|
||||
Expect(ipgw6).NotTo(BeNil())
|
||||
|
||||
result = ¤t.Result{
|
||||
Interfaces: []*current.Interface{
|
||||
{
|
||||
Name: "eth0",
|
||||
Mac: "00:11:22:33:44:55",
|
||||
Sandbox: "/proc/3553/ns/net",
|
||||
},
|
||||
{
|
||||
Name: "fake0",
|
||||
Mac: "00:33:44:55:66:77",
|
||||
Sandbox: "/proc/1234/ns/net",
|
||||
},
|
||||
},
|
||||
IPs: []*current.IPConfig{
|
||||
{
|
||||
Version: "4",
|
||||
Interface: 0,
|
||||
Address: *ipv4,
|
||||
Gateway: ipgw4,
|
||||
},
|
||||
{
|
||||
Version: "6",
|
||||
Interface: 0,
|
||||
Address: *ipv6,
|
||||
Gateway: ipgw6,
|
||||
},
|
||||
},
|
||||
Routes: []*types.Route{
|
||||
{Dst: *routev4, GW: routegwv4},
|
||||
{Dst: *routev6, GW: routegwv6},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
Expect(originalNS.Close()).To(Succeed())
|
||||
})
|
||||
|
||||
It("configures a link with addresses and routes", func() {
|
||||
err := originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
err := ConfigureIface(LINK_NAME, result)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
link, err := netlink.LinkByName(LINK_NAME)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(link.Attrs().Name).To(Equal(LINK_NAME))
|
||||
|
||||
v4addrs, err := netlink.AddrList(link, syscall.AF_INET)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(len(v4addrs)).To(Equal(1))
|
||||
Expect(ipNetEqual(v4addrs[0].IPNet, ipv4)).To(Equal(true))
|
||||
|
||||
v6addrs, err := netlink.AddrList(link, syscall.AF_INET6)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(len(v6addrs)).To(Equal(2))
|
||||
|
||||
var found bool
|
||||
for _, a := range v6addrs {
|
||||
if ipNetEqual(a.IPNet, ipv6) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
Expect(found).To(Equal(true))
|
||||
|
||||
// Ensure the v4 route, v6 route, and subnet route
|
||||
routes, err := netlink.RouteList(link, 0)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
var v4found, v6found bool
|
||||
for _, route := range routes {
|
||||
isv4 := route.Dst.IP.To4() != nil
|
||||
if isv4 && ipNetEqual(route.Dst, routev4) && route.Gw.Equal(routegwv4) {
|
||||
v4found = true
|
||||
}
|
||||
if !isv4 && ipNetEqual(route.Dst, routev6) && route.Gw.Equal(routegwv6) {
|
||||
v6found = true
|
||||
}
|
||||
|
||||
if v4found && v6found {
|
||||
break
|
||||
}
|
||||
}
|
||||
Expect(v4found).To(Equal(true))
|
||||
Expect(v6found).To(Equal(true))
|
||||
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
It("configures a link with routes using address gateways", func() {
|
||||
result.Routes[0].GW = nil
|
||||
result.Routes[1].GW = nil
|
||||
err := originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
err := ConfigureIface(LINK_NAME, result)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
link, err := netlink.LinkByName(LINK_NAME)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(link.Attrs().Name).To(Equal(LINK_NAME))
|
||||
|
||||
// Ensure the v4 route, v6 route, and subnet route
|
||||
routes, err := netlink.RouteList(link, 0)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
var v4found, v6found bool
|
||||
for _, route := range routes {
|
||||
isv4 := route.Dst.IP.To4() != nil
|
||||
if isv4 && ipNetEqual(route.Dst, routev4) && route.Gw.Equal(ipgw4) {
|
||||
v4found = true
|
||||
}
|
||||
if !isv4 && ipNetEqual(route.Dst, routev6) && route.Gw.Equal(ipgw6) {
|
||||
v6found = true
|
||||
}
|
||||
|
||||
if v4found && v6found {
|
||||
break
|
||||
}
|
||||
}
|
||||
Expect(v4found).To(Equal(true))
|
||||
Expect(v6found).To(Equal(true))
|
||||
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
It("returns an error when the interface index doesn't match the link name", func() {
|
||||
result.IPs[0].Interface = 1
|
||||
err := originalNS.Do(func(ns.NetNS) error {
|
||||
return ConfigureIface(LINK_NAME, result)
|
||||
})
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("returns an error when the interface index is too big", func() {
|
||||
result.IPs[0].Interface = 2
|
||||
err := originalNS.Do(func(ns.NetNS) error {
|
||||
return ConfigureIface(LINK_NAME, result)
|
||||
})
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("returns an error when there are no interfaces to configure", func() {
|
||||
result.Interfaces = []*current.Interface{}
|
||||
err := originalNS.Do(func(ns.NetNS) error {
|
||||
return ConfigureIface(LINK_NAME, result)
|
||||
})
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("returns an error when configuring the wrong interface", func() {
|
||||
err := originalNS.Do(func(ns.NetNS) error {
|
||||
return ConfigureIface("asdfasdf", result)
|
||||
})
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
34
pkg/ns/README.md
Normal file
34
pkg/ns/README.md
Normal file
@ -0,0 +1,34 @@
|
||||
### Namespaces, Threads, and Go
|
||||
On Linux each OS thread can have a different network namespace. Go's thread scheduling model switches goroutines between OS threads based on OS thread load and whether the goroutine would block other goroutines. This can result in a goroutine switching network namespaces without notice and lead to errors in your code.
|
||||
|
||||
### Namespace Switching
|
||||
Switching namespaces with the `ns.Set()` method is not recommended without additional strategies to prevent unexpected namespace changes when your goroutines switch OS threads.
|
||||
|
||||
Go provides the `runtime.LockOSThread()` function to ensure a specific goroutine executes on its current OS thread and prevents any other goroutine from running in that thread until the locked one exits. Careful usage of `LockOSThread()` and goroutines can provide good control over which network namespace a given goroutine executes in.
|
||||
|
||||
For example, you cannot rely on the `ns.Set()` namespace being the current namespace after the `Set()` call unless you do two things. First, the goroutine calling `Set()` must have previously called `LockOSThread()`. Second, you must ensure `runtime.UnlockOSThread()` is not called somewhere in-between. You also cannot rely on the initial network namespace remaining the current network namespace if any other code in your program switches namespaces, unless you have already called `LockOSThread()` in that goroutine. Note that `LockOSThread()` prevents the Go scheduler from optimally scheduling goroutines for best performance, so `LockOSThread()` should only be used in small, isolated goroutines that release the lock quickly.
|
||||
|
||||
### Do() The Recommended Thing
|
||||
The `ns.Do()` method provides control over network namespaces for you by implementing these strategies. All code dependent on a particular network namespace (including the root namespace) should be wrapped in the `ns.Do()` method to ensure the correct namespace is selected for the duration of your code. For example:
|
||||
|
||||
```go
|
||||
targetNs, err := ns.NewNS()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = targetNs.Do(func(hostNs ns.NetNS) error {
|
||||
dummy := &netlink.Dummy{
|
||||
LinkAttrs: netlink.LinkAttrs{
|
||||
Name: "dummy0",
|
||||
},
|
||||
}
|
||||
return netlink.LinkAdd(dummy)
|
||||
})
|
||||
```
|
||||
|
||||
Note this requirement to wrap every network call is very onerous - any libraries you call might call out to network services such as DNS, and all such calls need to be protected after you call `ns.Do()`. The CNI plugins all exit very soon after calling `ns.Do()` which helps to minimize the problem.
|
||||
|
||||
### Further Reading
|
||||
- https://github.com/golang/go/wiki/LockOSThread
|
||||
- http://morsmachine.dk/go-scheduler
|
||||
- https://github.com/containernetworking/cni/issues/262
|
178
pkg/ns/ns.go
Normal file
178
pkg/ns/ns.go
Normal file
@ -0,0 +1,178 @@
|
||||
// Copyright 2015 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.
|
||||
|
||||
package ns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
type NetNS interface {
|
||||
// Executes the passed closure in this object's network namespace,
|
||||
// attempting to restore the original namespace before returning.
|
||||
// However, since each OS thread can have a different network namespace,
|
||||
// and Go's thread scheduling is highly variable, callers cannot
|
||||
// guarantee any specific namespace is set unless operations that
|
||||
// require that namespace are wrapped with Do(). Also, no code called
|
||||
// from Do() should call runtime.UnlockOSThread(), or the risk
|
||||
// of executing code in an incorrect namespace will be greater. See
|
||||
// https://github.com/golang/go/wiki/LockOSThread for further details.
|
||||
Do(toRun func(NetNS) error) error
|
||||
|
||||
// Sets the current network namespace to this object's network namespace.
|
||||
// Note that since Go's thread scheduling is highly variable, callers
|
||||
// cannot guarantee the requested namespace will be the current namespace
|
||||
// after this function is called; to ensure this wrap operations that
|
||||
// require the namespace with Do() instead.
|
||||
Set() error
|
||||
|
||||
// Returns the filesystem path representing this object's network namespace
|
||||
Path() string
|
||||
|
||||
// Returns a file descriptor representing this object's network namespace
|
||||
Fd() uintptr
|
||||
|
||||
// Cleans up this instance of the network namespace; if this instance
|
||||
// is the last user the namespace will be destroyed
|
||||
Close() error
|
||||
}
|
||||
|
||||
type netNS struct {
|
||||
file *os.File
|
||||
mounted bool
|
||||
closed bool
|
||||
}
|
||||
|
||||
// netNS implements the NetNS interface
|
||||
var _ NetNS = &netNS{}
|
||||
|
||||
const (
|
||||
// https://github.com/torvalds/linux/blob/master/include/uapi/linux/magic.h
|
||||
NSFS_MAGIC = 0x6e736673
|
||||
PROCFS_MAGIC = 0x9fa0
|
||||
)
|
||||
|
||||
type NSPathNotExistErr struct{ msg string }
|
||||
|
||||
func (e NSPathNotExistErr) Error() string { return e.msg }
|
||||
|
||||
type NSPathNotNSErr struct{ msg string }
|
||||
|
||||
func (e NSPathNotNSErr) Error() string { return e.msg }
|
||||
|
||||
func IsNSorErr(nspath string) error {
|
||||
stat := syscall.Statfs_t{}
|
||||
if err := syscall.Statfs(nspath, &stat); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
err = NSPathNotExistErr{msg: fmt.Sprintf("failed to Statfs %q: %v", nspath, err)}
|
||||
} else {
|
||||
err = fmt.Errorf("failed to Statfs %q: %v", nspath, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
switch stat.Type {
|
||||
case PROCFS_MAGIC, NSFS_MAGIC:
|
||||
return nil
|
||||
default:
|
||||
return NSPathNotNSErr{msg: fmt.Sprintf("unknown FS magic on %q: %x", nspath, stat.Type)}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns an object representing the namespace referred to by @path
|
||||
func GetNS(nspath string) (NetNS, error) {
|
||||
err := IsNSorErr(nspath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fd, err := os.Open(nspath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &netNS{file: fd}, nil
|
||||
}
|
||||
|
||||
func (ns *netNS) Path() string {
|
||||
return ns.file.Name()
|
||||
}
|
||||
|
||||
func (ns *netNS) Fd() uintptr {
|
||||
return ns.file.Fd()
|
||||
}
|
||||
|
||||
func (ns *netNS) errorIfClosed() error {
|
||||
if ns.closed {
|
||||
return fmt.Errorf("%q has already been closed", ns.file.Name())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ns *netNS) Do(toRun func(NetNS) error) error {
|
||||
if err := ns.errorIfClosed(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
containedCall := func(hostNS NetNS) error {
|
||||
threadNS, err := GetCurrentNS()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open current netns: %v", err)
|
||||
}
|
||||
defer threadNS.Close()
|
||||
|
||||
// switch to target namespace
|
||||
if err = ns.Set(); err != nil {
|
||||
return fmt.Errorf("error switching to ns %v: %v", ns.file.Name(), err)
|
||||
}
|
||||
defer threadNS.Set() // switch back
|
||||
|
||||
return toRun(hostNS)
|
||||
}
|
||||
|
||||
// save a handle to current network namespace
|
||||
hostNS, err := GetCurrentNS()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to open current namespace: %v", err)
|
||||
}
|
||||
defer hostNS.Close()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
|
||||
var innerError error
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
runtime.LockOSThread()
|
||||
innerError = containedCall(hostNS)
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
return innerError
|
||||
}
|
||||
|
||||
// WithNetNSPath executes the passed closure under the given network
|
||||
// namespace, restoring the original namespace afterwards.
|
||||
func WithNetNSPath(nspath string, toRun func(NetNS) error) error {
|
||||
ns, err := GetNS(nspath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer ns.Close()
|
||||
return ns.Do(toRun)
|
||||
}
|
149
pkg/ns/ns_linux.go
Normal file
149
pkg/ns/ns_linux.go
Normal file
@ -0,0 +1,149 @@
|
||||
// Copyright 2015-2017 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.
|
||||
|
||||
package ns
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// Returns an object representing the current OS thread's network namespace
|
||||
func GetCurrentNS() (NetNS, error) {
|
||||
return GetNS(getCurrentThreadNetNSPath())
|
||||
}
|
||||
|
||||
func getCurrentThreadNetNSPath() string {
|
||||
// /proc/self/ns/net returns the namespace of the main thread, not
|
||||
// of whatever thread this goroutine is running on. Make sure we
|
||||
// use the thread's net namespace since the thread is switching around
|
||||
return fmt.Sprintf("/proc/%d/task/%d/ns/net", os.Getpid(), unix.Gettid())
|
||||
}
|
||||
|
||||
// Creates a new persistent network namespace and returns an object
|
||||
// representing that namespace, without switching to it
|
||||
func NewNS() (NetNS, error) {
|
||||
const nsRunDir = "/var/run/netns"
|
||||
|
||||
b := make([]byte, 16)
|
||||
_, err := rand.Reader.Read(b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate random netns name: %v", err)
|
||||
}
|
||||
|
||||
err = os.MkdirAll(nsRunDir, 0755)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// create an empty file at the mount point
|
||||
nsName := fmt.Sprintf("cni-%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
|
||||
nsPath := path.Join(nsRunDir, nsName)
|
||||
mountPointFd, err := os.Create(nsPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mountPointFd.Close()
|
||||
|
||||
// Ensure the mount point is cleaned up on errors; if the namespace
|
||||
// was successfully mounted this will have no effect because the file
|
||||
// is in-use
|
||||
defer os.RemoveAll(nsPath)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
|
||||
// do namespace work in a dedicated goroutine, so that we can safely
|
||||
// Lock/Unlock OSThread without upsetting the lock/unlock state of
|
||||
// the caller of this function
|
||||
var fd *os.File
|
||||
go (func() {
|
||||
defer wg.Done()
|
||||
runtime.LockOSThread()
|
||||
|
||||
var origNS NetNS
|
||||
origNS, err = GetNS(getCurrentThreadNetNSPath())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer origNS.Close()
|
||||
|
||||
// create a new netns on the current thread
|
||||
err = unix.Unshare(unix.CLONE_NEWNET)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer origNS.Set()
|
||||
|
||||
// bind mount the new netns from the current thread onto the mount point
|
||||
err = unix.Mount(getCurrentThreadNetNSPath(), nsPath, "none", unix.MS_BIND, "")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
fd, err = os.Open(nsPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
})()
|
||||
wg.Wait()
|
||||
|
||||
if err != nil {
|
||||
unix.Unmount(nsPath, unix.MNT_DETACH)
|
||||
return nil, fmt.Errorf("failed to create namespace: %v", err)
|
||||
}
|
||||
|
||||
return &netNS{file: fd, mounted: true}, nil
|
||||
}
|
||||
|
||||
func (ns *netNS) Close() error {
|
||||
if err := ns.errorIfClosed(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ns.file.Close(); err != nil {
|
||||
return fmt.Errorf("Failed to close %q: %v", ns.file.Name(), err)
|
||||
}
|
||||
ns.closed = true
|
||||
|
||||
if ns.mounted {
|
||||
if err := unix.Unmount(ns.file.Name(), unix.MNT_DETACH); err != nil {
|
||||
return fmt.Errorf("Failed to unmount namespace %s: %v", ns.file.Name(), err)
|
||||
}
|
||||
if err := os.RemoveAll(ns.file.Name()); err != nil {
|
||||
return fmt.Errorf("Failed to clean up namespace %s: %v", ns.file.Name(), err)
|
||||
}
|
||||
ns.mounted = false
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ns *netNS) Set() error {
|
||||
if err := ns.errorIfClosed(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, _, err := unix.Syscall(unix.SYS_SETNS, ns.Fd(), uintptr(unix.CLONE_NEWNET), 0); err != 0 {
|
||||
return fmt.Errorf("Error switching to ns %v: %v", ns.file.Name(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
34
pkg/ns/ns_suite_test.go
Normal file
34
pkg/ns/ns_suite_test.go
Normal file
@ -0,0 +1,34 @@
|
||||
// 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.
|
||||
|
||||
package ns_test
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"runtime"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
"github.com/onsi/ginkgo/config"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNs(t *testing.T) {
|
||||
rand.Seed(config.GinkgoConfig.RandomSeed)
|
||||
runtime.LockOSThread()
|
||||
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "pkg/ns Suite")
|
||||
}
|
252
pkg/ns/ns_test.go
Normal file
252
pkg/ns/ns_test.go
Normal file
@ -0,0 +1,252 @@
|
||||
// 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.
|
||||
|
||||
package ns_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/ns"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func getInodeCurNetNS() (uint64, error) {
|
||||
curNS, err := ns.GetCurrentNS()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer curNS.Close()
|
||||
return getInodeNS(curNS)
|
||||
}
|
||||
|
||||
func getInodeNS(netns ns.NetNS) (uint64, error) {
|
||||
return getInodeFd(int(netns.Fd()))
|
||||
}
|
||||
|
||||
func getInode(path string) (uint64, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer file.Close()
|
||||
return getInodeFd(int(file.Fd()))
|
||||
}
|
||||
|
||||
func getInodeFd(fd int) (uint64, error) {
|
||||
stat := &unix.Stat_t{}
|
||||
err := unix.Fstat(fd, stat)
|
||||
return stat.Ino, err
|
||||
}
|
||||
|
||||
var _ = Describe("Linux namespace operations", func() {
|
||||
Describe("WithNetNS", func() {
|
||||
var (
|
||||
originalNetNS ns.NetNS
|
||||
targetNetNS ns.NetNS
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
|
||||
originalNetNS, err = ns.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
targetNetNS, err = ns.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
Expect(targetNetNS.Close()).To(Succeed())
|
||||
Expect(originalNetNS.Close()).To(Succeed())
|
||||
})
|
||||
|
||||
It("executes the callback within the target network namespace", func() {
|
||||
expectedInode, err := getInodeNS(targetNetNS)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
err = targetNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
actualInode, err := getInodeCurNetNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(actualInode).To(Equal(expectedInode))
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
It("provides the original namespace as the argument to the callback", func() {
|
||||
// Ensure we start in originalNetNS
|
||||
err := originalNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
origNSInode, err := getInodeNS(originalNetNS)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
err = targetNetNS.Do(func(hostNS ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
hostNSInode, err := getInodeNS(hostNS)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(hostNSInode).To(Equal(origNSInode))
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
Context("when the callback returns an error", func() {
|
||||
It("restores the calling thread to the original namespace before returning", func() {
|
||||
err := originalNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
preTestInode, err := getInodeCurNetNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
_ = targetNetNS.Do(func(ns.NetNS) error {
|
||||
return errors.New("potato")
|
||||
})
|
||||
|
||||
postTestInode, err := getInodeCurNetNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(postTestInode).To(Equal(preTestInode))
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
It("returns the error from the callback", func() {
|
||||
err := targetNetNS.Do(func(ns.NetNS) error {
|
||||
return errors.New("potato")
|
||||
})
|
||||
Expect(err).To(MatchError("potato"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("validating inode mapping to namespaces", func() {
|
||||
It("checks that different namespaces have different inodes", func() {
|
||||
origNSInode, err := getInodeNS(originalNetNS)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
testNsInode, err := getInodeNS(targetNetNS)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(testNsInode).NotTo(Equal(0))
|
||||
Expect(testNsInode).NotTo(Equal(origNSInode))
|
||||
})
|
||||
|
||||
It("should not leak a closed netns onto any threads in the process", func() {
|
||||
By("creating a new netns")
|
||||
createdNetNS, err := ns.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("discovering the inode of the created netns")
|
||||
createdNetNSInode, err := getInodeNS(createdNetNS)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
createdNetNS.Close()
|
||||
|
||||
By("comparing against the netns inode of every thread in the process")
|
||||
for _, netnsPath := range allNetNSInCurrentProcess() {
|
||||
netnsInode, err := getInode(netnsPath)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(netnsInode).NotTo(Equal(createdNetNSInode))
|
||||
}
|
||||
})
|
||||
|
||||
It("fails when the path is not a namespace", func() {
|
||||
tempFile, err := ioutil.TempFile("", "nstest")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer tempFile.Close()
|
||||
|
||||
nspath := tempFile.Name()
|
||||
defer os.Remove(nspath)
|
||||
|
||||
_, err = ns.GetNS(nspath)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err).To(BeAssignableToTypeOf(ns.NSPathNotNSErr{}))
|
||||
Expect(err).NotTo(BeAssignableToTypeOf(ns.NSPathNotExistErr{}))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("closing a network namespace", func() {
|
||||
It("should prevent further operations", func() {
|
||||
createdNetNS, err := ns.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
err = createdNetNS.Close()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
err = createdNetNS.Do(func(ns.NetNS) error { return nil })
|
||||
Expect(err).To(HaveOccurred())
|
||||
|
||||
err = createdNetNS.Set()
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("should only work once", func() {
|
||||
createdNetNS, err := ns.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
err = createdNetNS.Close()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
err = createdNetNS.Close()
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("IsNSorErr", func() {
|
||||
It("should detect a namespace", func() {
|
||||
createdNetNS, err := ns.NewNS()
|
||||
err = ns.IsNSorErr(createdNetNS.Path())
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
It("should refuse other paths", func() {
|
||||
tempFile, err := ioutil.TempFile("", "nstest")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer tempFile.Close()
|
||||
|
||||
nspath := tempFile.Name()
|
||||
defer os.Remove(nspath)
|
||||
|
||||
err = ns.IsNSorErr(nspath)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err).To(BeAssignableToTypeOf(ns.NSPathNotNSErr{}))
|
||||
Expect(err).NotTo(BeAssignableToTypeOf(ns.NSPathNotExistErr{}))
|
||||
})
|
||||
|
||||
It("should error on non-existing paths", func() {
|
||||
err := ns.IsNSorErr("/tmp/IDoNotExist")
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err).To(BeAssignableToTypeOf(ns.NSPathNotExistErr{}))
|
||||
Expect(err).NotTo(BeAssignableToTypeOf(ns.NSPathNotNSErr{}))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
func allNetNSInCurrentProcess() []string {
|
||||
pid := unix.Getpid()
|
||||
paths, err := filepath.Glob(fmt.Sprintf("/proc/%d/task/*/ns/net", pid))
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return paths
|
||||
}
|
36
pkg/ns/ns_unspecified.go
Normal file
36
pkg/ns/ns_unspecified.go
Normal file
@ -0,0 +1,36 @@
|
||||
// Copyright 2015-2017 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.
|
||||
|
||||
// +build !linux
|
||||
|
||||
package ns
|
||||
|
||||
import "github.com/containernetworking/cni/pkg/types"
|
||||
|
||||
// Returns an object representing the current OS thread's network namespace
|
||||
func GetCurrentNS() (NetNS, error) {
|
||||
return nil, types.NotImplementedError
|
||||
}
|
||||
|
||||
func NewNS() (NetNS, error) {
|
||||
return nil, types.NotImplementedError
|
||||
}
|
||||
|
||||
func (ns *netNS) Close() error {
|
||||
return types.NotImplementedError
|
||||
}
|
||||
|
||||
func (ns *netNS) Set() error {
|
||||
return types.NotImplementedError
|
||||
}
|
228
pkg/skel/skel.go
Normal file
228
pkg/skel/skel.go
Normal file
@ -0,0 +1,228 @@
|
||||
// Copyright 2014-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.
|
||||
|
||||
// Package skel provides skeleton code for a CNI plugin.
|
||||
// In particular, it implements argument parsing and validation.
|
||||
package skel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
)
|
||||
|
||||
// CmdArgs captures all the arguments passed in to the plugin
|
||||
// via both env vars and stdin
|
||||
type CmdArgs struct {
|
||||
ContainerID string
|
||||
Netns string
|
||||
IfName string
|
||||
Args string
|
||||
Path string
|
||||
StdinData []byte
|
||||
}
|
||||
|
||||
type dispatcher struct {
|
||||
Getenv func(string) string
|
||||
Stdin io.Reader
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
|
||||
ConfVersionDecoder version.ConfigDecoder
|
||||
VersionReconciler version.Reconciler
|
||||
}
|
||||
|
||||
type reqForCmdEntry map[string]bool
|
||||
|
||||
func (t *dispatcher) getCmdArgsFromEnv() (string, *CmdArgs, error) {
|
||||
var cmd, contID, netns, ifName, args, path string
|
||||
|
||||
vars := []struct {
|
||||
name string
|
||||
val *string
|
||||
reqForCmd reqForCmdEntry
|
||||
}{
|
||||
{
|
||||
"CNI_COMMAND",
|
||||
&cmd,
|
||||
reqForCmdEntry{
|
||||
"ADD": true,
|
||||
"DEL": true,
|
||||
},
|
||||
},
|
||||
{
|
||||
"CNI_CONTAINERID",
|
||||
&contID,
|
||||
reqForCmdEntry{
|
||||
"ADD": false,
|
||||
"DEL": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
"CNI_NETNS",
|
||||
&netns,
|
||||
reqForCmdEntry{
|
||||
"ADD": true,
|
||||
"DEL": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
"CNI_IFNAME",
|
||||
&ifName,
|
||||
reqForCmdEntry{
|
||||
"ADD": true,
|
||||
"DEL": true,
|
||||
},
|
||||
},
|
||||
{
|
||||
"CNI_ARGS",
|
||||
&args,
|
||||
reqForCmdEntry{
|
||||
"ADD": false,
|
||||
"DEL": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
"CNI_PATH",
|
||||
&path,
|
||||
reqForCmdEntry{
|
||||
"ADD": true,
|
||||
"DEL": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
argsMissing := false
|
||||
for _, v := range vars {
|
||||
*v.val = t.Getenv(v.name)
|
||||
if *v.val == "" {
|
||||
if v.reqForCmd[cmd] || v.name == "CNI_COMMAND" {
|
||||
fmt.Fprintf(t.Stderr, "%v env variable missing\n", v.name)
|
||||
argsMissing = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if argsMissing {
|
||||
return "", nil, fmt.Errorf("required env variables missing")
|
||||
}
|
||||
|
||||
stdinData, err := ioutil.ReadAll(t.Stdin)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("error reading from stdin: %v", err)
|
||||
}
|
||||
|
||||
cmdArgs := &CmdArgs{
|
||||
ContainerID: contID,
|
||||
Netns: netns,
|
||||
IfName: ifName,
|
||||
Args: args,
|
||||
Path: path,
|
||||
StdinData: stdinData,
|
||||
}
|
||||
return cmd, cmdArgs, nil
|
||||
}
|
||||
|
||||
func createTypedError(f string, args ...interface{}) *types.Error {
|
||||
return &types.Error{
|
||||
Code: 100,
|
||||
Msg: fmt.Sprintf(f, args...),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *dispatcher) checkVersionAndCall(cmdArgs *CmdArgs, pluginVersionInfo version.PluginInfo, toCall func(*CmdArgs) error) error {
|
||||
configVersion, err := t.ConfVersionDecoder.Decode(cmdArgs.StdinData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
verErr := t.VersionReconciler.Check(configVersion, pluginVersionInfo)
|
||||
if verErr != nil {
|
||||
return &types.Error{
|
||||
Code: types.ErrIncompatibleCNIVersion,
|
||||
Msg: "incompatible CNI versions",
|
||||
Details: verErr.Details(),
|
||||
}
|
||||
}
|
||||
return toCall(cmdArgs)
|
||||
}
|
||||
|
||||
func (t *dispatcher) pluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo) *types.Error {
|
||||
cmd, cmdArgs, err := t.getCmdArgsFromEnv()
|
||||
if err != nil {
|
||||
return createTypedError(err.Error())
|
||||
}
|
||||
|
||||
switch cmd {
|
||||
case "ADD":
|
||||
err = t.checkVersionAndCall(cmdArgs, versionInfo, cmdAdd)
|
||||
case "DEL":
|
||||
err = t.checkVersionAndCall(cmdArgs, versionInfo, cmdDel)
|
||||
case "VERSION":
|
||||
err = versionInfo.Encode(t.Stdout)
|
||||
default:
|
||||
return createTypedError("unknown CNI_COMMAND: %v", cmd)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if e, ok := err.(*types.Error); ok {
|
||||
// don't wrap Error in Error
|
||||
return e
|
||||
}
|
||||
return createTypedError(err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PluginMainWithError is the core "main" for a plugin. It accepts
|
||||
// callback functions for add and del CNI commands and returns an error.
|
||||
//
|
||||
// The caller must also specify what CNI spec versions the plugin supports.
|
||||
//
|
||||
// It is the responsibility of the caller to check for non-nil error return.
|
||||
//
|
||||
// For a plugin to comply with the CNI spec, it must print any error to stdout
|
||||
// as JSON and then exit with nonzero status code.
|
||||
//
|
||||
// To let this package automatically handle errors and call os.Exit(1) for you,
|
||||
// use PluginMain() instead.
|
||||
func PluginMainWithError(cmdAdd, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo) *types.Error {
|
||||
return (&dispatcher{
|
||||
Getenv: os.Getenv,
|
||||
Stdin: os.Stdin,
|
||||
Stdout: os.Stdout,
|
||||
Stderr: os.Stderr,
|
||||
}).pluginMain(cmdAdd, cmdDel, versionInfo)
|
||||
}
|
||||
|
||||
// PluginMain is the core "main" for a plugin which includes automatic error handling.
|
||||
//
|
||||
// The caller must also specify what CNI spec versions the plugin supports.
|
||||
//
|
||||
// When an error occurs in either cmdAdd or cmdDel, PluginMain will print the error
|
||||
// as JSON to stdout and call os.Exit(1).
|
||||
//
|
||||
// To have more control over error handling, use PluginMainWithError() instead.
|
||||
func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo) {
|
||||
if e := PluginMainWithError(cmdAdd, cmdDel, versionInfo); e != nil {
|
||||
if err := e.Print(); err != nil {
|
||||
log.Print("Error writing error JSON to stdout: ", err)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
27
pkg/skel/skel_suite_test.go
Normal file
27
pkg/skel/skel_suite_test.go
Normal file
@ -0,0 +1,27 @@
|
||||
// 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.
|
||||
|
||||
package skel
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSkel(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Skel Suite")
|
||||
}
|
346
pkg/skel/skel_test.go
Normal file
346
pkg/skel/skel_test.go
Normal file
@ -0,0 +1,346 @@
|
||||
// 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.
|
||||
|
||||
package skel
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/testutils"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/ginkgo/extensions/table"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
type fakeCmd struct {
|
||||
CallCount int
|
||||
Returns struct {
|
||||
Error error
|
||||
}
|
||||
Received struct {
|
||||
CmdArgs *CmdArgs
|
||||
}
|
||||
}
|
||||
|
||||
func (c *fakeCmd) Func(args *CmdArgs) error {
|
||||
c.CallCount++
|
||||
c.Received.CmdArgs = args
|
||||
return c.Returns.Error
|
||||
}
|
||||
|
||||
var _ = Describe("dispatching to the correct callback", func() {
|
||||
var (
|
||||
environment map[string]string
|
||||
stdinData string
|
||||
stdout, stderr *bytes.Buffer
|
||||
cmdAdd, cmdDel *fakeCmd
|
||||
dispatch *dispatcher
|
||||
expectedCmdArgs *CmdArgs
|
||||
versionInfo version.PluginInfo
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
environment = map[string]string{
|
||||
"CNI_COMMAND": "ADD",
|
||||
"CNI_CONTAINERID": "some-container-id",
|
||||
"CNI_NETNS": "/some/netns/path",
|
||||
"CNI_IFNAME": "eth0",
|
||||
"CNI_ARGS": "some;extra;args",
|
||||
"CNI_PATH": "/some/cni/path",
|
||||
}
|
||||
|
||||
stdinData = `{ "some": "config", "cniVersion": "9.8.7" }`
|
||||
stdout = &bytes.Buffer{}
|
||||
stderr = &bytes.Buffer{}
|
||||
versionInfo = version.PluginSupports("9.8.7")
|
||||
dispatch = &dispatcher{
|
||||
Getenv: func(key string) string { return environment[key] },
|
||||
Stdin: strings.NewReader(stdinData),
|
||||
Stdout: stdout,
|
||||
Stderr: stderr,
|
||||
}
|
||||
cmdAdd = &fakeCmd{}
|
||||
cmdDel = &fakeCmd{}
|
||||
expectedCmdArgs = &CmdArgs{
|
||||
ContainerID: "some-container-id",
|
||||
Netns: "/some/netns/path",
|
||||
IfName: "eth0",
|
||||
Args: "some;extra;args",
|
||||
Path: "/some/cni/path",
|
||||
StdinData: []byte(stdinData),
|
||||
}
|
||||
})
|
||||
|
||||
var envVarChecker = func(envVar string, isRequired bool) {
|
||||
delete(environment, envVar)
|
||||
|
||||
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
|
||||
if isRequired {
|
||||
Expect(err).To(Equal(&types.Error{
|
||||
Code: 100,
|
||||
Msg: "required env variables missing",
|
||||
}))
|
||||
Expect(stderr.String()).To(ContainSubstring(envVar + " env variable missing\n"))
|
||||
} else {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
}
|
||||
}
|
||||
|
||||
Context("when the CNI_COMMAND is ADD", func() {
|
||||
It("extracts env vars and stdin data and calls cmdAdd", func() {
|
||||
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(cmdAdd.CallCount).To(Equal(1))
|
||||
Expect(cmdDel.CallCount).To(Equal(0))
|
||||
Expect(cmdAdd.Received.CmdArgs).To(Equal(expectedCmdArgs))
|
||||
})
|
||||
|
||||
It("does not call cmdDel", func() {
|
||||
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(cmdDel.CallCount).To(Equal(0))
|
||||
})
|
||||
|
||||
DescribeTable("required / optional env vars", envVarChecker,
|
||||
Entry("command", "CNI_COMMAND", true),
|
||||
Entry("container id", "CNI_CONTAINERID", false),
|
||||
Entry("net ns", "CNI_NETNS", true),
|
||||
Entry("if name", "CNI_IFNAME", true),
|
||||
Entry("args", "CNI_ARGS", false),
|
||||
Entry("path", "CNI_PATH", true),
|
||||
)
|
||||
|
||||
Context("when multiple required env vars are missing", func() {
|
||||
BeforeEach(func() {
|
||||
delete(environment, "CNI_NETNS")
|
||||
delete(environment, "CNI_IFNAME")
|
||||
delete(environment, "CNI_PATH")
|
||||
})
|
||||
|
||||
It("reports that all of them are missing, not just the first", func() {
|
||||
Expect(dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)).NotTo(Succeed())
|
||||
log := stderr.String()
|
||||
Expect(log).To(ContainSubstring("CNI_NETNS env variable missing\n"))
|
||||
Expect(log).To(ContainSubstring("CNI_IFNAME env variable missing\n"))
|
||||
Expect(log).To(ContainSubstring("CNI_PATH env variable missing\n"))
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
Context("when the stdin data is missing the required cniVersion config", func() {
|
||||
BeforeEach(func() {
|
||||
dispatch.Stdin = strings.NewReader(`{ "some": "config" }`)
|
||||
})
|
||||
|
||||
Context("when the plugin supports version 0.1.0", func() {
|
||||
BeforeEach(func() {
|
||||
versionInfo = version.PluginSupports("0.1.0")
|
||||
expectedCmdArgs.StdinData = []byte(`{ "some": "config" }`)
|
||||
})
|
||||
|
||||
It("infers the config is 0.1.0 and calls the cmdAdd callback", func() {
|
||||
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(cmdAdd.CallCount).To(Equal(1))
|
||||
Expect(cmdAdd.Received.CmdArgs).To(Equal(expectedCmdArgs))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when the plugin does not support 0.1.0", func() {
|
||||
BeforeEach(func() {
|
||||
versionInfo = version.PluginSupports("4.3.2")
|
||||
})
|
||||
|
||||
It("immediately returns a useful error", func() {
|
||||
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
|
||||
Expect(err.Code).To(Equal(types.ErrIncompatibleCNIVersion)) // see https://github.com/containernetworking/cni/blob/master/SPEC.md#well-known-error-codes
|
||||
Expect(err.Msg).To(Equal("incompatible CNI versions"))
|
||||
Expect(err.Details).To(Equal(`config is "0.1.0", plugin supports ["4.3.2"]`))
|
||||
})
|
||||
|
||||
It("does not call either callback", func() {
|
||||
dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
|
||||
Expect(cmdAdd.CallCount).To(Equal(0))
|
||||
Expect(cmdDel.CallCount).To(Equal(0))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Context("when the CNI_COMMAND is DEL", func() {
|
||||
BeforeEach(func() {
|
||||
environment["CNI_COMMAND"] = "DEL"
|
||||
})
|
||||
|
||||
It("calls cmdDel with the env vars and stdin data", func() {
|
||||
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(cmdDel.CallCount).To(Equal(1))
|
||||
Expect(cmdDel.Received.CmdArgs).To(Equal(expectedCmdArgs))
|
||||
})
|
||||
|
||||
It("does not call cmdAdd", func() {
|
||||
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(cmdAdd.CallCount).To(Equal(0))
|
||||
})
|
||||
|
||||
DescribeTable("required / optional env vars", envVarChecker,
|
||||
Entry("command", "CNI_COMMAND", true),
|
||||
Entry("container id", "CNI_CONTAINERID", false),
|
||||
Entry("net ns", "CNI_NETNS", false),
|
||||
Entry("if name", "CNI_IFNAME", true),
|
||||
Entry("args", "CNI_ARGS", false),
|
||||
Entry("path", "CNI_PATH", true),
|
||||
)
|
||||
})
|
||||
|
||||
Context("when the CNI_COMMAND is VERSION", func() {
|
||||
BeforeEach(func() {
|
||||
environment["CNI_COMMAND"] = "VERSION"
|
||||
})
|
||||
|
||||
It("prints the version to stdout", func() {
|
||||
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(stdout).To(MatchJSON(`{
|
||||
"cniVersion": "0.3.1",
|
||||
"supportedVersions": ["9.8.7"]
|
||||
}`))
|
||||
})
|
||||
|
||||
It("does not call cmdAdd or cmdDel", func() {
|
||||
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(cmdAdd.CallCount).To(Equal(0))
|
||||
Expect(cmdDel.CallCount).To(Equal(0))
|
||||
})
|
||||
|
||||
DescribeTable("VERSION does not need the usual env vars", envVarChecker,
|
||||
Entry("command", "CNI_COMMAND", true),
|
||||
Entry("container id", "CNI_CONTAINERID", false),
|
||||
Entry("net ns", "CNI_NETNS", false),
|
||||
Entry("if name", "CNI_IFNAME", false),
|
||||
Entry("args", "CNI_ARGS", false),
|
||||
Entry("path", "CNI_PATH", false),
|
||||
)
|
||||
|
||||
Context("when the stdin is empty", func() {
|
||||
BeforeEach(func() {
|
||||
dispatch.Stdin = strings.NewReader("")
|
||||
})
|
||||
|
||||
It("succeeds without error", func() {
|
||||
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(stdout).To(MatchJSON(`{
|
||||
"cniVersion": "0.3.1",
|
||||
"supportedVersions": ["9.8.7"]
|
||||
}`))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Context("when the CNI_COMMAND is unrecognized", func() {
|
||||
BeforeEach(func() {
|
||||
environment["CNI_COMMAND"] = "NOPE"
|
||||
})
|
||||
|
||||
It("does not call any cmd callback", func() {
|
||||
dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
|
||||
|
||||
Expect(cmdAdd.CallCount).To(Equal(0))
|
||||
Expect(cmdDel.CallCount).To(Equal(0))
|
||||
})
|
||||
|
||||
It("returns an error", func() {
|
||||
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
|
||||
|
||||
Expect(err).To(Equal(&types.Error{
|
||||
Code: 100,
|
||||
Msg: "unknown CNI_COMMAND: NOPE",
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when stdin cannot be read", func() {
|
||||
BeforeEach(func() {
|
||||
dispatch.Stdin = &testutils.BadReader{}
|
||||
})
|
||||
|
||||
It("does not call any cmd callback", func() {
|
||||
dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
|
||||
|
||||
Expect(cmdAdd.CallCount).To(Equal(0))
|
||||
Expect(cmdDel.CallCount).To(Equal(0))
|
||||
})
|
||||
|
||||
It("wraps and returns the error", func() {
|
||||
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
|
||||
|
||||
Expect(err).To(Equal(&types.Error{
|
||||
Code: 100,
|
||||
Msg: "error reading from stdin: banana",
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when the callback returns an error", func() {
|
||||
Context("when it is a typed Error", func() {
|
||||
BeforeEach(func() {
|
||||
cmdAdd.Returns.Error = &types.Error{
|
||||
Code: 1234,
|
||||
Msg: "insufficient something",
|
||||
}
|
||||
})
|
||||
|
||||
It("returns the error as-is", func() {
|
||||
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
|
||||
|
||||
Expect(err).To(Equal(&types.Error{
|
||||
Code: 1234,
|
||||
Msg: "insufficient something",
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when it is an unknown error", func() {
|
||||
BeforeEach(func() {
|
||||
cmdAdd.Returns.Error = errors.New("potato")
|
||||
})
|
||||
|
||||
It("wraps and returns the error", func() {
|
||||
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
|
||||
|
||||
Expect(err).To(Equal(&types.Error{
|
||||
Code: 100,
|
||||
Msg: "potato",
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
33
pkg/testutils/bad_reader.go
Normal file
33
pkg/testutils/bad_reader.go
Normal file
@ -0,0 +1,33 @@
|
||||
// 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.
|
||||
|
||||
package testutils
|
||||
|
||||
import "errors"
|
||||
|
||||
// BadReader is an io.Reader which always errors
|
||||
type BadReader struct {
|
||||
Error error
|
||||
}
|
||||
|
||||
func (r *BadReader) Read(buffer []byte) (int, error) {
|
||||
if r.Error != nil {
|
||||
return 0, r.Error
|
||||
}
|
||||
return 0, errors.New("banana")
|
||||
}
|
||||
|
||||
func (r *BadReader) Close() error {
|
||||
return nil
|
||||
}
|
85
pkg/testutils/cmd.go
Normal file
85
pkg/testutils/cmd.go
Normal file
@ -0,0 +1,85 @@
|
||||
// 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.
|
||||
|
||||
package testutils
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
)
|
||||
|
||||
func envCleanup() {
|
||||
os.Unsetenv("CNI_COMMAND")
|
||||
os.Unsetenv("CNI_PATH")
|
||||
os.Unsetenv("CNI_NETNS")
|
||||
os.Unsetenv("CNI_IFNAME")
|
||||
}
|
||||
|
||||
func CmdAddWithResult(cniNetns, cniIfname string, conf []byte, f func() error) (types.Result, []byte, error) {
|
||||
os.Setenv("CNI_COMMAND", "ADD")
|
||||
os.Setenv("CNI_PATH", os.Getenv("PATH"))
|
||||
os.Setenv("CNI_NETNS", cniNetns)
|
||||
os.Setenv("CNI_IFNAME", cniIfname)
|
||||
defer envCleanup()
|
||||
|
||||
// Redirect stdout to capture plugin result
|
||||
oldStdout := os.Stdout
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
os.Stdout = w
|
||||
err = f()
|
||||
w.Close()
|
||||
|
||||
var out []byte
|
||||
if err == nil {
|
||||
out, err = ioutil.ReadAll(r)
|
||||
}
|
||||
os.Stdout = oldStdout
|
||||
|
||||
// Return errors after restoring stdout so Ginkgo will correctly
|
||||
// emit verbose error information on stdout
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Plugin must return result in same version as specified in netconf
|
||||
versionDecoder := &version.ConfigDecoder{}
|
||||
confVersion, err := versionDecoder.Decode(conf)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
result, err := version.NewResult(confVersion, out)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return result, out, nil
|
||||
}
|
||||
|
||||
func CmdDelWithResult(cniNetns, cniIfname string, f func() error) error {
|
||||
os.Setenv("CNI_COMMAND", "DEL")
|
||||
os.Setenv("CNI_PATH", os.Getenv("PATH"))
|
||||
os.Setenv("CNI_NETNS", cniNetns)
|
||||
os.Setenv("CNI_IFNAME", cniIfname)
|
||||
defer envCleanup()
|
||||
|
||||
return f()
|
||||
}
|
135
pkg/types/020/types.go
Normal file
135
pkg/types/020/types.go
Normal file
@ -0,0 +1,135 @@
|
||||
// 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.
|
||||
|
||||
package types020
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
)
|
||||
|
||||
const ImplementedSpecVersion string = "0.2.0"
|
||||
|
||||
var SupportedVersions = []string{"", "0.1.0", ImplementedSpecVersion}
|
||||
|
||||
// Compatibility types for CNI version 0.1.0 and 0.2.0
|
||||
|
||||
func NewResult(data []byte) (types.Result, error) {
|
||||
result := &Result{}
|
||||
if err := json.Unmarshal(data, result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func GetResult(r types.Result) (*Result, error) {
|
||||
// We expect version 0.1.0/0.2.0 results
|
||||
result020, err := r.GetAsVersion(ImplementedSpecVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, ok := result020.(*Result)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to convert result")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Result is what gets returned from the plugin (via stdout) to the caller
|
||||
type Result struct {
|
||||
CNIVersion string `json:"cniVersion,omitempty"`
|
||||
IP4 *IPConfig `json:"ip4,omitempty"`
|
||||
IP6 *IPConfig `json:"ip6,omitempty"`
|
||||
DNS types.DNS `json:"dns,omitempty"`
|
||||
}
|
||||
|
||||
func (r *Result) Version() string {
|
||||
return ImplementedSpecVersion
|
||||
}
|
||||
|
||||
func (r *Result) GetAsVersion(version string) (types.Result, error) {
|
||||
for _, supportedVersion := range SupportedVersions {
|
||||
if version == supportedVersion {
|
||||
r.CNIVersion = version
|
||||
return r, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("cannot convert version %q to %s", SupportedVersions, version)
|
||||
}
|
||||
|
||||
func (r *Result) Print() error {
|
||||
data, err := json.MarshalIndent(r, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = os.Stdout.Write(data)
|
||||
return err
|
||||
}
|
||||
|
||||
// String returns a formatted string in the form of "[IP4: $1,][ IP6: $2,] DNS: $3" where
|
||||
// $1 represents the receiver's IPv4, $2 represents the receiver's IPv6 and $3 the
|
||||
// receiver's DNS. If $1 or $2 are nil, they won't be present in the returned string.
|
||||
func (r *Result) String() string {
|
||||
var str string
|
||||
if r.IP4 != nil {
|
||||
str = fmt.Sprintf("IP4:%+v, ", *r.IP4)
|
||||
}
|
||||
if r.IP6 != nil {
|
||||
str += fmt.Sprintf("IP6:%+v, ", *r.IP6)
|
||||
}
|
||||
return fmt.Sprintf("%sDNS:%+v", str, r.DNS)
|
||||
}
|
||||
|
||||
// IPConfig contains values necessary to configure an interface
|
||||
type IPConfig struct {
|
||||
IP net.IPNet
|
||||
Gateway net.IP
|
||||
Routes []types.Route
|
||||
}
|
||||
|
||||
// net.IPNet is not JSON (un)marshallable so this duality is needed
|
||||
// for our custom IPNet type
|
||||
|
||||
// JSON (un)marshallable types
|
||||
type ipConfig struct {
|
||||
IP types.IPNet `json:"ip"`
|
||||
Gateway net.IP `json:"gateway,omitempty"`
|
||||
Routes []types.Route `json:"routes,omitempty"`
|
||||
}
|
||||
|
||||
func (c *IPConfig) MarshalJSON() ([]byte, error) {
|
||||
ipc := ipConfig{
|
||||
IP: types.IPNet(c.IP),
|
||||
Gateway: c.Gateway,
|
||||
Routes: c.Routes,
|
||||
}
|
||||
|
||||
return json.Marshal(ipc)
|
||||
}
|
||||
|
||||
func (c *IPConfig) UnmarshalJSON(data []byte) error {
|
||||
ipc := ipConfig{}
|
||||
if err := json.Unmarshal(data, &ipc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.IP = net.IPNet(ipc.IP)
|
||||
c.Gateway = ipc.Gateway
|
||||
c.Routes = ipc.Routes
|
||||
return nil
|
||||
}
|
27
pkg/types/020/types_suite_test.go
Normal file
27
pkg/types/020/types_suite_test.go
Normal file
@ -0,0 +1,27 @@
|
||||
// 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.
|
||||
|
||||
package types020_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTypes010(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "0.1.0/0.2.0 Types Suite")
|
||||
}
|
130
pkg/types/020/types_test.go
Normal file
130
pkg/types/020/types_test.go
Normal file
@ -0,0 +1,130 @@
|
||||
// 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.
|
||||
|
||||
package types020_test
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/types/020"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Ensures compatibility with the 0.1.0/0.2.0 spec", func() {
|
||||
It("correctly encodes a 0.1.0/0.2.0 Result", func() {
|
||||
ipv4, err := types.ParseCIDR("1.2.3.30/24")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(ipv4).NotTo(BeNil())
|
||||
|
||||
routegwv4, routev4, err := net.ParseCIDR("15.5.6.8/24")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(routev4).NotTo(BeNil())
|
||||
Expect(routegwv4).NotTo(BeNil())
|
||||
|
||||
ipv6, err := types.ParseCIDR("abcd:1234:ffff::cdde/64")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(ipv6).NotTo(BeNil())
|
||||
|
||||
routegwv6, routev6, err := net.ParseCIDR("1111:dddd::aaaa/80")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(routev6).NotTo(BeNil())
|
||||
Expect(routegwv6).NotTo(BeNil())
|
||||
|
||||
// Set every field of the struct to ensure source compatibility
|
||||
res := types020.Result{
|
||||
CNIVersion: types020.ImplementedSpecVersion,
|
||||
IP4: &types020.IPConfig{
|
||||
IP: *ipv4,
|
||||
Gateway: net.ParseIP("1.2.3.1"),
|
||||
Routes: []types.Route{
|
||||
{Dst: *routev4, GW: routegwv4},
|
||||
},
|
||||
},
|
||||
IP6: &types020.IPConfig{
|
||||
IP: *ipv6,
|
||||
Gateway: net.ParseIP("abcd:1234:ffff::1"),
|
||||
Routes: []types.Route{
|
||||
{Dst: *routev6, GW: routegwv6},
|
||||
},
|
||||
},
|
||||
DNS: types.DNS{
|
||||
Nameservers: []string{"1.2.3.4", "1::cafe"},
|
||||
Domain: "acompany.com",
|
||||
Search: []string{"somedomain.com", "otherdomain.net"},
|
||||
Options: []string{"foo", "bar"},
|
||||
},
|
||||
}
|
||||
|
||||
Expect(res.String()).To(Equal("IP4:{IP:{IP:1.2.3.30 Mask:ffffff00} Gateway:1.2.3.1 Routes:[{Dst:{IP:15.5.6.0 Mask:ffffff00} GW:15.5.6.8}]}, IP6:{IP:{IP:abcd:1234:ffff::cdde Mask:ffffffffffffffff0000000000000000} Gateway:abcd:1234:ffff::1 Routes:[{Dst:{IP:1111:dddd:: Mask:ffffffffffffffffffff000000000000} GW:1111:dddd::aaaa}]}, DNS:{Nameservers:[1.2.3.4 1::cafe] Domain:acompany.com Search:[somedomain.com otherdomain.net] Options:[foo bar]}"))
|
||||
|
||||
// Redirect stdout to capture JSON result
|
||||
oldStdout := os.Stdout
|
||||
r, w, err := os.Pipe()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
os.Stdout = w
|
||||
err = res.Print()
|
||||
w.Close()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// parse the result
|
||||
out, err := ioutil.ReadAll(r)
|
||||
os.Stdout = oldStdout
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(string(out)).To(Equal(`{
|
||||
"cniVersion": "0.2.0",
|
||||
"ip4": {
|
||||
"ip": "1.2.3.30/24",
|
||||
"gateway": "1.2.3.1",
|
||||
"routes": [
|
||||
{
|
||||
"dst": "15.5.6.0/24",
|
||||
"gw": "15.5.6.8"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ip6": {
|
||||
"ip": "abcd:1234:ffff::cdde/64",
|
||||
"gateway": "abcd:1234:ffff::1",
|
||||
"routes": [
|
||||
{
|
||||
"dst": "1111:dddd::/80",
|
||||
"gw": "1111:dddd::aaaa"
|
||||
}
|
||||
]
|
||||
},
|
||||
"dns": {
|
||||
"nameservers": [
|
||||
"1.2.3.4",
|
||||
"1::cafe"
|
||||
],
|
||||
"domain": "acompany.com",
|
||||
"search": [
|
||||
"somedomain.com",
|
||||
"otherdomain.net"
|
||||
],
|
||||
"options": [
|
||||
"foo",
|
||||
"bar"
|
||||
]
|
||||
}
|
||||
}`))
|
||||
})
|
||||
})
|
101
pkg/types/args.go
Normal file
101
pkg/types/args.go
Normal file
@ -0,0 +1,101 @@
|
||||
// Copyright 2015 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.
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// UnmarshallableBool typedef for builtin bool
|
||||
// because builtin type's methods can't be declared
|
||||
type UnmarshallableBool bool
|
||||
|
||||
// UnmarshalText implements the encoding.TextUnmarshaler interface.
|
||||
// Returns boolean true if the string is "1" or "[Tt]rue"
|
||||
// Returns boolean false if the string is "0" or "[Ff]alse"
|
||||
func (b *UnmarshallableBool) UnmarshalText(data []byte) error {
|
||||
s := strings.ToLower(string(data))
|
||||
switch s {
|
||||
case "1", "true":
|
||||
*b = true
|
||||
case "0", "false":
|
||||
*b = false
|
||||
default:
|
||||
return fmt.Errorf("Boolean unmarshal error: invalid input %s", s)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshallableString typedef for builtin string
|
||||
type UnmarshallableString string
|
||||
|
||||
// UnmarshalText implements the encoding.TextUnmarshaler interface.
|
||||
// Returns the string
|
||||
func (s *UnmarshallableString) UnmarshalText(data []byte) error {
|
||||
*s = UnmarshallableString(data)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CommonArgs contains the IgnoreUnknown argument
|
||||
// and must be embedded by all Arg structs
|
||||
type CommonArgs struct {
|
||||
IgnoreUnknown UnmarshallableBool `json:"ignoreunknown,omitempty"`
|
||||
}
|
||||
|
||||
// GetKeyField is a helper function to receive Values
|
||||
// Values that represent a pointer to a struct
|
||||
func GetKeyField(keyString string, v reflect.Value) reflect.Value {
|
||||
return v.Elem().FieldByName(keyString)
|
||||
}
|
||||
|
||||
// LoadArgs parses args from a string in the form "K=V;K2=V2;..."
|
||||
func LoadArgs(args string, container interface{}) error {
|
||||
if args == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
containerValue := reflect.ValueOf(container)
|
||||
|
||||
pairs := strings.Split(args, ";")
|
||||
unknownArgs := []string{}
|
||||
for _, pair := range pairs {
|
||||
kv := strings.Split(pair, "=")
|
||||
if len(kv) != 2 {
|
||||
return fmt.Errorf("ARGS: invalid pair %q", pair)
|
||||
}
|
||||
keyString := kv[0]
|
||||
valueString := kv[1]
|
||||
keyField := GetKeyField(keyString, containerValue)
|
||||
if !keyField.IsValid() {
|
||||
unknownArgs = append(unknownArgs, pair)
|
||||
continue
|
||||
}
|
||||
|
||||
u := keyField.Addr().Interface().(encoding.TextUnmarshaler)
|
||||
err := u.UnmarshalText([]byte(valueString))
|
||||
if err != nil {
|
||||
return fmt.Errorf("ARGS: error parsing value of pair %q: %v)", pair, err)
|
||||
}
|
||||
}
|
||||
|
||||
isIgnoreUnknown := GetKeyField("IgnoreUnknown", containerValue).Bool()
|
||||
if len(unknownArgs) > 0 && !isIgnoreUnknown {
|
||||
return fmt.Errorf("ARGS: unknown args %q", unknownArgs)
|
||||
}
|
||||
return nil
|
||||
}
|
121
pkg/types/args_test.go
Normal file
121
pkg/types/args_test.go
Normal file
@ -0,0 +1,121 @@
|
||||
// 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.
|
||||
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
. "github.com/containernetworking/cni/pkg/types"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/ginkgo/extensions/table"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("UnmarshallableBool UnmarshalText", func() {
|
||||
DescribeTable("string to bool detection should succeed in all cases",
|
||||
func(inputs []string, expected bool) {
|
||||
for _, s := range inputs {
|
||||
var ub UnmarshallableBool
|
||||
err := ub.UnmarshalText([]byte(s))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ub).To(Equal(UnmarshallableBool(expected)))
|
||||
}
|
||||
},
|
||||
Entry("parse to true", []string{"True", "true", "1"}, true),
|
||||
Entry("parse to false", []string{"False", "false", "0"}, false),
|
||||
)
|
||||
|
||||
Context("When passed an invalid value", func() {
|
||||
It("should result in an error", func() {
|
||||
var ub UnmarshallableBool
|
||||
err := ub.UnmarshalText([]byte("invalid"))
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("UnmarshallableString UnmarshalText", func() {
|
||||
DescribeTable("string to string detection should succeed in all cases",
|
||||
func(inputs []string, expected string) {
|
||||
for _, s := range inputs {
|
||||
var us UnmarshallableString
|
||||
err := us.UnmarshalText([]byte(s))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(string(us)).To(Equal(expected))
|
||||
}
|
||||
},
|
||||
Entry("parse empty string", []string{""}, ""),
|
||||
Entry("parse non-empty string", []string{"notempty"}, "notempty"),
|
||||
)
|
||||
})
|
||||
|
||||
var _ = Describe("GetKeyField", func() {
|
||||
type testcontainer struct {
|
||||
Valid string `json:"valid,omitempty"`
|
||||
}
|
||||
var (
|
||||
container = testcontainer{Valid: "valid"}
|
||||
containerInterface = func(i interface{}) interface{} { return i }(&container)
|
||||
containerValue = reflect.ValueOf(containerInterface)
|
||||
)
|
||||
Context("When a valid field is provided", func() {
|
||||
It("should return the correct field", func() {
|
||||
field := GetKeyField("Valid", containerValue)
|
||||
Expect(field.String()).To(Equal("valid"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("LoadArgs", func() {
|
||||
Context("When no arguments are passed", func() {
|
||||
It("LoadArgs should succeed", func() {
|
||||
err := LoadArgs("", struct{}{})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Context("When unknown arguments are passed and ignored", func() {
|
||||
It("LoadArgs should succeed", func() {
|
||||
ca := CommonArgs{}
|
||||
err := LoadArgs("IgnoreUnknown=True;Unk=nown", &ca)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Context("When unknown arguments are passed and not ignored", func() {
|
||||
It("LoadArgs should fail", func() {
|
||||
ca := CommonArgs{}
|
||||
err := LoadArgs("Unk=nown", &ca)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Context("When unknown arguments are passed and explicitly not ignored", func() {
|
||||
It("LoadArgs should fail", func() {
|
||||
ca := CommonArgs{}
|
||||
err := LoadArgs("IgnoreUnknown=0, Unk=nown", &ca)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Context("When known arguments are passed", func() {
|
||||
It("LoadArgs should succeed", func() {
|
||||
ca := CommonArgs{}
|
||||
err := LoadArgs("IgnoreUnknown=1", &ca)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
})
|
||||
})
|
296
pkg/types/current/types.go
Normal file
296
pkg/types/current/types.go
Normal file
@ -0,0 +1,296 @@
|
||||
// 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.
|
||||
|
||||
package current
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/types/020"
|
||||
)
|
||||
|
||||
const ImplementedSpecVersion string = "0.3.1"
|
||||
|
||||
var SupportedVersions = []string{"0.3.0", ImplementedSpecVersion}
|
||||
|
||||
func NewResult(data []byte) (types.Result, error) {
|
||||
result := &Result{}
|
||||
if err := json.Unmarshal(data, result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func GetResult(r types.Result) (*Result, error) {
|
||||
resultCurrent, err := r.GetAsVersion(ImplementedSpecVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, ok := resultCurrent.(*Result)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to convert result")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
var resultConverters = []struct {
|
||||
versions []string
|
||||
convert func(types.Result) (*Result, error)
|
||||
}{
|
||||
{types020.SupportedVersions, convertFrom020},
|
||||
{SupportedVersions, convertFrom030},
|
||||
}
|
||||
|
||||
func convertFrom020(result types.Result) (*Result, error) {
|
||||
oldResult, err := types020.GetResult(result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newResult := &Result{
|
||||
CNIVersion: ImplementedSpecVersion,
|
||||
DNS: oldResult.DNS,
|
||||
Routes: []*types.Route{},
|
||||
}
|
||||
|
||||
if oldResult.IP4 != nil {
|
||||
newResult.IPs = append(newResult.IPs, &IPConfig{
|
||||
Version: "4",
|
||||
Interface: -1,
|
||||
Address: oldResult.IP4.IP,
|
||||
Gateway: oldResult.IP4.Gateway,
|
||||
})
|
||||
for _, route := range oldResult.IP4.Routes {
|
||||
gw := route.GW
|
||||
if gw == nil {
|
||||
gw = oldResult.IP4.Gateway
|
||||
}
|
||||
newResult.Routes = append(newResult.Routes, &types.Route{
|
||||
Dst: route.Dst,
|
||||
GW: gw,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if oldResult.IP6 != nil {
|
||||
newResult.IPs = append(newResult.IPs, &IPConfig{
|
||||
Version: "6",
|
||||
Interface: -1,
|
||||
Address: oldResult.IP6.IP,
|
||||
Gateway: oldResult.IP6.Gateway,
|
||||
})
|
||||
for _, route := range oldResult.IP6.Routes {
|
||||
gw := route.GW
|
||||
if gw == nil {
|
||||
gw = oldResult.IP6.Gateway
|
||||
}
|
||||
newResult.Routes = append(newResult.Routes, &types.Route{
|
||||
Dst: route.Dst,
|
||||
GW: gw,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(newResult.IPs) == 0 {
|
||||
return nil, fmt.Errorf("cannot convert: no valid IP addresses")
|
||||
}
|
||||
|
||||
return newResult, nil
|
||||
}
|
||||
|
||||
func convertFrom030(result types.Result) (*Result, error) {
|
||||
newResult, ok := result.(*Result)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to convert result")
|
||||
}
|
||||
newResult.CNIVersion = ImplementedSpecVersion
|
||||
return newResult, nil
|
||||
}
|
||||
|
||||
func NewResultFromResult(result types.Result) (*Result, error) {
|
||||
version := result.Version()
|
||||
for _, converter := range resultConverters {
|
||||
for _, supportedVersion := range converter.versions {
|
||||
if version == supportedVersion {
|
||||
return converter.convert(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("unsupported CNI result22 version %q", version)
|
||||
}
|
||||
|
||||
// Result is what gets returned from the plugin (via stdout) to the caller
|
||||
type Result struct {
|
||||
CNIVersion string `json:"cniVersion,omitempty"`
|
||||
Interfaces []*Interface `json:"interfaces,omitempty"`
|
||||
IPs []*IPConfig `json:"ips,omitempty"`
|
||||
Routes []*types.Route `json:"routes,omitempty"`
|
||||
DNS types.DNS `json:"dns,omitempty"`
|
||||
}
|
||||
|
||||
// Convert to the older 0.2.0 CNI spec Result type
|
||||
func (r *Result) convertTo020() (*types020.Result, error) {
|
||||
oldResult := &types020.Result{
|
||||
CNIVersion: types020.ImplementedSpecVersion,
|
||||
DNS: r.DNS,
|
||||
}
|
||||
|
||||
for _, ip := range r.IPs {
|
||||
// Only convert the first IP address of each version as 0.2.0
|
||||
// and earlier cannot handle multiple IP addresses
|
||||
if ip.Version == "4" && oldResult.IP4 == nil {
|
||||
oldResult.IP4 = &types020.IPConfig{
|
||||
IP: ip.Address,
|
||||
Gateway: ip.Gateway,
|
||||
}
|
||||
} else if ip.Version == "6" && oldResult.IP6 == nil {
|
||||
oldResult.IP6 = &types020.IPConfig{
|
||||
IP: ip.Address,
|
||||
Gateway: ip.Gateway,
|
||||
}
|
||||
}
|
||||
|
||||
if oldResult.IP4 != nil && oldResult.IP6 != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for _, route := range r.Routes {
|
||||
is4 := route.Dst.IP.To4() != nil
|
||||
if is4 && oldResult.IP4 != nil {
|
||||
oldResult.IP4.Routes = append(oldResult.IP4.Routes, types.Route{
|
||||
Dst: route.Dst,
|
||||
GW: route.GW,
|
||||
})
|
||||
} else if !is4 && oldResult.IP6 != nil {
|
||||
oldResult.IP6.Routes = append(oldResult.IP6.Routes, types.Route{
|
||||
Dst: route.Dst,
|
||||
GW: route.GW,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if oldResult.IP4 == nil && oldResult.IP6 == nil {
|
||||
return nil, fmt.Errorf("cannot convert: no valid IP addresses")
|
||||
}
|
||||
|
||||
return oldResult, nil
|
||||
}
|
||||
|
||||
func (r *Result) Version() string {
|
||||
return ImplementedSpecVersion
|
||||
}
|
||||
|
||||
func (r *Result) GetAsVersion(version string) (types.Result, error) {
|
||||
switch version {
|
||||
case "0.3.0", ImplementedSpecVersion:
|
||||
r.CNIVersion = version
|
||||
return r, nil
|
||||
case types020.SupportedVersions[0], types020.SupportedVersions[1], types020.SupportedVersions[2]:
|
||||
return r.convertTo020()
|
||||
}
|
||||
return nil, fmt.Errorf("cannot convert version 0.3.x to %q", version)
|
||||
}
|
||||
|
||||
func (r *Result) Print() error {
|
||||
data, err := json.MarshalIndent(r, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = os.Stdout.Write(data)
|
||||
return err
|
||||
}
|
||||
|
||||
// String returns a formatted string in the form of "[Interfaces: $1,][ IP: $2,] DNS: $3" where
|
||||
// $1 represents the receiver's Interfaces, $2 represents the receiver's IP addresses and $3 the
|
||||
// receiver's DNS. If $1 or $2 are nil, they won't be present in the returned string.
|
||||
func (r *Result) String() string {
|
||||
var str string
|
||||
if len(r.Interfaces) > 0 {
|
||||
str += fmt.Sprintf("Interfaces:%+v, ", r.Interfaces)
|
||||
}
|
||||
if len(r.IPs) > 0 {
|
||||
str += fmt.Sprintf("IP:%+v, ", r.IPs)
|
||||
}
|
||||
if len(r.Routes) > 0 {
|
||||
str += fmt.Sprintf("Routes:%+v, ", r.Routes)
|
||||
}
|
||||
return fmt.Sprintf("%sDNS:%+v", str, r.DNS)
|
||||
}
|
||||
|
||||
// Convert this old version result to the current CNI version result
|
||||
func (r *Result) Convert() (*Result, error) {
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Interface contains values about the created interfaces
|
||||
type Interface struct {
|
||||
Name string `json:"name"`
|
||||
Mac string `json:"mac,omitempty"`
|
||||
Sandbox string `json:"sandbox,omitempty"`
|
||||
}
|
||||
|
||||
func (i *Interface) String() string {
|
||||
return fmt.Sprintf("%+v", *i)
|
||||
}
|
||||
|
||||
// IPConfig contains values necessary to configure an IP address on an interface
|
||||
type IPConfig struct {
|
||||
// IP version, either "4" or "6"
|
||||
Version string
|
||||
// Index into Result structs Interfaces list
|
||||
Interface int
|
||||
Address net.IPNet
|
||||
Gateway net.IP
|
||||
}
|
||||
|
||||
func (i *IPConfig) String() string {
|
||||
return fmt.Sprintf("%+v", *i)
|
||||
}
|
||||
|
||||
// JSON (un)marshallable types
|
||||
type ipConfig struct {
|
||||
Version string `json:"version"`
|
||||
Interface int `json:"interface,omitempty"`
|
||||
Address types.IPNet `json:"address"`
|
||||
Gateway net.IP `json:"gateway,omitempty"`
|
||||
}
|
||||
|
||||
func (c *IPConfig) MarshalJSON() ([]byte, error) {
|
||||
ipc := ipConfig{
|
||||
Version: c.Version,
|
||||
Interface: c.Interface,
|
||||
Address: types.IPNet(c.Address),
|
||||
Gateway: c.Gateway,
|
||||
}
|
||||
|
||||
return json.Marshal(ipc)
|
||||
}
|
||||
|
||||
func (c *IPConfig) UnmarshalJSON(data []byte) error {
|
||||
ipc := ipConfig{}
|
||||
if err := json.Unmarshal(data, &ipc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Version = ipc.Version
|
||||
c.Interface = ipc.Interface
|
||||
c.Address = net.IPNet(ipc.Address)
|
||||
c.Gateway = ipc.Gateway
|
||||
return nil
|
||||
}
|
27
pkg/types/current/types_suite_test.go
Normal file
27
pkg/types/current/types_suite_test.go
Normal file
@ -0,0 +1,27 @@
|
||||
// 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.
|
||||
|
||||
package current_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTypesCurrent(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Current Types Suite")
|
||||
}
|
215
pkg/types/current/types_test.go
Normal file
215
pkg/types/current/types_test.go
Normal file
@ -0,0 +1,215 @@
|
||||
// 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.
|
||||
|
||||
package current_test
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func testResult() *current.Result {
|
||||
ipv4, err := types.ParseCIDR("1.2.3.30/24")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(ipv4).NotTo(BeNil())
|
||||
|
||||
routegwv4, routev4, err := net.ParseCIDR("15.5.6.8/24")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(routev4).NotTo(BeNil())
|
||||
Expect(routegwv4).NotTo(BeNil())
|
||||
|
||||
ipv6, err := types.ParseCIDR("abcd:1234:ffff::cdde/64")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(ipv6).NotTo(BeNil())
|
||||
|
||||
routegwv6, routev6, err := net.ParseCIDR("1111:dddd::aaaa/80")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(routev6).NotTo(BeNil())
|
||||
Expect(routegwv6).NotTo(BeNil())
|
||||
|
||||
// Set every field of the struct to ensure source compatibility
|
||||
return ¤t.Result{
|
||||
CNIVersion: "0.3.1",
|
||||
Interfaces: []*current.Interface{
|
||||
{
|
||||
Name: "eth0",
|
||||
Mac: "00:11:22:33:44:55",
|
||||
Sandbox: "/proc/3553/ns/net",
|
||||
},
|
||||
},
|
||||
IPs: []*current.IPConfig{
|
||||
{
|
||||
Version: "4",
|
||||
Interface: 0,
|
||||
Address: *ipv4,
|
||||
Gateway: net.ParseIP("1.2.3.1"),
|
||||
},
|
||||
{
|
||||
Version: "6",
|
||||
Interface: 0,
|
||||
Address: *ipv6,
|
||||
Gateway: net.ParseIP("abcd:1234:ffff::1"),
|
||||
},
|
||||
},
|
||||
Routes: []*types.Route{
|
||||
{Dst: *routev4, GW: routegwv4},
|
||||
{Dst: *routev6, GW: routegwv6},
|
||||
},
|
||||
DNS: types.DNS{
|
||||
Nameservers: []string{"1.2.3.4", "1::cafe"},
|
||||
Domain: "acompany.com",
|
||||
Search: []string{"somedomain.com", "otherdomain.net"},
|
||||
Options: []string{"foo", "bar"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var _ = Describe("Current types operations", func() {
|
||||
It("correctly encodes a 0.3.x Result", func() {
|
||||
res := testResult()
|
||||
|
||||
Expect(res.String()).To(Equal("Interfaces:[{Name:eth0 Mac:00:11:22:33:44:55 Sandbox:/proc/3553/ns/net}], IP:[{Version:4 Interface:0 Address:{IP:1.2.3.30 Mask:ffffff00} Gateway:1.2.3.1} {Version:6 Interface:0 Address:{IP:abcd:1234:ffff::cdde Mask:ffffffffffffffff0000000000000000} Gateway:abcd:1234:ffff::1}], Routes:[{Dst:{IP:15.5.6.0 Mask:ffffff00} GW:15.5.6.8} {Dst:{IP:1111:dddd:: Mask:ffffffffffffffffffff000000000000} GW:1111:dddd::aaaa}], DNS:{Nameservers:[1.2.3.4 1::cafe] Domain:acompany.com Search:[somedomain.com otherdomain.net] Options:[foo bar]}"))
|
||||
|
||||
// Redirect stdout to capture JSON result
|
||||
oldStdout := os.Stdout
|
||||
r, w, err := os.Pipe()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
os.Stdout = w
|
||||
err = res.Print()
|
||||
w.Close()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// parse the result
|
||||
out, err := ioutil.ReadAll(r)
|
||||
os.Stdout = oldStdout
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(string(out)).To(Equal(`{
|
||||
"cniVersion": "0.3.1",
|
||||
"interfaces": [
|
||||
{
|
||||
"name": "eth0",
|
||||
"mac": "00:11:22:33:44:55",
|
||||
"sandbox": "/proc/3553/ns/net"
|
||||
}
|
||||
],
|
||||
"ips": [
|
||||
{
|
||||
"version": "4",
|
||||
"address": "1.2.3.30/24",
|
||||
"gateway": "1.2.3.1"
|
||||
},
|
||||
{
|
||||
"version": "6",
|
||||
"address": "abcd:1234:ffff::cdde/64",
|
||||
"gateway": "abcd:1234:ffff::1"
|
||||
}
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"dst": "15.5.6.0/24",
|
||||
"gw": "15.5.6.8"
|
||||
},
|
||||
{
|
||||
"dst": "1111:dddd::/80",
|
||||
"gw": "1111:dddd::aaaa"
|
||||
}
|
||||
],
|
||||
"dns": {
|
||||
"nameservers": [
|
||||
"1.2.3.4",
|
||||
"1::cafe"
|
||||
],
|
||||
"domain": "acompany.com",
|
||||
"search": [
|
||||
"somedomain.com",
|
||||
"otherdomain.net"
|
||||
],
|
||||
"options": [
|
||||
"foo",
|
||||
"bar"
|
||||
]
|
||||
}
|
||||
}`))
|
||||
})
|
||||
|
||||
It("correctly encodes a 0.1.0 Result", func() {
|
||||
res, err := testResult().GetAsVersion("0.1.0")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(res.String()).To(Equal("IP4:{IP:{IP:1.2.3.30 Mask:ffffff00} Gateway:1.2.3.1 Routes:[{Dst:{IP:15.5.6.0 Mask:ffffff00} GW:15.5.6.8}]}, IP6:{IP:{IP:abcd:1234:ffff::cdde Mask:ffffffffffffffff0000000000000000} Gateway:abcd:1234:ffff::1 Routes:[{Dst:{IP:1111:dddd:: Mask:ffffffffffffffffffff000000000000} GW:1111:dddd::aaaa}]}, DNS:{Nameservers:[1.2.3.4 1::cafe] Domain:acompany.com Search:[somedomain.com otherdomain.net] Options:[foo bar]}"))
|
||||
|
||||
// Redirect stdout to capture JSON result
|
||||
oldStdout := os.Stdout
|
||||
r, w, err := os.Pipe()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
os.Stdout = w
|
||||
err = res.Print()
|
||||
w.Close()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// parse the result
|
||||
out, err := ioutil.ReadAll(r)
|
||||
os.Stdout = oldStdout
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(string(out)).To(Equal(`{
|
||||
"cniVersion": "0.2.0",
|
||||
"ip4": {
|
||||
"ip": "1.2.3.30/24",
|
||||
"gateway": "1.2.3.1",
|
||||
"routes": [
|
||||
{
|
||||
"dst": "15.5.6.0/24",
|
||||
"gw": "15.5.6.8"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ip6": {
|
||||
"ip": "abcd:1234:ffff::cdde/64",
|
||||
"gateway": "abcd:1234:ffff::1",
|
||||
"routes": [
|
||||
{
|
||||
"dst": "1111:dddd::/80",
|
||||
"gw": "1111:dddd::aaaa"
|
||||
}
|
||||
]
|
||||
},
|
||||
"dns": {
|
||||
"nameservers": [
|
||||
"1.2.3.4",
|
||||
"1::cafe"
|
||||
],
|
||||
"domain": "acompany.com",
|
||||
"search": [
|
||||
"somedomain.com",
|
||||
"otherdomain.net"
|
||||
],
|
||||
"options": [
|
||||
"foo",
|
||||
"bar"
|
||||
]
|
||||
}
|
||||
}`))
|
||||
})
|
||||
})
|
185
pkg/types/types.go
Normal file
185
pkg/types/types.go
Normal file
@ -0,0 +1,185 @@
|
||||
// Copyright 2015 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.
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
)
|
||||
|
||||
// like net.IPNet but adds JSON marshalling and unmarshalling
|
||||
type IPNet net.IPNet
|
||||
|
||||
// ParseCIDR takes a string like "10.2.3.1/24" and
|
||||
// return IPNet with "10.2.3.1" and /24 mask
|
||||
func ParseCIDR(s string) (*net.IPNet, error) {
|
||||
ip, ipn, err := net.ParseCIDR(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ipn.IP = ip
|
||||
return ipn, nil
|
||||
}
|
||||
|
||||
func (n IPNet) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal((*net.IPNet)(&n).String())
|
||||
}
|
||||
|
||||
func (n *IPNet) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmp, err := ParseCIDR(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*n = IPNet(*tmp)
|
||||
return nil
|
||||
}
|
||||
|
||||
// NetConf describes a network.
|
||||
type NetConf struct {
|
||||
CNIVersion string `json:"cniVersion,omitempty"`
|
||||
|
||||
Name string `json:"name,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Capabilities map[string]bool `json:"capabilities,omitempty"`
|
||||
IPAM struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
} `json:"ipam,omitempty"`
|
||||
DNS DNS `json:"dns"`
|
||||
}
|
||||
|
||||
// NetConfList describes an ordered list of networks.
|
||||
type NetConfList struct {
|
||||
CNIVersion string `json:"cniVersion,omitempty"`
|
||||
|
||||
Name string `json:"name,omitempty"`
|
||||
Plugins []*NetConf `json:"plugins,omitempty"`
|
||||
}
|
||||
|
||||
type ResultFactoryFunc func([]byte) (Result, error)
|
||||
|
||||
// Result is an interface that provides the result of plugin execution
|
||||
type Result interface {
|
||||
// The highest CNI specification result verison the result supports
|
||||
// without having to convert
|
||||
Version() string
|
||||
|
||||
// Returns the result converted into the requested CNI specification
|
||||
// result version, or an error if conversion failed
|
||||
GetAsVersion(version string) (Result, error)
|
||||
|
||||
// Prints the result in JSON format to stdout
|
||||
Print() error
|
||||
|
||||
// Returns a JSON string representation of the result
|
||||
String() string
|
||||
}
|
||||
|
||||
func PrintResult(result Result, version string) error {
|
||||
newResult, err := result.GetAsVersion(version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return newResult.Print()
|
||||
}
|
||||
|
||||
// DNS contains values interesting for DNS resolvers
|
||||
type DNS struct {
|
||||
Nameservers []string `json:"nameservers,omitempty"`
|
||||
Domain string `json:"domain,omitempty"`
|
||||
Search []string `json:"search,omitempty"`
|
||||
Options []string `json:"options,omitempty"`
|
||||
}
|
||||
|
||||
type Route struct {
|
||||
Dst net.IPNet
|
||||
GW net.IP
|
||||
}
|
||||
|
||||
func (r *Route) String() string {
|
||||
return fmt.Sprintf("%+v", *r)
|
||||
}
|
||||
|
||||
// Well known error codes
|
||||
// see https://github.com/containernetworking/cni/blob/master/SPEC.md#well-known-error-codes
|
||||
const (
|
||||
ErrUnknown uint = iota // 0
|
||||
ErrIncompatibleCNIVersion // 1
|
||||
ErrUnsupportedField // 2
|
||||
)
|
||||
|
||||
type Error struct {
|
||||
Code uint `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Details string `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
return e.Msg
|
||||
}
|
||||
|
||||
func (e *Error) Print() error {
|
||||
return prettyPrint(e)
|
||||
}
|
||||
|
||||
// net.IPNet is not JSON (un)marshallable so this duality is needed
|
||||
// for our custom IPNet type
|
||||
|
||||
// JSON (un)marshallable types
|
||||
type route struct {
|
||||
Dst IPNet `json:"dst"`
|
||||
GW net.IP `json:"gw,omitempty"`
|
||||
}
|
||||
|
||||
func (r *Route) UnmarshalJSON(data []byte) error {
|
||||
rt := route{}
|
||||
if err := json.Unmarshal(data, &rt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.Dst = net.IPNet(rt.Dst)
|
||||
r.GW = rt.GW
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Route) MarshalJSON() ([]byte, error) {
|
||||
rt := route{
|
||||
Dst: IPNet(r.Dst),
|
||||
GW: r.GW,
|
||||
}
|
||||
|
||||
return json.Marshal(rt)
|
||||
}
|
||||
|
||||
func prettyPrint(obj interface{}) error {
|
||||
data, err := json.MarshalIndent(obj, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = os.Stdout.Write(data)
|
||||
return err
|
||||
}
|
||||
|
||||
// NotImplementedError is used to indicate that a method is not implemented for the given platform
|
||||
var NotImplementedError = errors.New("Not Implemented")
|
27
pkg/types/types_suite_test.go
Normal file
27
pkg/types/types_suite_test.go
Normal file
@ -0,0 +1,27 @@
|
||||
// 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.
|
||||
|
||||
package types_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTypes(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Types Suite")
|
||||
}
|
63
pkg/utils/hwaddr/hwaddr.go
Normal file
63
pkg/utils/hwaddr/hwaddr.go
Normal file
@ -0,0 +1,63 @@
|
||||
// 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.
|
||||
|
||||
package hwaddr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
const (
|
||||
ipRelevantByteLen = 4
|
||||
PrivateMACPrefixString = "0a:58"
|
||||
)
|
||||
|
||||
var (
|
||||
// private mac prefix safe to use
|
||||
PrivateMACPrefix = []byte{0x0a, 0x58}
|
||||
)
|
||||
|
||||
type SupportIp4OnlyErr struct{ msg string }
|
||||
|
||||
func (e SupportIp4OnlyErr) Error() string { return e.msg }
|
||||
|
||||
type MacParseErr struct{ msg string }
|
||||
|
||||
func (e MacParseErr) Error() string { return e.msg }
|
||||
|
||||
type InvalidPrefixLengthErr struct{ msg string }
|
||||
|
||||
func (e InvalidPrefixLengthErr) Error() string { return e.msg }
|
||||
|
||||
// GenerateHardwareAddr4 generates 48 bit virtual mac addresses based on the IP4 input.
|
||||
func GenerateHardwareAddr4(ip net.IP, prefix []byte) (net.HardwareAddr, error) {
|
||||
switch {
|
||||
|
||||
case ip.To4() == nil:
|
||||
return nil, SupportIp4OnlyErr{msg: "GenerateHardwareAddr4 only supports valid IPv4 address as input"}
|
||||
|
||||
case len(prefix) != len(PrivateMACPrefix):
|
||||
return nil, InvalidPrefixLengthErr{msg: fmt.Sprintf(
|
||||
"Prefix has length %d instead of %d", len(prefix), len(PrivateMACPrefix)),
|
||||
}
|
||||
}
|
||||
|
||||
ipByteLen := len(ip)
|
||||
return (net.HardwareAddr)(
|
||||
append(
|
||||
prefix,
|
||||
ip[ipByteLen-ipRelevantByteLen:ipByteLen]...),
|
||||
), nil
|
||||
}
|
27
pkg/utils/hwaddr/hwaddr_suite_test.go
Normal file
27
pkg/utils/hwaddr/hwaddr_suite_test.go
Normal file
@ -0,0 +1,27 @@
|
||||
// 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.
|
||||
|
||||
package hwaddr_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHwaddr(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Hwaddr Suite")
|
||||
}
|
74
pkg/utils/hwaddr/hwaddr_test.go
Normal file
74
pkg/utils/hwaddr/hwaddr_test.go
Normal file
@ -0,0 +1,74 @@
|
||||
// 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.
|
||||
|
||||
package hwaddr_test
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/utils/hwaddr"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Hwaddr", func() {
|
||||
Context("Generate Hardware Address", func() {
|
||||
It("generate hardware address based on ipv4 address", func() {
|
||||
testCases := []struct {
|
||||
ip net.IP
|
||||
expectedMAC net.HardwareAddr
|
||||
}{
|
||||
{
|
||||
ip: net.ParseIP("10.0.0.2"),
|
||||
expectedMAC: (net.HardwareAddr)(append(hwaddr.PrivateMACPrefix, 0x0a, 0x00, 0x00, 0x02)),
|
||||
},
|
||||
{
|
||||
ip: net.ParseIP("10.250.0.244"),
|
||||
expectedMAC: (net.HardwareAddr)(append(hwaddr.PrivateMACPrefix, 0x0a, 0xfa, 0x00, 0xf4)),
|
||||
},
|
||||
{
|
||||
ip: net.ParseIP("172.17.0.2"),
|
||||
expectedMAC: (net.HardwareAddr)(append(hwaddr.PrivateMACPrefix, 0xac, 0x11, 0x00, 0x02)),
|
||||
},
|
||||
{
|
||||
ip: net.IPv4(byte(172), byte(17), byte(0), byte(2)),
|
||||
expectedMAC: (net.HardwareAddr)(append(hwaddr.PrivateMACPrefix, 0xac, 0x11, 0x00, 0x02)),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
mac, err := hwaddr.GenerateHardwareAddr4(tc.ip, hwaddr.PrivateMACPrefix)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(mac).To(Equal(tc.expectedMAC))
|
||||
}
|
||||
})
|
||||
|
||||
It("return error if input is not ipv4 address", func() {
|
||||
testCases := []net.IP{
|
||||
net.ParseIP(""),
|
||||
net.ParseIP("2001:db8:0:1:1:1:1:1"),
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
_, err := hwaddr.GenerateHardwareAddr4(tc, hwaddr.PrivateMACPrefix)
|
||||
Expect(err).To(BeAssignableToTypeOf(hwaddr.SupportIp4OnlyErr{}))
|
||||
}
|
||||
})
|
||||
|
||||
It("return error if prefix is invalid", func() {
|
||||
_, err := hwaddr.GenerateHardwareAddr4(net.ParseIP("10.0.0.2"), []byte{0x58})
|
||||
Expect(err).To(BeAssignableToTypeOf(hwaddr.InvalidPrefixLengthErr{}))
|
||||
})
|
||||
})
|
||||
})
|
56
pkg/utils/sysctl/sysctl_linux.go
Normal file
56
pkg/utils/sysctl/sysctl_linux.go
Normal file
@ -0,0 +1,56 @@
|
||||
// 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.
|
||||
|
||||
package sysctl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Sysctl provides a method to set/get values from /proc/sys - in linux systems
|
||||
// new interface to set/get values of variables formerly handled by sysctl syscall
|
||||
// If optional `params` have only one string value - this function will
|
||||
// set this value into corresponding sysctl variable
|
||||
func Sysctl(name string, params ...string) (string, error) {
|
||||
if len(params) > 1 {
|
||||
return "", fmt.Errorf("unexcepted additional parameters")
|
||||
} else if len(params) == 1 {
|
||||
return setSysctl(name, params[0])
|
||||
}
|
||||
return getSysctl(name)
|
||||
}
|
||||
|
||||
func getSysctl(name string) (string, error) {
|
||||
fullName := filepath.Join("/proc/sys", strings.Replace(name, ".", "/", -1))
|
||||
fullName = filepath.Clean(fullName)
|
||||
data, err := ioutil.ReadFile(fullName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(data[:len(data)-1]), nil
|
||||
}
|
||||
|
||||
func setSysctl(name, value string) (string, error) {
|
||||
fullName := filepath.Join("/proc/sys", strings.Replace(name, ".", "/", -1))
|
||||
fullName = filepath.Clean(fullName)
|
||||
if err := ioutil.WriteFile(fullName, []byte(value), 0644); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return getSysctl(name)
|
||||
}
|
41
pkg/utils/utils.go
Normal file
41
pkg/utils/utils.go
Normal file
@ -0,0 +1,41 @@
|
||||
// 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.
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/sha512"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
maxChainLength = 28
|
||||
chainPrefix = "CNI-"
|
||||
prefixLength = len(chainPrefix)
|
||||
)
|
||||
|
||||
// Generates a chain name to be used with iptables.
|
||||
// Ensures that the generated chain name is exactly
|
||||
// maxChainLength chars in length
|
||||
func FormatChainName(name string, id string) string {
|
||||
chainBytes := sha512.Sum512([]byte(name + id))
|
||||
chain := fmt.Sprintf("%s%x", chainPrefix, chainBytes)
|
||||
return chain[:maxChainLength]
|
||||
}
|
||||
|
||||
// FormatComment returns a comment used for easier
|
||||
// rule identification within iptables.
|
||||
func FormatComment(name string, id string) string {
|
||||
return fmt.Sprintf("name: %q id: %q", name, id)
|
||||
}
|
27
pkg/utils/utils_suite_test.go
Normal file
27
pkg/utils/utils_suite_test.go
Normal file
@ -0,0 +1,27 @@
|
||||
// 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.
|
||||
|
||||
package utils_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUtils(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Utils Suite")
|
||||
}
|
51
pkg/utils/utils_test.go
Normal file
51
pkg/utils/utils_test.go
Normal file
@ -0,0 +1,51 @@
|
||||
// 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.
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Utils", func() {
|
||||
It("must format a short name", func() {
|
||||
chain := FormatChainName("test", "1234")
|
||||
Expect(len(chain)).To(Equal(maxChainLength))
|
||||
Expect(chain).To(Equal("CNI-2bbe0c48b91a7d1b8a6753a8"))
|
||||
})
|
||||
|
||||
It("must truncate a long name", func() {
|
||||
chain := FormatChainName("testalongnamethatdoesnotmakesense", "1234")
|
||||
Expect(len(chain)).To(Equal(maxChainLength))
|
||||
Expect(chain).To(Equal("CNI-374f33fe84ab0ed84dcdebe3"))
|
||||
})
|
||||
|
||||
It("must be predictable", func() {
|
||||
chain1 := FormatChainName("testalongnamethatdoesnotmakesense", "1234")
|
||||
chain2 := FormatChainName("testalongnamethatdoesnotmakesense", "1234")
|
||||
Expect(len(chain1)).To(Equal(maxChainLength))
|
||||
Expect(len(chain2)).To(Equal(maxChainLength))
|
||||
Expect(chain1).To(Equal(chain2))
|
||||
})
|
||||
|
||||
It("must change when a character changes", func() {
|
||||
chain1 := FormatChainName("testalongnamethatdoesnotmakesense", "1234")
|
||||
chain2 := FormatChainName("testalongnamethatdoesnotmakesense", "1235")
|
||||
Expect(len(chain1)).To(Equal(maxChainLength))
|
||||
Expect(len(chain2)).To(Equal(maxChainLength))
|
||||
Expect(chain1).To(Equal("CNI-374f33fe84ab0ed84dcdebe3"))
|
||||
Expect(chain1).NotTo(Equal(chain2))
|
||||
})
|
||||
})
|
37
pkg/version/conf.go
Normal file
37
pkg/version/conf.go
Normal file
@ -0,0 +1,37 @@
|
||||
// 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.
|
||||
|
||||
package version
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ConfigDecoder can decode the CNI version available in network config data
|
||||
type ConfigDecoder struct{}
|
||||
|
||||
func (*ConfigDecoder) Decode(jsonBytes []byte) (string, error) {
|
||||
var conf struct {
|
||||
CNIVersion string `json:"cniVersion"`
|
||||
}
|
||||
err := json.Unmarshal(jsonBytes, &conf)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("decoding version from network config: %s", err)
|
||||
}
|
||||
if conf.CNIVersion == "" {
|
||||
return "0.1.0", nil
|
||||
}
|
||||
return conf.CNIVersion, nil
|
||||
}
|
69
pkg/version/conf_test.go
Normal file
69
pkg/version/conf_test.go
Normal file
@ -0,0 +1,69 @@
|
||||
// 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.
|
||||
|
||||
package version_test
|
||||
|
||||
import (
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Decoding the version of network config", func() {
|
||||
var (
|
||||
decoder *version.ConfigDecoder
|
||||
configBytes []byte
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
decoder = &version.ConfigDecoder{}
|
||||
configBytes = []byte(`{ "cniVersion": "4.3.2" }`)
|
||||
})
|
||||
|
||||
Context("when the version is explict", func() {
|
||||
It("returns the version", func() {
|
||||
version, err := decoder.Decode(configBytes)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(version).To(Equal("4.3.2"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when the version is not present in the config", func() {
|
||||
BeforeEach(func() {
|
||||
configBytes = []byte(`{ "not-a-version-field": "foo" }`)
|
||||
})
|
||||
|
||||
It("assumes the config is version 0.1.0", func() {
|
||||
version, err := decoder.Decode(configBytes)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(version).To(Equal("0.1.0"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when the config data is malformed", func() {
|
||||
BeforeEach(func() {
|
||||
configBytes = []byte(`{{{`)
|
||||
})
|
||||
|
||||
It("returns a useful error", func() {
|
||||
_, err := decoder.Decode(configBytes)
|
||||
Expect(err).To(MatchError(HavePrefix(
|
||||
"decoding version from network config: invalid character",
|
||||
)))
|
||||
})
|
||||
})
|
||||
})
|
167
pkg/version/legacy_examples/example_runtime.go
Normal file
167
pkg/version/legacy_examples/example_runtime.go
Normal file
@ -0,0 +1,167 @@
|
||||
// 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.
|
||||
|
||||
package legacy_examples
|
||||
|
||||
// An ExampleRuntime is a small program that uses libcni to invoke a network plugin.
|
||||
// It should call ADD and DELETE, verifying all intermediate steps
|
||||
// and data structures.
|
||||
type ExampleRuntime struct {
|
||||
Example
|
||||
NetConfs []string // The network configuration names to pass
|
||||
}
|
||||
|
||||
// NetConfs are various versioned network configuration files. Examples should
|
||||
// specify which version they expect
|
||||
var NetConfs = map[string]string{
|
||||
"unversioned": `{
|
||||
"name": "default",
|
||||
"type": "ptp",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "10.1.2.0/24"
|
||||
}
|
||||
}`,
|
||||
"0.1.0": `{
|
||||
"cniVersion": "0.1.0",
|
||||
"name": "default",
|
||||
"type": "ptp",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "10.1.2.0/24"
|
||||
}
|
||||
}`,
|
||||
}
|
||||
|
||||
// V010_Runtime creates a simple ptp network configuration, then
|
||||
// executes libcni against the currently-built plugins.
|
||||
var V010_Runtime = ExampleRuntime{
|
||||
NetConfs: []string{"unversioned", "0.1.0"},
|
||||
Example: Example{
|
||||
Name: "example_invoker_v010",
|
||||
CNIRepoGitRef: "c0d34c69", //version with ns.Do
|
||||
PluginSource: `package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/ns"
|
||||
"github.com/containernetworking/cni/libcni"
|
||||
)
|
||||
|
||||
func main(){
|
||||
code := exec()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func exec() int {
|
||||
confBytes, err := ioutil.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
fmt.Printf("could not read netconfig from stdin: %+v", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
netConf, err := libcni.ConfFromBytes(confBytes)
|
||||
if err != nil {
|
||||
fmt.Printf("could not parse netconfig: %+v", err)
|
||||
return 1
|
||||
}
|
||||
fmt.Printf("Parsed network configuration: %+v\n", netConf.Network)
|
||||
|
||||
if len(os.Args) == 1 {
|
||||
fmt.Printf("Expect CNI plugin paths in argv")
|
||||
return 1
|
||||
}
|
||||
|
||||
targetNs, err := ns.NewNS()
|
||||
if err != nil {
|
||||
fmt.Printf("Could not create ns: %+v", err)
|
||||
return 1
|
||||
}
|
||||
defer targetNs.Close()
|
||||
|
||||
ifName := "eth0"
|
||||
|
||||
runtimeConf := &libcni.RuntimeConf{
|
||||
ContainerID: "some-container-id",
|
||||
NetNS: targetNs.Path(),
|
||||
IfName: ifName,
|
||||
}
|
||||
|
||||
cniConfig := &libcni.CNIConfig{Path: os.Args[1:]}
|
||||
|
||||
result, err := cniConfig.AddNetwork(netConf, runtimeConf)
|
||||
if err != nil {
|
||||
fmt.Printf("AddNetwork failed: %+v", err)
|
||||
return 2
|
||||
}
|
||||
fmt.Printf("AddNetwork result: %+v", result)
|
||||
|
||||
expectedIP := result.IP4.IP
|
||||
|
||||
err = targetNs.Do(func(ns.NetNS) error {
|
||||
netif, err := net.InterfaceByName(ifName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not retrieve interface: %v", err)
|
||||
}
|
||||
|
||||
addrs, err := netif.Addrs()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not retrieve addresses, %+v", err)
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, addr := range addrs {
|
||||
if addr.String() == expectedIP.String() {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return fmt.Errorf("Far-side link did not have expected address %s", expectedIP)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return 4
|
||||
}
|
||||
|
||||
err = cniConfig.DelNetwork(netConf, runtimeConf)
|
||||
if err != nil {
|
||||
fmt.Printf("DelNetwork failed: %v", err)
|
||||
return 5
|
||||
}
|
||||
|
||||
err = targetNs.Do(func(ns.NetNS) error {
|
||||
_, err := net.InterfaceByName(ifName)
|
||||
if err == nil {
|
||||
return fmt.Errorf("interface was not deleted")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return 6
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
`,
|
||||
},
|
||||
}
|
139
pkg/version/legacy_examples/examples.go
Normal file
139
pkg/version/legacy_examples/examples.go
Normal file
@ -0,0 +1,139 @@
|
||||
// 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.
|
||||
|
||||
// Package legacy_examples contains sample code from prior versions of
|
||||
// the CNI library, for use in verifying backwards compatibility.
|
||||
package legacy_examples
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/types/020"
|
||||
"github.com/containernetworking/cni/pkg/version/testhelpers"
|
||||
)
|
||||
|
||||
// An Example is a Git reference to the CNI repo and a Golang CNI plugin that
|
||||
// builds against that version of the repo.
|
||||
//
|
||||
// By convention, every Example plugin returns an ADD result that is
|
||||
// semantically equivalent to the ExpectedResult.
|
||||
type Example struct {
|
||||
Name string
|
||||
CNIRepoGitRef string
|
||||
PluginSource string
|
||||
}
|
||||
|
||||
var buildDir = ""
|
||||
var buildDirLock sync.Mutex
|
||||
|
||||
func ensureBuildDirExists() error {
|
||||
buildDirLock.Lock()
|
||||
defer buildDirLock.Unlock()
|
||||
|
||||
if buildDir != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
buildDir, err = ioutil.TempDir("", "cni-example-plugins")
|
||||
return err
|
||||
}
|
||||
|
||||
// Build builds the example, returning the path to the binary
|
||||
func (e Example) Build() (string, error) {
|
||||
if err := ensureBuildDirExists(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
outBinPath := filepath.Join(buildDir, e.Name)
|
||||
|
||||
if err := testhelpers.BuildAt([]byte(e.PluginSource), e.CNIRepoGitRef, outBinPath); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return outBinPath, nil
|
||||
}
|
||||
|
||||
// V010 acts like a CNI plugin from the v0.1.0 era
|
||||
var V010 = Example{
|
||||
Name: "example_v010",
|
||||
CNIRepoGitRef: "2c482f4",
|
||||
PluginSource: `package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/skel"
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
)
|
||||
|
||||
var result = types.Result{
|
||||
IP4: &types.IPConfig{
|
||||
IP: net.IPNet{
|
||||
IP: net.ParseIP("10.1.2.3"),
|
||||
Mask: net.CIDRMask(24, 32),
|
||||
},
|
||||
Gateway: net.ParseIP("10.1.2.1"),
|
||||
Routes: []types.Route{
|
||||
types.Route{
|
||||
Dst: net.IPNet{
|
||||
IP: net.ParseIP("0.0.0.0"),
|
||||
Mask: net.CIDRMask(0, 32),
|
||||
},
|
||||
GW: net.ParseIP("10.1.0.1"),
|
||||
},
|
||||
},
|
||||
},
|
||||
DNS: types.DNS{
|
||||
Nameservers: []string{"8.8.8.8"},
|
||||
Domain: "example.com",
|
||||
},
|
||||
}
|
||||
|
||||
func c(_ *skel.CmdArgs) error { result.Print(); return nil }
|
||||
|
||||
func main() { skel.PluginMain(c, c) }
|
||||
`,
|
||||
}
|
||||
|
||||
// ExpectedResult is the current representation of the plugin result
|
||||
// that is expected from each of the examples.
|
||||
//
|
||||
// As we change the CNI spec, the Result type and this value may change.
|
||||
// The text of the example plugins should not.
|
||||
var ExpectedResult = &types020.Result{
|
||||
IP4: &types020.IPConfig{
|
||||
IP: net.IPNet{
|
||||
IP: net.ParseIP("10.1.2.3"),
|
||||
Mask: net.CIDRMask(24, 32),
|
||||
},
|
||||
Gateway: net.ParseIP("10.1.2.1"),
|
||||
Routes: []types.Route{
|
||||
types.Route{
|
||||
Dst: net.IPNet{
|
||||
IP: net.ParseIP("0.0.0.0"),
|
||||
Mask: net.CIDRMask(0, 32),
|
||||
},
|
||||
GW: net.ParseIP("10.1.0.1"),
|
||||
},
|
||||
},
|
||||
},
|
||||
DNS: types.DNS{
|
||||
Nameservers: []string{"8.8.8.8"},
|
||||
Domain: "example.com",
|
||||
},
|
||||
}
|
27
pkg/version/legacy_examples/legacy_examples_suite_test.go
Normal file
27
pkg/version/legacy_examples/legacy_examples_suite_test.go
Normal file
@ -0,0 +1,27 @@
|
||||
// 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.
|
||||
|
||||
package legacy_examples_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLegacyExamples(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "LegacyExamples Suite")
|
||||
}
|
36
pkg/version/legacy_examples/legacy_examples_test.go
Normal file
36
pkg/version/legacy_examples/legacy_examples_test.go
Normal file
@ -0,0 +1,36 @@
|
||||
// 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.
|
||||
|
||||
package legacy_examples_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/version/legacy_examples"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("The v0.1.0 Example", func() {
|
||||
It("builds ok", func() {
|
||||
example := legacy_examples.V010
|
||||
pluginPath, err := example.Build()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(filepath.Base(pluginPath)).To(Equal(example.Name))
|
||||
|
||||
Expect(os.RemoveAll(pluginPath)).To(Succeed())
|
||||
})
|
||||
})
|
81
pkg/version/plugin.go
Normal file
81
pkg/version/plugin.go
Normal file
@ -0,0 +1,81 @@
|
||||
// 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.
|
||||
|
||||
package version
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// PluginInfo reports information about CNI versioning
|
||||
type PluginInfo interface {
|
||||
// SupportedVersions returns one or more CNI spec versions that the plugin
|
||||
// supports. If input is provided in one of these versions, then the plugin
|
||||
// promises to use the same CNI version in its response
|
||||
SupportedVersions() []string
|
||||
|
||||
// Encode writes this CNI version information as JSON to the given Writer
|
||||
Encode(io.Writer) error
|
||||
}
|
||||
|
||||
type pluginInfo struct {
|
||||
CNIVersion_ string `json:"cniVersion"`
|
||||
SupportedVersions_ []string `json:"supportedVersions,omitempty"`
|
||||
}
|
||||
|
||||
// pluginInfo implements the PluginInfo interface
|
||||
var _ PluginInfo = &pluginInfo{}
|
||||
|
||||
func (p *pluginInfo) Encode(w io.Writer) error {
|
||||
return json.NewEncoder(w).Encode(p)
|
||||
}
|
||||
|
||||
func (p *pluginInfo) SupportedVersions() []string {
|
||||
return p.SupportedVersions_
|
||||
}
|
||||
|
||||
// PluginSupports returns a new PluginInfo that will report the given versions
|
||||
// as supported
|
||||
func PluginSupports(supportedVersions ...string) PluginInfo {
|
||||
if len(supportedVersions) < 1 {
|
||||
panic("programmer error: you must support at least one version")
|
||||
}
|
||||
return &pluginInfo{
|
||||
CNIVersion_: Current(),
|
||||
SupportedVersions_: supportedVersions,
|
||||
}
|
||||
}
|
||||
|
||||
// PluginDecoder can decode the response returned by a plugin's VERSION command
|
||||
type PluginDecoder struct{}
|
||||
|
||||
func (*PluginDecoder) Decode(jsonBytes []byte) (PluginInfo, error) {
|
||||
var info pluginInfo
|
||||
err := json.Unmarshal(jsonBytes, &info)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decoding version info: %s", err)
|
||||
}
|
||||
if info.CNIVersion_ == "" {
|
||||
return nil, fmt.Errorf("decoding version info: missing field cniVersion")
|
||||
}
|
||||
if len(info.SupportedVersions_) == 0 {
|
||||
if info.CNIVersion_ == "0.2.0" {
|
||||
return PluginSupports("0.1.0", "0.2.0"), nil
|
||||
}
|
||||
return nil, fmt.Errorf("decoding version info: missing field supportedVersions")
|
||||
}
|
||||
return &info, nil
|
||||
}
|
85
pkg/version/plugin_test.go
Normal file
85
pkg/version/plugin_test.go
Normal file
@ -0,0 +1,85 @@
|
||||
// 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.
|
||||
|
||||
package version_test
|
||||
|
||||
import (
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Decoding versions reported by a plugin", func() {
|
||||
var (
|
||||
decoder *version.PluginDecoder
|
||||
versionStdout []byte
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
decoder = &version.PluginDecoder{}
|
||||
versionStdout = []byte(`{
|
||||
"cniVersion": "some-library-version",
|
||||
"supportedVersions": [ "some-version", "some-other-version" ]
|
||||
}`)
|
||||
})
|
||||
|
||||
It("returns a PluginInfo that represents the given json bytes", func() {
|
||||
pluginInfo, err := decoder.Decode(versionStdout)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(pluginInfo).NotTo(BeNil())
|
||||
Expect(pluginInfo.SupportedVersions()).To(Equal([]string{
|
||||
"some-version",
|
||||
"some-other-version",
|
||||
}))
|
||||
})
|
||||
|
||||
Context("when the bytes cannot be decoded as json", func() {
|
||||
BeforeEach(func() {
|
||||
versionStdout = []byte(`{{{`)
|
||||
})
|
||||
|
||||
It("returns a meaningful error", func() {
|
||||
_, err := decoder.Decode(versionStdout)
|
||||
Expect(err).To(MatchError("decoding version info: invalid character '{' looking for beginning of object key string"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when the json bytes are missing the required CNIVersion field", func() {
|
||||
BeforeEach(func() {
|
||||
versionStdout = []byte(`{ "supportedVersions": [ "foo" ] }`)
|
||||
})
|
||||
|
||||
It("returns a meaningful error", func() {
|
||||
_, err := decoder.Decode(versionStdout)
|
||||
Expect(err).To(MatchError("decoding version info: missing field cniVersion"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when there are no supported versions", func() {
|
||||
BeforeEach(func() {
|
||||
versionStdout = []byte(`{ "cniVersion": "0.2.0" }`)
|
||||
})
|
||||
|
||||
It("assumes that the supported versions are 0.1.0 and 0.2.0", func() {
|
||||
pluginInfo, err := decoder.Decode(versionStdout)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(pluginInfo).NotTo(BeNil())
|
||||
Expect(pluginInfo.SupportedVersions()).To(Equal([]string{
|
||||
"0.1.0",
|
||||
"0.2.0",
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
})
|
49
pkg/version/reconcile.go
Normal file
49
pkg/version/reconcile.go
Normal file
@ -0,0 +1,49 @@
|
||||
// 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.
|
||||
|
||||
package version
|
||||
|
||||
import "fmt"
|
||||
|
||||
type ErrorIncompatible struct {
|
||||
Config string
|
||||
Supported []string
|
||||
}
|
||||
|
||||
func (e *ErrorIncompatible) Details() string {
|
||||
return fmt.Sprintf("config is %q, plugin supports %q", e.Config, e.Supported)
|
||||
}
|
||||
|
||||
func (e *ErrorIncompatible) Error() string {
|
||||
return fmt.Sprintf("incompatible CNI versions: %s", e.Details())
|
||||
}
|
||||
|
||||
type Reconciler struct{}
|
||||
|
||||
func (r *Reconciler) Check(configVersion string, pluginInfo PluginInfo) *ErrorIncompatible {
|
||||
return r.CheckRaw(configVersion, pluginInfo.SupportedVersions())
|
||||
}
|
||||
|
||||
func (*Reconciler) CheckRaw(configVersion string, supportedVersions []string) *ErrorIncompatible {
|
||||
for _, supportedVersion := range supportedVersions {
|
||||
if configVersion == supportedVersion {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return &ErrorIncompatible{
|
||||
Config: configVersion,
|
||||
Supported: supportedVersions,
|
||||
}
|
||||
}
|
51
pkg/version/reconcile_test.go
Normal file
51
pkg/version/reconcile_test.go
Normal file
@ -0,0 +1,51 @@
|
||||
// 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.
|
||||
|
||||
package version_test
|
||||
|
||||
import (
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Reconcile versions of net config with versions supported by plugins", func() {
|
||||
var (
|
||||
reconciler *version.Reconciler
|
||||
pluginInfo version.PluginInfo
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
reconciler = &version.Reconciler{}
|
||||
pluginInfo = version.PluginSupports("1.2.3", "4.3.2")
|
||||
})
|
||||
|
||||
It("succeeds if the config version is supported by the plugin", func() {
|
||||
err := reconciler.Check("4.3.2", pluginInfo)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
Context("when the config version is not supported by the plugin", func() {
|
||||
It("returns a helpful error", func() {
|
||||
err := reconciler.Check("0.1.0", pluginInfo)
|
||||
|
||||
Expect(err).To(Equal(&version.ErrorIncompatible{
|
||||
Config: "0.1.0",
|
||||
Supported: []string{"1.2.3", "4.3.2"},
|
||||
}))
|
||||
|
||||
Expect(err.Error()).To(Equal(`incompatible CNI versions: config is "0.1.0", plugin supports ["1.2.3" "4.3.2"]`))
|
||||
})
|
||||
})
|
||||
})
|
156
pkg/version/testhelpers/testhelpers.go
Normal file
156
pkg/version/testhelpers/testhelpers.go
Normal file
@ -0,0 +1,156 @@
|
||||
// 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.
|
||||
|
||||
// Package testhelpers supports testing of CNI components of different versions
|
||||
//
|
||||
// For example, to build a plugin against an old version of the CNI library,
|
||||
// we can pass the plugin's source and the old git commit reference to BuildAt.
|
||||
// We could then test how the built binary responds when called by the latest
|
||||
// version of this library.
|
||||
package testhelpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const packageBaseName = "github.com/containernetworking/cni"
|
||||
|
||||
func run(cmd *exec.Cmd) error {
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
command := strings.Join(cmd.Args, " ")
|
||||
return fmt.Errorf("running %q: %s", command, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func goBuildEnviron(gopath string) []string {
|
||||
environ := os.Environ()
|
||||
for i, kvp := range environ {
|
||||
if strings.HasPrefix(kvp, "GOPATH=") {
|
||||
environ[i] = "GOPATH=" + gopath
|
||||
return environ
|
||||
}
|
||||
}
|
||||
environ = append(environ, "GOPATH="+gopath)
|
||||
return environ
|
||||
}
|
||||
|
||||
func buildGoProgram(gopath, packageName, outputFilePath string) error {
|
||||
cmd := exec.Command("go", "build", "-o", outputFilePath, packageName)
|
||||
cmd.Env = goBuildEnviron(gopath)
|
||||
return run(cmd)
|
||||
}
|
||||
|
||||
func createSingleFilePackage(gopath, packageName string, fileContents []byte) error {
|
||||
dirName := filepath.Join(gopath, "src", packageName)
|
||||
err := os.MkdirAll(dirName, 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(filepath.Join(dirName, "main.go"), fileContents, 0600)
|
||||
}
|
||||
|
||||
func removePackage(gopath, packageName string) error {
|
||||
dirName := filepath.Join(gopath, "src", packageName)
|
||||
return os.RemoveAll(dirName)
|
||||
}
|
||||
|
||||
func isRepoRoot(path string) bool {
|
||||
_, err := ioutil.ReadDir(filepath.Join(path, ".git"))
|
||||
return (err == nil) && (filepath.Base(path) == "cni")
|
||||
}
|
||||
|
||||
func LocateCurrentGitRepo() (string, error) {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
if isRepoRoot(dir) {
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
dir, err = filepath.Abs(filepath.Dir(dir))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("abs(dir(%q)): %s", dir, err)
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("unable to find cni repo root, landed at %q", dir)
|
||||
}
|
||||
|
||||
func gitCloneThisRepo(cloneDestination string) error {
|
||||
err := os.MkdirAll(cloneDestination, 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentGitRepo, err := LocateCurrentGitRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return run(exec.Command("git", "clone", currentGitRepo, cloneDestination))
|
||||
}
|
||||
|
||||
func gitCheckout(localRepo string, gitRef string) error {
|
||||
return run(exec.Command("git", "-C", localRepo, "checkout", gitRef))
|
||||
}
|
||||
|
||||
// BuildAt builds the go programSource using the version of the CNI library
|
||||
// at gitRef, and saves the resulting binary file at outputFilePath
|
||||
func BuildAt(programSource []byte, gitRef string, outputFilePath string) error {
|
||||
tempGoPath, err := ioutil.TempDir("", "cni-git-")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(tempGoPath)
|
||||
|
||||
cloneDestination := filepath.Join(tempGoPath, "src", packageBaseName)
|
||||
err = gitCloneThisRepo(cloneDestination)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = gitCheckout(cloneDestination, gitRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
testPackageName := fmt.Sprintf("test-package-%x", rand.Int31())
|
||||
|
||||
err = createSingleFilePackage(tempGoPath, testPackageName, programSource)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer removePackage(tempGoPath, testPackageName)
|
||||
|
||||
err = buildGoProgram(tempGoPath, testPackageName, outputFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
27
pkg/version/testhelpers/testhelpers_suite_test.go
Normal file
27
pkg/version/testhelpers/testhelpers_suite_test.go
Normal file
@ -0,0 +1,27 @@
|
||||
// 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.
|
||||
|
||||
package testhelpers_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTesthelpers(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Testhelpers Suite")
|
||||
}
|
106
pkg/version/testhelpers/testhelpers_test.go
Normal file
106
pkg/version/testhelpers/testhelpers_test.go
Normal file
@ -0,0 +1,106 @@
|
||||
// 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.
|
||||
package testhelpers_test
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/version/testhelpers"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("BuildAt", func() {
|
||||
var (
|
||||
gitRef string
|
||||
outputFilePath string
|
||||
outputDir string
|
||||
programSource []byte
|
||||
)
|
||||
BeforeEach(func() {
|
||||
programSource = []byte(`package main
|
||||
|
||||
import "github.com/containernetworking/cni/pkg/skel"
|
||||
|
||||
func c(_ *skel.CmdArgs) error { return nil }
|
||||
|
||||
func main() { skel.PluginMain(c, c) }
|
||||
`)
|
||||
gitRef = "f4364185253"
|
||||
|
||||
var err error
|
||||
outputDir, err = ioutil.TempDir("", "bin")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
outputFilePath = filepath.Join(outputDir, "some-binary")
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
Expect(os.RemoveAll(outputDir)).To(Succeed())
|
||||
})
|
||||
|
||||
It("builds the provided source code using the CNI library at the given git ref", func() {
|
||||
Expect(outputFilePath).NotTo(BeAnExistingFile())
|
||||
|
||||
err := testhelpers.BuildAt(programSource, gitRef, outputFilePath)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(outputFilePath).To(BeAnExistingFile())
|
||||
|
||||
cmd := exec.Command(outputFilePath)
|
||||
cmd.Env = []string{"CNI_COMMAND=VERSION"}
|
||||
output, err := cmd.CombinedOutput()
|
||||
Expect(err).To(BeAssignableToTypeOf(&exec.ExitError{}))
|
||||
Expect(output).To(ContainSubstring("unknown CNI_COMMAND: VERSION"))
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("LocateCurrentGitRepo", func() {
|
||||
It("returns the path to the root of the CNI git repo", func() {
|
||||
path, err := testhelpers.LocateCurrentGitRepo()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
AssertItIsTheCNIRepoRoot(path)
|
||||
})
|
||||
|
||||
Context("when run from a different directory", func() {
|
||||
BeforeEach(func() {
|
||||
os.Chdir("..")
|
||||
})
|
||||
|
||||
It("still finds the CNI repo root", func() {
|
||||
path, err := testhelpers.LocateCurrentGitRepo()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
AssertItIsTheCNIRepoRoot(path)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
func AssertItIsTheCNIRepoRoot(path string) {
|
||||
Expect(path).To(BeADirectory())
|
||||
files, err := ioutil.ReadDir(path)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
names := []string{}
|
||||
for _, file := range files {
|
||||
names = append(names, file.Name())
|
||||
}
|
||||
|
||||
Expect(names).To(ContainElement("SPEC.md"))
|
||||
Expect(names).To(ContainElement("libcni"))
|
||||
Expect(names).To(ContainElement("cnitool"))
|
||||
}
|
61
pkg/version/version.go
Normal file
61
pkg/version/version.go
Normal file
@ -0,0 +1,61 @@
|
||||
// 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.
|
||||
|
||||
package version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/types/020"
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
)
|
||||
|
||||
// Current reports the version of the CNI spec implemented by this library
|
||||
func Current() string {
|
||||
return "0.3.1"
|
||||
}
|
||||
|
||||
// Legacy PluginInfo describes a plugin that is backwards compatible with the
|
||||
// CNI spec version 0.1.0. In particular, a runtime compiled against the 0.1.0
|
||||
// library ought to work correctly with a plugin that reports support for
|
||||
// Legacy versions.
|
||||
//
|
||||
// Any future CNI spec versions which meet this definition should be added to
|
||||
// this list.
|
||||
var Legacy = PluginSupports("0.1.0", "0.2.0")
|
||||
var All = PluginSupports("0.1.0", "0.2.0", "0.3.0", "0.3.1")
|
||||
|
||||
var resultFactories = []struct {
|
||||
supportedVersions []string
|
||||
newResult types.ResultFactoryFunc
|
||||
}{
|
||||
{current.SupportedVersions, current.NewResult},
|
||||
{types020.SupportedVersions, types020.NewResult},
|
||||
}
|
||||
|
||||
// Finds a Result object matching the requested version (if any) and asks
|
||||
// that object to parse the plugin result, returning an error if parsing failed.
|
||||
func NewResult(version string, resultBytes []byte) (types.Result, error) {
|
||||
reconciler := &Reconciler{}
|
||||
for _, resultFactory := range resultFactories {
|
||||
err := reconciler.CheckRaw(version, resultFactory.supportedVersions)
|
||||
if err == nil {
|
||||
// Result supports this version
|
||||
return resultFactory.newResult(resultBytes)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unsupported CNI result version %q", version)
|
||||
}
|
27
pkg/version/version_suite_test.go
Normal file
27
pkg/version/version_suite_test.go
Normal file
@ -0,0 +1,27 @@
|
||||
// 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.
|
||||
|
||||
package version_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestVersion(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Version Suite")
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user