versioning: plugins report a list of supported versions

Further progress on versioning support (Issue #266).
Bump CNI spec version to 0.3.0
This commit is contained in:
Gabe Rosenhouse
2016-08-21 23:48:04 -07:00
parent c5e39a87f7
commit 536cb5b99b
20 changed files with 262 additions and 64 deletions

20
SPEC.md
View File

@ -64,8 +64,14 @@ The operations that the CNI plugin needs to support are:
- Report version
- Parameters: NONE.
- Result:
- The version of the CNI spec implemented by the plugin: `{ "cniVersion": "0.2.0" }`
- Result: information about the CNI spec versions supported by the plugin
```
{
"cniVersion": "0.3.0", // the version of the CNI spec in use for this output
"supportedVersions": [ "0.1.0", "0.2.0", "0.3.0" ] // the list of CNI spec versions that this plugin supports
}
```
The executable command-line API uses the type of network (see [Network Configuration](#network-configuration) below) as the name of the executable to invoke.
It will then look for this executable in a list of predefined directories. Once found, it will invoke the executable using the following environment variables for argument passing:
@ -85,7 +91,7 @@ Success is indicated by a return code of zero and the following JSON printed to
```
{
"cniVersion": "0.2.0",
"cniVersion": "0.3.0",
"ip4": {
"ip": <ipv4-and-subnet-in-CIDR>,
"gateway": <ipv4-of-the-gateway>, (optional)
@ -114,7 +120,7 @@ Examples include generating an `/etc/resolv.conf` file to be injected into the c
Errors are indicated by a non-zero return code and the following JSON being printed to stdout:
```
{
"cniVersion": "0.2.0",
"cniVersion": "0.3.0",
"code": <numeric-error-code>,
"msg": <short-error-message>,
"details": <long-error-message> (optional)
@ -151,7 +157,7 @@ Plugins may define additional fields that they accept and may generate an error
```json
{
"cniVersion": "0.2.0",
"cniVersion": "0.3.0",
"name": "dbnet",
"type": "bridge",
// type (plugin) specific
@ -170,7 +176,7 @@ Plugins may define additional fields that they accept and may generate an error
```json
{
"cniVersion": "0.2.0",
"cniVersion": "0.3.0",
"name": "pci",
"type": "ovs",
// type (plugin) specific
@ -220,7 +226,7 @@ Success is indicated by a zero return code and the following JSON being printed
```
{
"cniVersion": "0.2.0",
"cniVersion": "0.3.0",
"ip4": {
"ip": <ipv4-and-subnet-in-CIDR>,
"gateway": <ipv4-of-the-gateway>, (optional)

View File

@ -19,6 +19,7 @@ import (
"github.com/containernetworking/cni/pkg/invoke"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/version"
)
type RuntimeConf struct {
@ -60,6 +61,17 @@ func (c *CNIConfig) DelNetwork(net *NetworkConfig, rt *RuntimeConf) error {
return invoke.ExecPluginWithoutResult(pluginPath, net.Bytes, c.args("DEL", rt))
}
func (c *CNIConfig) GetVersionInfo(pluginType string) (version.PluginInfo, error) {
pluginPath, err := invoke.FindInPath(pluginType, c.Path)
if err != nil {
return nil, err
}
// TODO: if error is because plugin is old and VERSION command is unrecognized
// then do the right thing and return version.PluginSupports("0.1.0"), nil
return invoke.ExecPluginForVersion(pluginPath)
}
// =====
func (c *CNIConfig) args(action string, rt *RuntimeConf) *invoke.Args {
return &invoke.Args{

View File

@ -155,4 +155,23 @@ var _ = Describe("Invoking the plugin", func() {
})
})
})
Describe("GetVersionInfo", func() {
It("executes the plugin with the command VERSION", func() {
versionInfo, err := cniConfig.GetVersionInfo("noop")
Expect(err).NotTo(HaveOccurred())
Expect(versionInfo).NotTo(BeNil())
Expect(versionInfo.SupportedVersions()).To(Equal([]string{
"0.-42.0", "0.1.0", "0.2.0", "0.3.0",
}))
})
Context("when finding the plugin fails", func() {
It("returns the error", func() {
_, err := cniConfig.GetVersionInfo("does-not-exist")
Expect(err).To(MatchError(ContainSubstring(`failed to find plugin "does-not-exist"`)))
})
})
})
})

View File

@ -23,6 +23,7 @@ import (
"os/exec"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/version"
)
func pluginErr(err error, output []byte) error {
@ -57,6 +58,15 @@ func ExecPluginWithoutResult(pluginPath string, netconf []byte, args CNIArgs) er
return err
}
func ExecPluginForVersion(pluginPath string) (version.PluginInfo, error) {
stdoutBytes, err := execPlugin(pluginPath, nil, &Args{Command: "VERSION"})
if err != nil {
return nil, err
}
return version.Decode(stdoutBytes)
}
func execPlugin(pluginPath string, netconf []byte, args CNIArgs) ([]byte, error) {
return defaultRawExec.ExecPlugin(pluginPath, netconf, args.AsEnv())
}

View File

@ -43,7 +43,6 @@ type dispatcher struct {
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
Versioner version.PluginVersioner
}
type reqForCmdEntry map[string]bool
@ -144,7 +143,7 @@ func createTypedError(f string, args ...interface{}) *types.Error {
}
}
func (t *dispatcher) pluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) *types.Error {
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())
@ -158,7 +157,7 @@ func (t *dispatcher) pluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) *types.Er
err = cmdDel(cmdArgs)
case "VERSION":
err = t.Versioner.Encode(t.Stdout)
err = versionInfo.Encode(t.Stdout)
default:
return createTypedError("unknown CNI_COMMAND: %v", cmd)
@ -176,16 +175,15 @@ func (t *dispatcher) pluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) *types.Er
// PluginMain is the "main" for a plugin. It accepts
// two callback functions for add and del commands.
func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) {
func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo) {
caller := dispatcher{
Getenv: os.Getenv,
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
Versioner: version.DefaultPluginVersioner,
}
err := caller.pluginMain(cmdAdd, cmdDel)
err := caller.pluginMain(cmdAdd, cmdDel, versionInfo)
if err != nil {
dieErr(err)
}

View File

@ -53,6 +53,7 @@ var _ = Describe("dispatching to the correct callback", func() {
cmdAdd, cmdDel *fakeCmd
dispatch *dispatcher
expectedCmdArgs *CmdArgs
versionInfo version.PluginInfo
)
BeforeEach(func() {
@ -67,13 +68,12 @@ var _ = Describe("dispatching to the correct callback", func() {
stdin = strings.NewReader(`{ "some": "config" }`)
stdout = &bytes.Buffer{}
stderr = &bytes.Buffer{}
versioner := &version.BasicVersioner{CNIVersion: "9.8.7"}
versionInfo = version.PluginSupports("9.8.7")
dispatch = &dispatcher{
Getenv: func(key string) string { return environment[key] },
Stdin: stdin,
Stdout: stdout,
Stderr: stderr,
Versioner: versioner,
}
cmdAdd = &fakeCmd{}
cmdDel = &fakeCmd{}
@ -90,7 +90,7 @@ var _ = Describe("dispatching to the correct callback", func() {
var envVarChecker = func(envVar string, isRequired bool) {
delete(environment, envVar)
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func)
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
if isRequired {
Expect(err).To(Equal(&types.Error{
Code: 100,
@ -104,7 +104,7 @@ var _ = Describe("dispatching to the correct callback", func() {
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)
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
Expect(err).NotTo(HaveOccurred())
Expect(cmdAdd.CallCount).To(Equal(1))
@ -113,7 +113,7 @@ var _ = Describe("dispatching to the correct callback", func() {
})
It("does not call cmdDel", func() {
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func)
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
Expect(err).NotTo(HaveOccurred())
Expect(cmdDel.CallCount).To(Equal(0))
@ -136,7 +136,7 @@ var _ = Describe("dispatching to the correct callback", func() {
})
It("reports that all of them are missing, not just the first", func() {
Expect(dispatch.pluginMain(cmdAdd.Func, cmdDel.Func)).NotTo(Succeed())
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"))
@ -152,7 +152,7 @@ var _ = Describe("dispatching to the correct callback", func() {
})
It("calls cmdDel with the env vars and stdin data", func() {
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func)
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
Expect(err).NotTo(HaveOccurred())
Expect(cmdDel.CallCount).To(Equal(1))
@ -160,7 +160,7 @@ var _ = Describe("dispatching to the correct callback", func() {
})
It("does not call cmdAdd", func() {
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func)
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
Expect(err).NotTo(HaveOccurred())
Expect(cmdAdd.CallCount).To(Equal(0))
@ -182,14 +182,17 @@ var _ = Describe("dispatching to the correct callback", func() {
})
It("prints the version to stdout", func() {
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func)
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
Expect(err).NotTo(HaveOccurred())
Expect(stdout).To(MatchJSON(`{ "cniVersion": "9.8.7" }`))
Expect(stdout).To(MatchJSON(`{
"cniVersion": "0.3.0",
"supportedVersions": ["9.8.7"]
}`))
})
It("does not call cmdAdd or cmdDel", func() {
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func)
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
Expect(err).NotTo(HaveOccurred())
Expect(cmdAdd.CallCount).To(Equal(0))
@ -212,14 +215,14 @@ var _ = Describe("dispatching to the correct callback", func() {
})
It("does not call any cmd callback", func() {
dispatch.pluginMain(cmdAdd.Func, cmdDel.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)
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
Expect(err).To(Equal(&types.Error{
Code: 100,
@ -234,14 +237,14 @@ var _ = Describe("dispatching to the correct callback", func() {
})
It("does not call any cmd callback", func() {
dispatch.pluginMain(cmdAdd.Func, cmdDel.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)
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
Expect(err).To(Equal(&types.Error{
Code: 100,
@ -260,7 +263,7 @@ var _ = Describe("dispatching to the correct callback", func() {
})
It("returns the error as-is", func() {
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func)
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
Expect(err).To(Equal(&types.Error{
Code: 1234,
@ -275,7 +278,7 @@ var _ = Describe("dispatching to the correct callback", func() {
})
It("wraps and returns the error", func() {
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func)
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
Expect(err).To(Equal(&types.Error{
Code: 100,

View File

@ -16,27 +16,67 @@ package version
import (
"encoding/json"
"fmt"
"io"
)
// A PluginVersioner can encode information about its version
type PluginVersioner interface {
// Current reports the version of the CNI spec implemented by this library
func Current() string {
return "0.3.0"
}
// 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
}
// BasicVersioner is a PluginVersioner which reports a single cniVersion string
type BasicVersioner struct {
CNIVersion string `json:"cniVersion"`
type simple struct {
CNIVersion_ string `json:"cniVersion"`
SupportedVersions_ []string `json:"supportedVersions,omitempty"`
}
func (p *BasicVersioner) Encode(w io.Writer) error {
func (p *simple) Encode(w io.Writer) error {
return json.NewEncoder(w).Encode(p)
}
// Current reports the version of the CNI spec implemented by this library
func Current() string {
return "0.2.0"
func (p *simple) SupportedVersions() []string {
return p.SupportedVersions_
}
// DefaultPluginVersioner reports the Current library spec version as the cniVersion
var DefaultPluginVersioner = &BasicVersioner{CNIVersion: Current()}
// 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 &simple{
CNIVersion_: Current(),
SupportedVersions_: supportedVersions,
}
}
func Decode(jsonBytes []byte) (PluginInfo, error) {
var info simple
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
}
var Legacy = PluginSupports("0.1.0", "0.2.0", "0.3.0")

View 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")
}

View File

@ -0,0 +1,72 @@
// 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("Decode", func() {
It("returns a PluginInfo that represents the given json bytes", func() {
pluginInfo, err := version.Decode([]byte(`{
"cniVersion": "some-library-version",
"supportedVersions": [ "some-version", "some-other-version" ]
}`))
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() {
It("returns a meaningful error", func() {
_, err := version.Decode([]byte(`{{{`))
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() {
It("returns a meaningful error", func() {
_, err := version.Decode([]byte(`{ "supportedVersions": [ "foo" ] }`))
Expect(err).To(MatchError("decoding version info: missing field cniVersion"))
})
})
Context("when there are no supported versions", func() {
Context("when the cniVersion is 0.2.0", func() {
It("infers the supported versions are 0.1.0 and 0.2.0", func() {
pluginInfo, err := version.Decode([]byte(`{ "cniVersion": "0.2.0" }`))
Expect(err).NotTo(HaveOccurred())
Expect(pluginInfo).NotTo(BeNil())
Expect(pluginInfo.SupportedVersions()).To(Equal([]string{
"0.1.0",
"0.2.0",
}))
})
})
Context("when the cniVersion is >= 0.3.0", func() {
It("returns a meaningful error", func() {
_, err := version.Decode([]byte(`{ "cniVersion": "0.3.0" }`))
Expect(err).To(MatchError("decoding version info: missing field supportedVersions"))
})
})
})
})

View File

@ -22,6 +22,7 @@ import (
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/version"
)
const socketPath = "/run/cni/dhcp.sock"
@ -30,7 +31,7 @@ func main() {
if len(os.Args) > 1 && os.Args[1] == "daemon" {
runDaemon()
} else {
skel.PluginMain(cmdAdd, cmdDel)
skel.PluginMain(cmdAdd, cmdDel, version.Legacy)
}
}

View File

@ -19,10 +19,11 @@ import (
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/version"
)
func main() {
skel.PluginMain(cmdAdd, cmdDel)
skel.PluginMain(cmdAdd, cmdDel, version.Legacy)
}
func cmdAdd(args *skel.CmdArgs) error {

View File

@ -28,6 +28,7 @@ import (
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/utils"
"github.com/containernetworking/cni/pkg/version"
"github.com/vishvananda/netlink"
)
@ -354,5 +355,5 @@ func cmdDel(args *skel.CmdArgs) error {
}
func main() {
skel.PluginMain(cmdAdd, cmdDel)
skel.PluginMain(cmdAdd, cmdDel, version.Legacy)
}

View File

@ -25,6 +25,7 @@ import (
"github.com/containernetworking/cni/pkg/ns"
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/version"
"github.com/vishvananda/netlink"
)
@ -171,5 +172,5 @@ func renameLink(curName, newName string) error {
}
func main() {
skel.PluginMain(cmdAdd, cmdDel)
skel.PluginMain(cmdAdd, cmdDel, version.Legacy)
}

View File

@ -18,6 +18,7 @@ import (
"github.com/containernetworking/cni/pkg/ns"
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/version"
"github.com/vishvananda/netlink"
)
@ -67,5 +68,5 @@ func cmdDel(args *skel.CmdArgs) error {
}
func main() {
skel.PluginMain(cmdAdd, cmdDel)
skel.PluginMain(cmdAdd, cmdDel, version.Legacy)
}

View File

@ -26,6 +26,7 @@ import (
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/utils/sysctl"
"github.com/containernetworking/cni/pkg/version"
"github.com/vishvananda/netlink"
)
@ -193,5 +194,5 @@ func renameLink(curName, newName string) error {
}
func main() {
skel.PluginMain(cmdAdd, cmdDel)
skel.PluginMain(cmdAdd, cmdDel, version.Legacy)
}

View File

@ -30,6 +30,7 @@ import (
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/utils"
"github.com/containernetworking/cni/pkg/version"
)
func init() {
@ -236,5 +237,5 @@ func cmdDel(args *skel.CmdArgs) error {
}
func main() {
skel.PluginMain(cmdAdd, cmdDel)
skel.PluginMain(cmdAdd, cmdDel, version.Legacy)
}

View File

@ -32,6 +32,7 @@ import (
"github.com/containernetworking/cni/pkg/invoke"
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/version"
)
const (
@ -249,5 +250,5 @@ func cmdDel(args *skel.CmdArgs) error {
}
func main() {
skel.PluginMain(cmdAdd, cmdDel)
skel.PluginMain(cmdAdd, cmdDel, version.Legacy)
}

View File

@ -27,6 +27,7 @@ import (
"github.com/containernetworking/cni/pkg/ns"
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/version"
)
// TuningConf represents the network tuning configuration.
@ -78,5 +79,5 @@ func cmdDel(args *skel.CmdArgs) error {
}
func main() {
skel.PluginMain(cmdAdd, cmdDel)
skel.PluginMain(cmdAdd, cmdDel, version.Legacy)
}

View File

@ -28,6 +28,7 @@ import (
"strings"
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/version"
"github.com/containernetworking/cni/plugins/test/noop/debug"
)
@ -71,5 +72,6 @@ func cmdDel(args *skel.CmdArgs) error {
}
func main() {
skel.PluginMain(cmdAdd, cmdDel)
skel.PluginMain(cmdAdd, cmdDel,
version.PluginSupports("0.-42.0", "0.1.0", "0.2.0", "0.3.0"))
}

2
test
View File

@ -11,7 +11,7 @@ set -e
source ./build
TESTABLE="libcni plugins/ipam/dhcp plugins/ipam/host-local plugins/main/loopback pkg/invoke pkg/ns pkg/skel pkg/types pkg/utils plugins/main/ipvlan plugins/main/macvlan plugins/main/bridge plugins/main/ptp plugins/test/noop pkg/utils/hwaddr pkg/ip"
TESTABLE="libcni plugins/ipam/dhcp plugins/ipam/host-local plugins/main/loopback pkg/invoke pkg/ns pkg/skel pkg/types pkg/utils plugins/main/ipvlan plugins/main/macvlan plugins/main/bridge plugins/main/ptp plugins/test/noop pkg/utils/hwaddr pkg/ip pkg/version"
FORMATTABLE="$TESTABLE pkg/testutils plugins/meta/flannel plugins/meta/tuning"
# user has not provided PKG override