Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement GC keyword for CNI spec 1.1 #1022

Merged
merged 4 commits into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 89 additions & 1 deletion libcni/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,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 +113,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 +163,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 +754,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 joinErrors(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
97 changes: 89 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,79 @@ 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() {
squeed marked this conversation as resolved.
Show resolved Hide resolved
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(HaveLen(4))

validations := []struct {
name string
fn func(entry noop_debug.CmdLogEntry)
}{
{
name: "ADD",
fn: func(entry noop_debug.CmdLogEntry) {
Expect(entry.CmdArgs.ContainerID).To(Equal(runtimeConfig.ContainerID))
Expect(entry.CmdArgs.IfName).To(Equal(runtimeConfig.IfName))
},
},
{
name: "GC",
fn: func(entry noop_debug.CmdLogEntry) {
var conf struct {
Attachments []map[string]string `json:"cni.dev/valid-attachments"`
}
err = json.Unmarshal(entry.CmdArgs.StdinData, &conf)
Expect(err).NotTo(HaveOccurred())
Expect(conf.Attachments).To(HaveLen(1))
Expect(conf.Attachments[0]).To(Equal(map[string]string{"containerID": runtimeConfig.ContainerID, "ifname": runtimeConfig.IfName}))
},
},
{
name: "DEL",
fn: func(entry noop_debug.CmdLogEntry) {
Expect(entry.CmdArgs.ContainerID).To(Equal(runtimeConfig.ContainerID))
Expect(entry.CmdArgs.IfName).To(Equal(runtimeConfig.IfName))
},
},
{
name: "GC",
fn: func(entry noop_debug.CmdLogEntry) {
var conf struct {
Attachments []map[string]string `json:"cni.dev/valid-attachments"`
}
err = json.Unmarshal(entry.CmdArgs.StdinData, &conf)
Expect(err).NotTo(HaveOccurred())
Expect(conf.Attachments).To(BeEmpty())
},
},
}
for i, c := range validations {
Expect(commands[i].Command).To(Equal(c.name))
c.fn(commands[i])
}
})
})
})

Describe("Invoking a sleep plugin", func() {
Expand Down
58 changes: 58 additions & 0 deletions libcni/multierror.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// 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 libcni

func joinErrors(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)
}
Loading