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