Skip to content

Commit

Permalink
implement GC keyword for CNI spec 1.1
Browse files Browse the repository at this point in the history
Signed-off-by: Henry Wang <[email protected]>
  • Loading branch information
henry118 committed Sep 16, 2023
1 parent f6506e2 commit 7454f04
Show file tree
Hide file tree
Showing 7 changed files with 487 additions and 77 deletions.
91 changes: 90 additions & 1 deletion libcni/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"strings"

"github.com/containernetworking/cni/pkg/invoke"
"github.com/containernetworking/cni/pkg/multierror"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/types/create"
"github.com/containernetworking/cni/pkg/utils"
Expand Down Expand Up @@ -89,6 +90,14 @@ type NetworkAttachment struct {
CapabilityArgs map[string]interface{}
}

type GCAttachment struct {
ContainerID string `json:"containerID"`
IfName string `json:"ifname"`
}
type GCArgs struct {
ValidAttachments []GCAttachment
}

type CNI interface {
AddNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) (types.Result, error)
CheckNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) error
Expand All @@ -105,6 +114,8 @@ type CNI interface {
ValidateNetworkList(ctx context.Context, net *NetworkConfigList) ([]string, error)
ValidateNetwork(ctx context.Context, net *NetworkConfig) ([]string, error)

GCNetworkList(ctx context.Context, net *NetworkConfigList, args *GCArgs) error

GetCachedAttachments(containerID string) ([]*NetworkAttachment, error)
}

Expand Down Expand Up @@ -153,8 +164,11 @@ func buildOneConfig(name, cniVersion string, orig *NetworkConfig, prevResult typ
if err != nil {
return nil, err
}
if rt != nil {
return injectRuntimeConfig(orig, rt)
}

return injectRuntimeConfig(orig, rt)
return orig, nil
}

// This function takes a libcni RuntimeConf structure and injects values into
Expand Down Expand Up @@ -741,6 +755,81 @@ func (c *CNIConfig) GetVersionInfo(ctx context.Context, pluginType string) (vers
return invoke.GetVersionInfo(ctx, pluginPath, c.exec)
}

// GCNetworkList will do two things
// - dump the list of cached attachments, and issue deletes as necessary
// - issue a GC to the underlying plugins (if the version is high enough)
func (c *CNIConfig) GCNetworkList(ctx context.Context, list *NetworkConfigList, args *GCArgs) error {
// First, get the list of cached attachments
cachedAttachments, err := c.GetCachedAttachments("")
if err != nil {
return nil
}

validAttachments := make(map[GCAttachment]interface{}, len(args.ValidAttachments))
for _, a := range args.ValidAttachments {
validAttachments[a] = nil
}

var errs []error

for _, cachedAttachment := range cachedAttachments {
if cachedAttachment.Network != list.Name {
continue
}
// we found this attachment
gca := GCAttachment{
ContainerID: cachedAttachment.ContainerID,
IfName: cachedAttachment.IfName,
}
if _, ok := validAttachments[gca]; ok {
continue
}
// otherwise, this attachment wasn't valid and we should issue a CNI DEL
rt := RuntimeConf{
ContainerID: cachedAttachment.ContainerID,
NetNS: cachedAttachment.NetNS,
IfName: cachedAttachment.IfName,
Args: cachedAttachment.CniArgs,
CapabilityArgs: cachedAttachment.CapabilityArgs,
}
if err := c.DelNetworkList(ctx, list, &rt); err != nil {
errs = append(errs, fmt.Errorf("failed to delete stale attachment %s %s: %w", rt.ContainerID, rt.IfName, err))
}
}

// now, if the version supports it, issue a GC
if gt, _ := version.GreaterThanOrEqualTo(list.CNIVersion, "1.1.0"); gt {
inject := map[string]interface{}{
"name": list.Name,
"cniVersion": list.CNIVersion,
"cni.dev/valid-attachments": args.ValidAttachments,
}
for _, plugin := range list.Plugins {
// build config here
pluginConfig, err := InjectConf(plugin, inject)
if err != nil {
errs = append(errs, fmt.Errorf("failed to generate configuration to GC plugin %s: %w", plugin.Network.Type, err))
}
if err := c.gcNetwork(ctx, pluginConfig); err != nil {
errs = append(errs, fmt.Errorf("failed to GC plugin %s: %w", plugin.Network.Type, err))
}
}
}

return multierror.Join(errs...)
}

func (c *CNIConfig) gcNetwork(ctx context.Context, net *NetworkConfig) error {
c.ensureExec()
pluginPath, err := c.exec.FindInPath(net.Network.Type, c.Path)
if err != nil {
return err
}
args := c.args("GC", &RuntimeConf{})

return invoke.ExecPluginWithoutResult(ctx, pluginPath, net.Bytes, args, c.exec)
}

// =====
func (c *CNIConfig) args(action string, rt *RuntimeConf) *invoke.Args {
return &invoke.Args{
Expand Down
50 changes: 42 additions & 8 deletions libcni/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,11 @@ import (
)

type pluginInfo struct {
debugFilePath string
debug *noop_debug.Debug
config string
stdinData []byte
debugFilePath string
commandFilePath string
debug *noop_debug.Debug
config string
stdinData []byte
}

type portMapping struct {
Expand All @@ -66,6 +67,11 @@ func newPluginInfo(cniVersion, configValue, prevResult string, injectDebugFilePa
Expect(debugFile.Close()).To(Succeed())
debugFilePath := debugFile.Name()

commandLog, err := os.CreateTemp("", "cni_debug")
Expect(err).NotTo(HaveOccurred())
Expect(commandLog.Close()).To(Succeed())
commandFilePath := commandLog.Name()

debug := &noop_debug.Debug{
ReportResult: result,
}
Expand All @@ -79,6 +85,7 @@ func newPluginInfo(cniVersion, configValue, prevResult string, injectDebugFilePa
}
if injectDebugFilePath {
config += fmt.Sprintf(`, "debugFile": %q`, debugFilePath)
config += fmt.Sprintf(`, "commandLog": %q`, commandFilePath)
}
if len(capabilities) > 0 {
config += `, "capabilities": {`
Expand Down Expand Up @@ -115,10 +122,11 @@ func newPluginInfo(cniVersion, configValue, prevResult string, injectDebugFilePa
Expect(err).NotTo(HaveOccurred())

return pluginInfo{
debugFilePath: debugFilePath,
debug: debug,
config: config,
stdinData: stdinData,
debugFilePath: debugFilePath,
commandFilePath: commandFilePath,
debug: debug,
config: config,
stdinData: stdinData,
}
}

Expand Down Expand Up @@ -1499,6 +1507,32 @@ var _ = Describe("Invoking plugins", func() {
Expect(err).To(MatchError("[plugin noop does not support config version \"broken\" plugin noop does not support config version \"broken\" plugin noop does not support config version \"broken\"]"))
})
})
Describe("GCNetworkList", func() {
It("issues a DEL and GC as necessary", func() {
By("doing a CNI ADD")
_, err := cniConfig.AddNetworkList(ctx, netConfigList, runtimeConfig)
Expect(err).NotTo(HaveOccurred())

By("Issuing a GC with valid networks")
gcargs := &libcni.GCArgs{
ValidAttachments: []libcni.GCAttachment{{
ContainerID: runtimeConfig.ContainerID,
IfName: runtimeConfig.IfName,
}},
}
err = cniConfig.GCNetworkList(ctx, netConfigList, gcargs)
Expect(err).NotTo(HaveOccurred())

By("Issuing a GC with no valid networks")
gcargs.ValidAttachments = nil
err = cniConfig.GCNetworkList(ctx, netConfigList, gcargs)
Expect(err).NotTo(HaveOccurred())

commands, err := noop_debug.ReadCommandLog(plugins[0].commandFilePath)
Expect(err).NotTo(HaveOccurred())
Expect(commands).To(Equal([]string{"ADD", "GC", "DEL", "GC"}))
})
})
})

Describe("Invoking a sleep plugin", func() {
Expand Down
70 changes: 70 additions & 0 deletions pkg/multierror/multierror.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Copyright the 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.
//
// Adapted from errors/join.go from go 1.20
// This package can be removed once the toolchain is updated to 1.20

package multierror

// Join returns an error that wraps the given errors.
// Any nil error values are discarded.
// Join returns nil if every value in errs is nil.
// The error formats as the concatenation of the strings obtained
// by calling the Error method of each element of errs, with a newline
// between each string.
//
// A non-nil error returned by Join implements the Unwrap() []error method.
func Join(errs ...error) error {
n := 0
for _, err := range errs {
if err != nil {
n++
}
}
if n == 0 {
return nil
}
e := &multiError{
errs: make([]error, 0, n),
}
for _, err := range errs {
if err != nil {
e.errs = append(e.errs, err)
}
}
return e
}

type multiError struct {
errs []error
}

func (e *multiError) Error() string {
var b []byte
for i, err := range e.errs {
if i > 0 {
b = append(b, '\n')
}
b = append(b, err.Error()...)
}
return string(b)
}

func (e *multiError) Unwrap() []error {

Check failure on line 68 in pkg/multierror/multierror.go

View workflow job for this annotation

GitHub Actions / Run tests on Linux amd64

method Unwrap() []error should have signature Unwrap() error

Check failure on line 68 in pkg/multierror/multierror.go

View workflow job for this annotation

GitHub Actions / Build and run tests on Windows

method Unwrap() []error should have signature Unwrap() error
return e.errs
}
Loading

0 comments on commit 7454f04

Please sign in to comment.