Merge pull request #7069 from fuweid/failpoint-in-runc-shimv2
test: introduce failpoint control to runc-shimv2 and cni
This commit is contained in:
		
							
								
								
									
										4
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -425,6 +425,10 @@ jobs: | ||||
|           sudo apt-get update | ||||
|           sudo apt-get install -y criu | ||||
|  | ||||
|       - name: Install failpoint binaries | ||||
|         run: | | ||||
|           script/setup/install-failpoint-binaries | ||||
|  | ||||
|       - name: Install containerd | ||||
|         env: | ||||
|           CGO_ENABLED: 1 | ||||
|   | ||||
							
								
								
									
										12
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								Makefile
									
									
									
									
									
								
							| @@ -223,6 +223,16 @@ cri-integration: binaries bin/cri-integration.test ## run cri integration tests | ||||
| 	@bash -x ./script/test/cri-integration.sh | ||||
| 	@rm -rf bin/cri-integration.test | ||||
|  | ||||
| # build runc shimv2 with failpoint control, only used by integration test | ||||
| bin/containerd-shim-runc-fp-v1: integration/failpoint/cmd/containerd-shim-runc-fp-v1 FORCE | ||||
| 	@echo "$(WHALE) $@" | ||||
| 	@CGO_ENABLED=${SHIM_CGO_ENABLED} $(GO) build ${GO_BUILD_FLAGS} -o $@ ${SHIM_GO_LDFLAGS} ${GO_TAGS} ./integration/failpoint/cmd/containerd-shim-runc-fp-v1 | ||||
|  | ||||
| # build CNI bridge plugin wrapper with failpoint support, only used by integration test | ||||
| bin/cni-bridge-fp: integration/failpoint/cmd/cni-bridge-fp FORCE | ||||
| 	@echo "$(WHALE) $@" | ||||
| 	@$(GO) build ${GO_BUILD_FLAGS} -o $@ ./integration/failpoint/cmd/cni-bridge-fp | ||||
|  | ||||
| benchmark: ## run benchmarks tests | ||||
| 	@echo "$(WHALE) $@" | ||||
| 	@$(GO) test ${TESTFLAGS} -bench . -run Benchmark -test.root | ||||
| @@ -374,6 +384,8 @@ clean-test: ## clean up debris from previously failed tests | ||||
| 	@rm -rf /run/containerd/fifo/* | ||||
| 	@rm -rf /run/containerd-test/* | ||||
| 	@rm -rf bin/cri-integration.test | ||||
| 	@rm -rf bin/cni-bridge-fp | ||||
| 	@rm -rf bin/containerd-shim-runc-fp-v1 | ||||
|  | ||||
| install: ## install binaries | ||||
| 	@echo "$(WHALE) $@ $(BINARIES)" | ||||
|   | ||||
| @@ -116,6 +116,10 @@ var ( | ||||
| 			Name:  "label", | ||||
| 			Usage: "specify additional labels (e.g. foo=bar)", | ||||
| 		}, | ||||
| 		cli.StringSliceFlag{ | ||||
| 			Name:  "annotation", | ||||
| 			Usage: "specify additional OCI annotations (e.g. foo=bar)", | ||||
| 		}, | ||||
| 		cli.StringSliceFlag{ | ||||
| 			Name:  "mount", | ||||
| 			Usage: "specify additional container mount (e.g. type=bind,src=/tmp,dst=/host,options=rbind:ro)", | ||||
| @@ -239,6 +243,19 @@ func LabelArgs(labelStrings []string) map[string]string { | ||||
| 	return labels | ||||
| } | ||||
|  | ||||
| // AnnotationArgs returns a map of annotation key,value pairs. | ||||
| func AnnotationArgs(annoStrings []string) (map[string]string, error) { | ||||
| 	annotations := make(map[string]string, len(annoStrings)) | ||||
| 	for _, anno := range annoStrings { | ||||
| 		parts := strings.SplitN(anno, "=", 2) | ||||
| 		if len(parts) != 2 { | ||||
| 			return nil, fmt.Errorf("invalid key=value format annotation: %v", anno) | ||||
| 		} | ||||
| 		annotations[parts[0]] = parts[1] | ||||
| 	} | ||||
| 	return annotations, nil | ||||
| } | ||||
|  | ||||
| // PrintAsJSON prints input in JSON format | ||||
| func PrintAsJSON(x interface{}) { | ||||
| 	b, err := json.MarshalIndent(x, "", "    ") | ||||
|   | ||||
| @@ -217,6 +217,13 @@ func NewContainer(ctx gocontext.Context, client *containerd.Client, context *cli | ||||
| 				oci.WithEnv([]string{fmt.Sprintf("HOSTNAME=%s", hostname)}), | ||||
| 			) | ||||
| 		} | ||||
| 		if annoStrings := context.StringSlice("annotation"); len(annoStrings) > 0 { | ||||
| 			annos, err := commands.AnnotationArgs(annoStrings) | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 			opts = append(opts, oci.WithAnnotations(annos)) | ||||
| 		} | ||||
|  | ||||
| 		if context.Bool("cni") { | ||||
| 			cniMeta := &commands.NetworkMetaData{EnableCni: true} | ||||
|   | ||||
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							| @@ -20,6 +20,7 @@ require ( | ||||
| 	github.com/containerd/ttrpc v1.1.1-0.20220420014843-944ef4a40df3 | ||||
| 	github.com/containerd/typeurl v1.0.3-0.20220422153119-7f6e6d160d67 | ||||
| 	github.com/containerd/zfs v1.0.0 | ||||
| 	github.com/containernetworking/cni v1.1.1 | ||||
| 	github.com/containernetworking/plugins v1.1.1 | ||||
| 	github.com/coreos/go-systemd/v22 v22.3.2 | ||||
| 	github.com/davecgh/go-spew v1.1.1 | ||||
| @@ -82,7 +83,6 @@ require ( | ||||
| 	github.com/cenkalti/backoff/v4 v4.1.3 // indirect | ||||
| 	github.com/cespare/xxhash/v2 v2.1.2 // indirect | ||||
| 	github.com/cilium/ebpf v0.7.0 // indirect | ||||
| 	github.com/containernetworking/cni v1.1.1 // indirect | ||||
| 	github.com/containers/ocicrypt v1.1.3 // indirect | ||||
| 	github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect | ||||
| 	github.com/cyphar/filepath-securejoin v0.2.3 // indirect | ||||
|   | ||||
							
								
								
									
										159
									
								
								integration/failpoint/cmd/cni-bridge-fp/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								integration/failpoint/cmd/cni-bridge-fp/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,159 @@ | ||||
| ## cni-bridge-f(ail)p(oint) | ||||
|  | ||||
| ### Overview | ||||
|  | ||||
| The `cni-bridge-fp` is a CNI plugin which delegates interface-creating function | ||||
| to [CNI bridge plugin][1] and allows user to inject failpoint before delegation. | ||||
|  | ||||
| Since the CNI plugin is invoked by binary call from CRI and it is short-lived, | ||||
| the failpoint need to be configured by a JSON file, which can be persisted. | ||||
| There is an example about failpoint description. | ||||
|  | ||||
| ```json | ||||
| { | ||||
| 	"cmdAdd":   "1*error(you-shall-not-pass!)->1*panic(again)", | ||||
| 	"cmdDel":   "1*error(try-again)", | ||||
| 	"cmdCheck": "10*off" | ||||
| } | ||||
| ``` | ||||
|  | ||||
| * `cmdAdd` (string, optional): The failpoint for `ADD` command. | ||||
| * `cmdDel` (string, optional): The failpoint for `DEL` command. | ||||
| * `cmdCheck` (string, optional): The failpoint for `CHECK` command. | ||||
|  | ||||
| Since the `cmdXXX` can be multiple failpoints, each CNI binary call will update | ||||
| the current state to make sure the order of execution is expected. | ||||
|  | ||||
| And the failpoint injection is enabled by pod's annotation. Currently, the key | ||||
| of customized CNI capabilities in containerd can only be `io.kubernetes.cri.pod-annotations` | ||||
| and containerd will pass pod's annotations to CNI under the that object. The | ||||
| user can use the `failpoint.cni.containerd.io/confpath` annotation to enable | ||||
| failpoint for the pod. | ||||
|  | ||||
| ```yaml | ||||
| apiVersion: v1 | ||||
| kind: Pod | ||||
| metadata: | ||||
|   name: nginx | ||||
|   annotations: | ||||
|     failpoint.cni.containerd.io/confpath: "/tmp/pod-failpoints.json" | ||||
| spec: | ||||
|   containers: | ||||
|   - name: nginx | ||||
|     image: nginx:1.14.2 | ||||
|     ports: | ||||
|     - containerPort: 80 | ||||
| ``` | ||||
|  | ||||
| ### Example | ||||
|  | ||||
| Let's use the following json as failpoint description. | ||||
|  | ||||
| ```bash | ||||
| $ cat <<EOF | tee /tmp/cni-failpoint.json | ||||
| { | ||||
| 	"cmdAdd":   "1*error(try-again)", | ||||
| 	"cmdDel":   "2*error(oops)", | ||||
| 	"cmdCheck": "1*off->1*panic(sorry)" | ||||
| } | ||||
| EOF | ||||
| ``` | ||||
|  | ||||
| And use `ip netns` to create persisted net namespace named by `failpoint`. | ||||
|  | ||||
| ```bash | ||||
| $ sudo ip netns add failpoint | ||||
| ``` | ||||
|  | ||||
| And then setup the following bash script for demo. | ||||
|  | ||||
| ```bash | ||||
| $ cat <<EOFDEMO | tee /tmp/cni-failpoint-demo-helper.sh | ||||
| #!/usr/bin/env bash | ||||
|  | ||||
| export CNI_CONTAINERID=failpoint-testing | ||||
| export CNI_NETNS=/run/netns/failpoint | ||||
| export CNI_IFNAME=fpeni0 | ||||
| export CNI_PATH=/opt/cni/bin/ | ||||
|  | ||||
| cat <<EOF | /opt/cni/bin/cni-bridge-fp | ||||
| { | ||||
|   "cniVersion": "0.3.0", | ||||
|   "name": "containerd-net-fp", | ||||
|   "type": "cni-bridge-fp", | ||||
|   "bridge": "fp-cni0", | ||||
|   "isGateway": true, | ||||
|   "ipMasq": true, | ||||
|   "promiscMode": true, | ||||
|   "ipam": { | ||||
|     "type": "host-local", | ||||
|     "ranges": [ | ||||
|       [{ | ||||
|         "subnet": "10.88.0.0/16" | ||||
|       }], | ||||
|       [{ | ||||
|         "subnet": "2001:4860:4860::/64" | ||||
|       }] | ||||
|     ], | ||||
|     "routes": [ | ||||
|       { "dst": "0.0.0.0/0" }, | ||||
|       { "dst": "::/0" } | ||||
|     ] | ||||
|   }, | ||||
|   "runtimeConfig": { | ||||
|     "io.kubernetes.cri.pod-annotations": { | ||||
|       "failpoint.cni.containerd.io/confpath": "/tmp/cni-failpoint.json" | ||||
|     } | ||||
|   } | ||||
| } | ||||
| EOF | ||||
|  | ||||
| EOFDEMO | ||||
| ``` | ||||
|  | ||||
| Let's try to setup CNI and we should get a error `try-again`. | ||||
|  | ||||
| ```bash | ||||
| $ sudo CNI_COMMAND=ADD bash /tmp/cni-failpoint-demo-helper.sh | ||||
| { | ||||
|     "code": 999, | ||||
|     "msg": "try-again" | ||||
| } | ||||
|  | ||||
| # there is no failpoint for ADD command. | ||||
| $ cat /tmp/cni-failpoint.json | jq . | ||||
| { | ||||
|   "cmdAdd": "0*error(try-again)", | ||||
|   "cmdDel": "2*error(oops)", | ||||
|   "cmdCheck": "1*off->1*panic(sorry)" | ||||
| } | ||||
| ``` | ||||
|  | ||||
| We should setup CNI successfully after retry. When we teardown the interface, | ||||
| there should be two failpoints. | ||||
|  | ||||
| ```bash | ||||
| $ sudo CNI_COMMAND=ADD bash /tmp/cni-failpoint-demo-helper.sh | ||||
| ... | ||||
|  | ||||
| $ sudo CNI_COMMAND=DEL bash /tmp/cni-failpoint-demo-helper.sh | ||||
| { | ||||
|     "code": 999, | ||||
|     "msg": "oops" | ||||
| } | ||||
|  | ||||
| $ sudo CNI_COMMAND=DEL bash /tmp/cni-failpoint-demo-helper.sh | ||||
| { | ||||
|     "code": 999, | ||||
|     "msg": "oops" | ||||
| } | ||||
|  | ||||
| $ cat /tmp/cni-failpoint.json | jq . | ||||
| { | ||||
|   "cmdAdd": "0*error(try-again)", | ||||
|   "cmdDel": "0*error(oops)", | ||||
|   "cmdCheck": "1*off->1*panic(sorry)" | ||||
| } | ||||
| ``` | ||||
|  | ||||
| [1]: <https://www.cni.dev/plugins/current/main/bridge/> | ||||
							
								
								
									
										202
									
								
								integration/failpoint/cmd/cni-bridge-fp/main_linux.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										202
									
								
								integration/failpoint/cmd/cni-bridge-fp/main_linux.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,202 @@ | ||||
| /* | ||||
|    Copyright The containerd 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 main | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"syscall" | ||||
|  | ||||
| 	"github.com/containerd/containerd/pkg/failpoint" | ||||
| 	"github.com/containerd/continuity" | ||||
| 	"github.com/containernetworking/cni/pkg/invoke" | ||||
| 	"github.com/containernetworking/cni/pkg/skel" | ||||
| 	"github.com/containernetworking/cni/pkg/version" | ||||
| ) | ||||
|  | ||||
| const delegatedPlugin = "bridge" | ||||
|  | ||||
| type netConf struct { | ||||
| 	RuntimeConfig struct { | ||||
| 		PodAnnotations inheritedPodAnnotations `json:"io.kubernetes.cri.pod-annotations"` | ||||
| 	} `json:"runtimeConfig,omitempty"` | ||||
| } | ||||
|  | ||||
| type inheritedPodAnnotations struct { | ||||
| 	// FailpointConfPath represents filepath of failpoint settings. | ||||
| 	FailpointConfPath string `json:"failpoint.cni.containerd.io/confpath,omitempty"` | ||||
| } | ||||
|  | ||||
| // failpointConf is used to describe cmdAdd/cmdDel/cmdCheck command's failpoint. | ||||
| type failpointConf struct { | ||||
| 	Add   string `json:"cmdAdd,omitempty"` | ||||
| 	Del   string `json:"cmdDel,omitempty"` | ||||
| 	Check string `json:"cmdCheck,omitempty"` | ||||
| } | ||||
|  | ||||
| func main() { | ||||
| 	skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, "bridge with failpoint support") | ||||
| } | ||||
|  | ||||
| func cmdAdd(args *skel.CmdArgs) error { | ||||
| 	if err := handleFailpoint(args, "ADD"); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	result, err := invoke.DelegateAdd(context.TODO(), delegatedPlugin, args.StdinData, nil) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return result.Print() | ||||
| } | ||||
|  | ||||
| func cmdCheck(args *skel.CmdArgs) error { | ||||
| 	if err := handleFailpoint(args, "CHECK"); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return invoke.DelegateCheck(context.TODO(), delegatedPlugin, args.StdinData, nil) | ||||
| } | ||||
|  | ||||
| func cmdDel(args *skel.CmdArgs) error { | ||||
| 	if err := handleFailpoint(args, "DEL"); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return invoke.DelegateDel(context.TODO(), delegatedPlugin, args.StdinData, nil) | ||||
| } | ||||
|  | ||||
| func handleFailpoint(args *skel.CmdArgs, cmdKind string) error { | ||||
| 	var conf netConf | ||||
| 	if err := json.Unmarshal(args.StdinData, &conf); err != nil { | ||||
| 		return fmt.Errorf("failed to parse network configuration: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	confPath := conf.RuntimeConfig.PodAnnotations.FailpointConfPath | ||||
| 	if len(confPath) == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	control, err := newFailpointControl(confPath) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	evalFn, err := control.delegatedEvalFn(cmdKind) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return evalFn() | ||||
| } | ||||
|  | ||||
| type failpointControl struct { | ||||
| 	confPath string | ||||
| } | ||||
|  | ||||
| func newFailpointControl(confPath string) (*failpointControl, error) { | ||||
| 	if !filepath.IsAbs(confPath) { | ||||
| 		return nil, fmt.Errorf("failpoint confPath(%s) is required to be absolute", confPath) | ||||
| 	} | ||||
|  | ||||
| 	return &failpointControl{ | ||||
| 		confPath: confPath, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| func (c *failpointControl) delegatedEvalFn(cmdKind string) (failpoint.EvalFn, error) { | ||||
| 	var resFn failpoint.EvalFn = nopEvalFn | ||||
|  | ||||
| 	if err := c.updateTx(func(conf *failpointConf) error { | ||||
| 		var fpStr *string | ||||
|  | ||||
| 		switch cmdKind { | ||||
| 		case "ADD": | ||||
| 			fpStr = &conf.Add | ||||
| 		case "DEL": | ||||
| 			fpStr = &conf.Del | ||||
| 		case "CHECK": | ||||
| 			fpStr = &conf.Check | ||||
| 		} | ||||
|  | ||||
| 		if fpStr == nil || *fpStr == "" { | ||||
| 			return nil | ||||
| 		} | ||||
|  | ||||
| 		fp, err := failpoint.NewFailpoint(cmdKind, *fpStr) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("failed to parse failpoint %s: %w", *fpStr, err) | ||||
| 		} | ||||
|  | ||||
| 		resFn = fp.DelegatedEval() | ||||
|  | ||||
| 		*fpStr = fp.Marshal() | ||||
| 		return nil | ||||
|  | ||||
| 	}); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return resFn, nil | ||||
| } | ||||
|  | ||||
| func (c *failpointControl) updateTx(updateFn func(conf *failpointConf) error) error { | ||||
| 	f, err := os.OpenFile(c.confPath, os.O_RDWR, 0666) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to open confPath %s: %w", c.confPath, err) | ||||
| 	} | ||||
| 	defer f.Close() | ||||
|  | ||||
| 	if err := flock(f.Fd()); err != nil { | ||||
| 		return fmt.Errorf("failed to lock failpoint setting %s: %w", c.confPath, err) | ||||
| 	} | ||||
| 	defer unflock(f.Fd()) | ||||
|  | ||||
| 	data, err := ioutil.ReadAll(f) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to read failpoint setting %s: %w", c.confPath, err) | ||||
| 	} | ||||
|  | ||||
| 	var conf failpointConf | ||||
| 	if err := json.Unmarshal(data, &conf); err != nil { | ||||
| 		return fmt.Errorf("failed to unmarshal failpoint conf %s: %w", string(data), err) | ||||
| 	} | ||||
|  | ||||
| 	if err := updateFn(&conf); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	data, err = json.Marshal(conf) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to marshal failpoint conf: %w", err) | ||||
| 	} | ||||
| 	return continuity.AtomicWriteFile(c.confPath, data, 0666) | ||||
| } | ||||
|  | ||||
| func nopEvalFn() error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func flock(fd uintptr) error { | ||||
| 	return syscall.Flock(int(fd), syscall.LOCK_EX) | ||||
| } | ||||
|  | ||||
| func unflock(fd uintptr) error { | ||||
| 	return syscall.Flock(int(fd), syscall.LOCK_UN) | ||||
| } | ||||
| @@ -0,0 +1,29 @@ | ||||
| /* | ||||
|    Copyright The containerd 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 main | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"github.com/containerd/containerd/runtime/v2/runc/manager" | ||||
| 	_ "github.com/containerd/containerd/runtime/v2/runc/pause" | ||||
| 	"github.com/containerd/containerd/runtime/v2/shim" | ||||
| ) | ||||
|  | ||||
| func main() { | ||||
| 	shim.RunManager(context.Background(), manager.NewShimManager("io.containerd.runc-fp.v1")) | ||||
| } | ||||
| @@ -0,0 +1,141 @@ | ||||
| /* | ||||
|    Copyright The containerd 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 main | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
|  | ||||
| 	taskapi "github.com/containerd/containerd/api/runtime/task/v2" | ||||
| 	"github.com/containerd/containerd/oci" | ||||
| 	"github.com/containerd/containerd/pkg/failpoint" | ||||
| 	"github.com/containerd/containerd/pkg/shutdown" | ||||
| 	"github.com/containerd/containerd/plugin" | ||||
| 	"github.com/containerd/containerd/runtime/v2/runc/task" | ||||
| 	"github.com/containerd/containerd/runtime/v2/shim" | ||||
| 	"github.com/containerd/ttrpc" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	ociConfigFilename = "config.json" | ||||
|  | ||||
| 	failpointPrefixKey = "io.containerd.runtime.v2.shim.failpoint." | ||||
| ) | ||||
|  | ||||
| func init() { | ||||
| 	plugin.Register(&plugin.Registration{ | ||||
| 		Type: plugin.TTRPCPlugin, | ||||
| 		ID:   "task", | ||||
| 		Requires: []plugin.Type{ | ||||
| 			plugin.EventPlugin, | ||||
| 			plugin.InternalPlugin, | ||||
| 		}, | ||||
| 		InitFn: func(ic *plugin.InitContext) (interface{}, error) { | ||||
| 			pp, err := ic.GetByID(plugin.EventPlugin, "publisher") | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 			ss, err := ic.GetByID(plugin.InternalPlugin, "shutdown") | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 			fps, err := newFailpointFromOCIAnnotation() | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 			service, err := task.NewTaskService(ic.Context, pp.(shim.Publisher), ss.(shutdown.Service)) | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
|  | ||||
| 			return &taskServiceWithFp{ | ||||
| 				fps:   fps, | ||||
| 				local: service, | ||||
| 			}, nil | ||||
| 		}, | ||||
| 	}) | ||||
|  | ||||
| } | ||||
|  | ||||
| type taskServiceWithFp struct { | ||||
| 	fps   map[string]*failpoint.Failpoint | ||||
| 	local taskapi.TaskService | ||||
| } | ||||
|  | ||||
| func (s *taskServiceWithFp) RegisterTTRPC(server *ttrpc.Server) error { | ||||
| 	taskapi.RegisterTaskService(server, s.local) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (s *taskServiceWithFp) UnaryInterceptor() ttrpc.UnaryServerInterceptor { | ||||
| 	return func(ctx context.Context, unmarshal ttrpc.Unmarshaler, info *ttrpc.UnaryServerInfo, method ttrpc.Method) (interface{}, error) { | ||||
| 		methodName := filepath.Base(info.FullMethod) | ||||
| 		if fp, ok := s.fps[methodName]; ok { | ||||
| 			if err := fp.Evaluate(); err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 		} | ||||
| 		return method(ctx, unmarshal) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // newFailpointFromOCIAnnotation reloads and parses the annotation from | ||||
| // bundle-path/config.json. | ||||
| // | ||||
| // The annotation controlling task API's failpoint should be like: | ||||
| // | ||||
| //	io.containerd.runtime.v2.shim.failpoint.Create = 1*off->1*error(please retry) | ||||
| // | ||||
| // The `Create` is the shim unary API and the value of annotation is the | ||||
| // failpoint control. The function will return a set of failpoint controllers. | ||||
| func newFailpointFromOCIAnnotation() (map[string]*failpoint.Failpoint, error) { | ||||
| 	// NOTE: shim's current working dir is in bundle dir. | ||||
| 	cwd, err := os.Getwd() | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to get current working dir: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	configPath := filepath.Join(cwd, ociConfigFilename) | ||||
| 	data, err := os.ReadFile(configPath) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to read %v: %w", configPath, err) | ||||
| 	} | ||||
|  | ||||
| 	var spec oci.Spec | ||||
| 	if err := json.Unmarshal(data, &spec); err != nil { | ||||
| 		return nil, fmt.Errorf("failed to parse oci.Spec(%v): %w", string(data), err) | ||||
| 	} | ||||
|  | ||||
| 	res := make(map[string]*failpoint.Failpoint) | ||||
| 	for k, v := range spec.Annotations { | ||||
| 		if !strings.HasPrefix(k, failpointPrefixKey) { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		methodName := strings.TrimPrefix(k, failpointPrefixKey) | ||||
| 		fp, err := failpoint.NewFailpoint(methodName, v) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("failed to parse failpoint %v: %w", v, err) | ||||
| 		} | ||||
| 		res[methodName] = fp | ||||
| 	} | ||||
| 	return res, nil | ||||
| } | ||||
| @@ -174,7 +174,8 @@ func PodSandboxConfig(name, ns string, opts ...PodSandboxOpts) *runtime.PodSandb | ||||
| 			Uid:       util.GenerateID(), | ||||
| 			Namespace: Randomize(ns), | ||||
| 		}, | ||||
| 		Linux: &runtime.LinuxPodSandboxConfig{}, | ||||
| 		Linux:       &runtime.LinuxPodSandboxConfig{}, | ||||
| 		Annotations: make(map[string]string), | ||||
| 	} | ||||
| 	for _, opt := range opts { | ||||
| 		opt(config) | ||||
|   | ||||
							
								
								
									
										122
									
								
								integration/sandbox_run_rollback_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								integration/sandbox_run_rollback_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,122 @@ | ||||
| //go:build linux | ||||
| // +build linux | ||||
|  | ||||
| /* | ||||
|    Copyright The containerd 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 integration | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"runtime" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
|  | ||||
| 	criapiv1 "k8s.io/cri-api/pkg/apis/runtime/v1" | ||||
|  | ||||
| 	"github.com/containerd/containerd/pkg/failpoint" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	failpointRuntimeHandler = "runc-fp" | ||||
|  | ||||
| 	failpointShimPrefixKey = "io.containerd.runtime.v2.shim.failpoint." | ||||
|  | ||||
| 	failpointCNIConfPathKey = "failpoint.cni.containerd.io/confpath" | ||||
| ) | ||||
|  | ||||
| func TestRunPodSandboxWithSetupCNIFailure(t *testing.T) { | ||||
| 	if runtime.GOOS != "linux" { | ||||
| 		t.Skip() | ||||
| 	} | ||||
|  | ||||
| 	t.Logf("Inject CNI failpoint") | ||||
| 	conf := &failpointConf{ | ||||
| 		Add: "1*error(you-shall-not-pass!)", | ||||
| 	} | ||||
|  | ||||
| 	sbConfig := PodSandboxConfig(t.Name(), "failpoint") | ||||
| 	injectCNIFailpoint(t, sbConfig, conf) | ||||
|  | ||||
| 	t.Logf("Create a sandbox") | ||||
| 	_, err := runtimeService.RunPodSandbox(sbConfig, failpointRuntimeHandler) | ||||
| 	require.Error(t, err) | ||||
| 	require.Equal(t, true, strings.Contains(err.Error(), "you-shall-not-pass!")) | ||||
|  | ||||
| 	t.Logf("Retry to create sandbox with same config") | ||||
| 	sb, err := runtimeService.RunPodSandbox(sbConfig, failpointRuntimeHandler) | ||||
| 	require.NoError(t, err) | ||||
|  | ||||
| 	err = runtimeService.StopPodSandbox(sb) | ||||
| 	require.NoError(t, err) | ||||
|  | ||||
| 	err = runtimeService.RemovePodSandbox(sb) | ||||
| 	require.NoError(t, err) | ||||
| } | ||||
|  | ||||
| func TestRunPodSandboxWithShimStartFailure(t *testing.T) { | ||||
| 	if runtime.GOOS != "linux" { | ||||
| 		t.Skip() | ||||
| 	} | ||||
|  | ||||
| 	t.Logf("Inject Shim failpoint") | ||||
|  | ||||
| 	sbConfig := PodSandboxConfig(t.Name(), "failpoint") | ||||
| 	injectShimFailpoint(t, sbConfig, map[string]string{ | ||||
| 		"Start": "1*error(no hard feelings)", | ||||
| 	}) | ||||
|  | ||||
| 	t.Logf("Create a sandbox") | ||||
| 	_, err := runtimeService.RunPodSandbox(sbConfig, failpointRuntimeHandler) | ||||
| 	require.Error(t, err) | ||||
| 	require.Equal(t, true, strings.Contains(err.Error(), "no hard feelings")) | ||||
| } | ||||
|  | ||||
| // failpointConf is used to describe cmdAdd/cmdDel/cmdCheck command's failpoint. | ||||
| type failpointConf struct { | ||||
| 	Add   string `json:"cmdAdd"` | ||||
| 	Del   string `json:"cmdDel"` | ||||
| 	Check string `json:"cmdCheck"` | ||||
| } | ||||
|  | ||||
| func injectCNIFailpoint(t *testing.T, sbConfig *criapiv1.PodSandboxConfig, conf *failpointConf) { | ||||
| 	stateDir := t.TempDir() | ||||
|  | ||||
| 	metadata := sbConfig.Metadata | ||||
| 	fpFilename := filepath.Join(stateDir, | ||||
| 		fmt.Sprintf("%s-%s.json", metadata.Namespace, metadata.Name)) | ||||
|  | ||||
| 	data, err := json.Marshal(conf) | ||||
| 	require.NoError(t, err) | ||||
|  | ||||
| 	err = os.WriteFile(fpFilename, data, 0666) | ||||
| 	require.NoError(t, err) | ||||
|  | ||||
| 	sbConfig.Annotations[failpointCNIConfPathKey] = fpFilename | ||||
| } | ||||
|  | ||||
| func injectShimFailpoint(t *testing.T, sbConfig *criapiv1.PodSandboxConfig, methodFps map[string]string) { | ||||
| 	for method, fp := range methodFps { | ||||
| 		_, err := failpoint.NewFailpoint(method, fp) | ||||
| 		require.NoError(t, err, "check failpoint %s for shim method %s", fp, method) | ||||
|  | ||||
| 		sbConfig.Annotations[failpointShimPrefixKey+method] = fp | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										310
									
								
								pkg/failpoint/fail.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										310
									
								
								pkg/failpoint/fail.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,310 @@ | ||||
| /* | ||||
|    Copyright The containerd 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 failpoint provides the code point in the path, which can be controlled | ||||
| // by user's variable. | ||||
| // | ||||
| // Inspired by FreeBSD fail(9): https://freebsd.org/cgi/man.cgi?query=fail. | ||||
| package failpoint | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"fmt" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| // EvalFn is the func type about delegated evaluation. | ||||
| type EvalFn func() error | ||||
|  | ||||
| // Type is the type of failpoint to specifies which action to take. | ||||
| type Type int | ||||
|  | ||||
| const ( | ||||
| 	// TypeInvalid is invalid type | ||||
| 	TypeInvalid Type = iota | ||||
| 	// TypeOff takes no action | ||||
| 	TypeOff | ||||
| 	// TypeError triggers failpoint error with specified argument | ||||
| 	TypeError | ||||
| 	// TypePanic triggers panic with specified argument | ||||
| 	TypePanic | ||||
| 	// TypeDelay sleeps with the specified number of milliseconds | ||||
| 	TypeDelay | ||||
| ) | ||||
|  | ||||
| // String returns the name of type. | ||||
| func (t Type) String() string { | ||||
| 	switch t { | ||||
| 	case TypeOff: | ||||
| 		return "off" | ||||
| 	case TypeError: | ||||
| 		return "error" | ||||
| 	case TypePanic: | ||||
| 		return "panic" | ||||
| 	case TypeDelay: | ||||
| 		return "delay" | ||||
| 	default: | ||||
| 		return "invalid" | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Failpoint is used to add code points where error or panic may be injected by | ||||
| // user. The user controlled variable will be parsed for how the error injected | ||||
| // code should fire. There is the way to set the rule for failpoint. | ||||
| // | ||||
| // 	<count>*<type>[(arg)][-><more terms>] | ||||
| // | ||||
| // The <type> argument specifies which action to take; it can be one of: | ||||
| // | ||||
| //	off:	Takes no action (does not trigger failpoint and no argument) | ||||
| //	error:	Triggers failpoint error with specified argument(string) | ||||
| //	panic:	Triggers panic with specified argument(string) | ||||
| //	delay:	Sleep the specified number of milliseconds | ||||
| // | ||||
| // The <count>* modifiers prior to <type> control when <type> is executed. For | ||||
| // example, "5*error(oops)" means "return error oops 5 times total". The | ||||
| // operator -> can be used to express cascading terms. If you specify | ||||
| // <term1>-><term2>, it means that if <term1> does not execute, <term2> will | ||||
| // be evaluated. If you want the error injected code should fire in second | ||||
| // call, you can specify "1*off->1*error(oops)". | ||||
| // | ||||
| // Inspired by FreeBSD fail(9): https://freebsd.org/cgi/man.cgi?query=fail. | ||||
| type Failpoint struct { | ||||
| 	sync.Mutex | ||||
|  | ||||
| 	fnName  string | ||||
| 	entries []*failpointEntry | ||||
| } | ||||
|  | ||||
| // NewFailpoint returns failpoint control. | ||||
| func NewFailpoint(fnName string, terms string) (*Failpoint, error) { | ||||
| 	entries, err := parseTerms([]byte(terms)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return &Failpoint{ | ||||
| 		fnName:  fnName, | ||||
| 		entries: entries, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| // Evaluate evaluates a failpoint. | ||||
| func (fp *Failpoint) Evaluate() error { | ||||
| 	fn := fp.DelegatedEval() | ||||
| 	return fn() | ||||
| } | ||||
|  | ||||
| // DelegatedEval evaluates a failpoint but delegates to caller to fire that. | ||||
| func (fp *Failpoint) DelegatedEval() EvalFn { | ||||
| 	var target *failpointEntry | ||||
|  | ||||
| 	func() { | ||||
| 		fp.Lock() | ||||
| 		defer fp.Unlock() | ||||
|  | ||||
| 		for _, entry := range fp.entries { | ||||
| 			if entry.count == 0 { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			entry.count-- | ||||
| 			target = entry | ||||
| 			break | ||||
| 		} | ||||
| 	}() | ||||
|  | ||||
| 	if target == nil { | ||||
| 		return nopEvalFn | ||||
| 	} | ||||
| 	return target.evaluate | ||||
| } | ||||
|  | ||||
| // Failpoint returns the current state of control in string format. | ||||
| func (fp *Failpoint) Marshal() string { | ||||
| 	fp.Lock() | ||||
| 	defer fp.Unlock() | ||||
|  | ||||
| 	res := make([]string, 0, len(fp.entries)) | ||||
| 	for _, entry := range fp.entries { | ||||
| 		res = append(res, entry.marshal()) | ||||
| 	} | ||||
| 	return strings.Join(res, "->") | ||||
| } | ||||
|  | ||||
| type failpointEntry struct { | ||||
| 	typ   Type | ||||
| 	arg   interface{} | ||||
| 	count int64 | ||||
| } | ||||
|  | ||||
| func newFailpointEntry() *failpointEntry { | ||||
| 	return &failpointEntry{ | ||||
| 		typ:   TypeInvalid, | ||||
| 		count: 0, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (fpe *failpointEntry) marshal() string { | ||||
| 	base := fmt.Sprintf("%d*%s", fpe.count, fpe.typ) | ||||
| 	switch fpe.typ { | ||||
| 	case TypeOff: | ||||
| 		return base | ||||
| 	case TypeError, TypePanic: | ||||
| 		return fmt.Sprintf("%s(%s)", base, fpe.arg.(string)) | ||||
| 	case TypeDelay: | ||||
| 		return fmt.Sprintf("%s(%d)", base, fpe.arg.(time.Duration)/time.Millisecond) | ||||
| 	default: | ||||
| 		return base | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (fpe *failpointEntry) evaluate() error { | ||||
| 	switch fpe.typ { | ||||
| 	case TypeOff: | ||||
| 		return nil | ||||
| 	case TypeError: | ||||
| 		return fmt.Errorf("%v", fpe.arg) | ||||
| 	case TypePanic: | ||||
| 		panic(fpe.arg) | ||||
| 	case TypeDelay: | ||||
| 		time.Sleep(fpe.arg.(time.Duration)) | ||||
| 		return nil | ||||
| 	default: | ||||
| 		panic("invalid failpoint type") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func parseTerms(term []byte) ([]*failpointEntry, error) { | ||||
| 	var entry *failpointEntry | ||||
| 	var err error | ||||
|  | ||||
| 	// count*type[(arg)] | ||||
| 	term, entry, err = parseTerm(term) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	res := []*failpointEntry{entry} | ||||
|  | ||||
| 	// cascading terms | ||||
| 	for len(term) > 0 { | ||||
| 		if !bytes.HasPrefix(term, []byte("->")) { | ||||
| 			return nil, fmt.Errorf("invalid cascading terms: %s", string(term)) | ||||
| 		} | ||||
|  | ||||
| 		term = term[2:] | ||||
| 		term, entry, err = parseTerm(term) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("failed to parse cascading term: %w", err) | ||||
| 		} | ||||
|  | ||||
| 		res = append(res, entry) | ||||
| 	} | ||||
| 	return res, nil | ||||
| } | ||||
|  | ||||
| func parseTerm(term []byte) ([]byte, *failpointEntry, error) { | ||||
| 	var err error | ||||
| 	var entry = newFailpointEntry() | ||||
|  | ||||
| 	// count* | ||||
| 	term, err = parseInt64(term, '*', &entry.count) | ||||
| 	if err != nil { | ||||
| 		return nil, nil, err | ||||
| 	} | ||||
|  | ||||
| 	// type[(arg)] | ||||
| 	term, err = parseType(term, entry) | ||||
| 	return term, entry, err | ||||
| } | ||||
|  | ||||
| func parseType(term []byte, entry *failpointEntry) ([]byte, error) { | ||||
| 	var nameToTyp = map[string]Type{ | ||||
| 		"off":    TypeOff, | ||||
| 		"error(": TypeError, | ||||
| 		"panic(": TypePanic, | ||||
| 		"delay(": TypeDelay, | ||||
| 	} | ||||
|  | ||||
| 	var found bool | ||||
| 	for name, typ := range nameToTyp { | ||||
| 		if bytes.HasPrefix(term, []byte(name)) { | ||||
| 			found = true | ||||
| 			term = term[len(name):] | ||||
| 			entry.typ = typ | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if !found { | ||||
| 		return nil, fmt.Errorf("invalid type format: %s", string(term)) | ||||
| 	} | ||||
|  | ||||
| 	switch entry.typ { | ||||
| 	case TypePanic, TypeError: | ||||
| 		endIdx := bytes.IndexByte(term, ')') | ||||
| 		if endIdx <= 0 { | ||||
| 			return nil, fmt.Errorf("invalid argument for %s type", entry.typ) | ||||
| 		} | ||||
| 		entry.arg = string(term[:endIdx]) | ||||
| 		return term[endIdx+1:], nil | ||||
| 	case TypeOff: | ||||
| 		// do nothing | ||||
| 		return term, nil | ||||
| 	case TypeDelay: | ||||
| 		var msVal int64 | ||||
| 		var err error | ||||
|  | ||||
| 		term, err = parseInt64(term, ')', &msVal) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		entry.arg = time.Millisecond * time.Duration(msVal) | ||||
| 		return term, nil | ||||
| 	default: | ||||
| 		panic("unreachable") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func parseInt64(term []byte, terminate byte, val *int64) ([]byte, error) { | ||||
| 	i := 0 | ||||
|  | ||||
| 	for ; i < len(term); i++ { | ||||
| 		if b := term[i]; b < '0' || b > '9' { | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if i == 0 || i == len(term) || term[i] != terminate { | ||||
| 		return nil, fmt.Errorf("failed to parse int64 because of invalid terminate byte: %s", string(term)) | ||||
| 	} | ||||
|  | ||||
| 	v, err := strconv.ParseInt(string(term[:i]), 10, 64) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to parse int64 from %s: %v", string(term[:i]), err) | ||||
| 	} | ||||
|  | ||||
| 	*val = v | ||||
| 	return term[i+1:], nil | ||||
| } | ||||
|  | ||||
| func nopEvalFn() error { | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										134
									
								
								pkg/failpoint/fail_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								pkg/failpoint/fail_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,134 @@ | ||||
| /* | ||||
|    Copyright The containerd 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 failpoint | ||||
|  | ||||
| import ( | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| func TestParseTerms(t *testing.T) { | ||||
| 	cases := []struct { | ||||
| 		terms    string | ||||
| 		hasError bool | ||||
| 	}{ | ||||
| 		// off | ||||
| 		{"5", true}, | ||||
| 		{"*off()", true}, | ||||
| 		{"5*off()", true}, | ||||
| 		{"5*off(nothing)", true}, | ||||
| 		{"5*off(", true}, | ||||
| 		{"5*off", false}, | ||||
|  | ||||
| 		// error | ||||
| 		{"10000error(oops)", true}, | ||||
| 		{"10*error(oops)", false}, | ||||
| 		{"1234*error(oops))", true}, | ||||
| 		{"12342*error()", true}, | ||||
|  | ||||
| 		// panic | ||||
| 		{"1panic(oops)", true}, | ||||
| 		{"1000000*panic(oops)", false}, | ||||
| 		{"12345*panic(oops))", true}, | ||||
| 		{"12*panic()", true}, | ||||
|  | ||||
| 		// delay | ||||
| 		{"1*delay(oops)", true}, | ||||
| 		{"1000000*delay(-1)", true}, | ||||
| 		{"1000000*delay(1)", false}, | ||||
|  | ||||
| 		// cascading terms | ||||
| 		{"1*delay(1)-", true}, | ||||
| 		{"10*delay(2)->", true}, | ||||
| 		{"11*delay(3)->10*off(", true}, | ||||
| 		{"12*delay(4)->10*of", true}, | ||||
| 		{"13*delay(5)->10*off->1000*panic(oops)", false}, | ||||
| 	} | ||||
|  | ||||
| 	for i, c := range cases { | ||||
| 		fp, err := NewFailpoint(t.Name(), c.terms) | ||||
|  | ||||
| 		if (err != nil && !c.hasError) || | ||||
| 			(err == nil && c.hasError) { | ||||
|  | ||||
| 			t.Fatalf("[%v - %s] expected hasError=%v, but got %v", i, c.terms, c.hasError, err) | ||||
| 		} | ||||
|  | ||||
| 		if err != nil { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		if got := fp.Marshal(); !reflect.DeepEqual(got, c.terms) { | ||||
| 			t.Fatalf("[%v] expected %v, but got %v", i, c.terms, got) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestEvaluate(t *testing.T) { | ||||
| 	terms := "1*error(oops-)->1*off->1*delay(1000)->1*panic(panic)" | ||||
|  | ||||
| 	fp, err := NewFailpoint(t.Name(), terms) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("unexpected error %v", err) | ||||
| 	} | ||||
|  | ||||
| 	injectedFn := func() error { | ||||
| 		if err := fp.Evaluate(); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	// should return oops- error | ||||
| 	if err := injectedFn(); err == nil || err.Error() != "oops-" { | ||||
| 		t.Fatalf("expected error %v, but got %v", "oops-", err) | ||||
| 	} | ||||
|  | ||||
| 	// should return nil | ||||
| 	if err := injectedFn(); err != nil { | ||||
| 		t.Fatalf("expected nil, but got %v", err) | ||||
| 	} | ||||
|  | ||||
| 	// should sleep 1s and return nil | ||||
| 	now := time.Now() | ||||
| 	err = injectedFn() | ||||
| 	du := time.Since(now) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("expected nil, but got %v", err) | ||||
| 	} | ||||
| 	if du < 1*time.Second { | ||||
| 		t.Fatalf("expected sleep 1s, but got %v", du) | ||||
| 	} | ||||
|  | ||||
| 	// should panic | ||||
| 	defer func() { | ||||
| 		if err := recover(); err == nil || err.(string) != "panic" { | ||||
| 			t.Fatalf("should panic(panic), but got %v", err) | ||||
| 		} | ||||
|  | ||||
| 		expected := "0*error(oops-)->0*off->0*delay(1000)->0*panic(panic)" | ||||
| 		if got := fp.Marshal(); got != expected { | ||||
| 			t.Fatalf("expected %v, but got %v", expected, got) | ||||
| 		} | ||||
|  | ||||
| 		if err := injectedFn(); err != nil { | ||||
| 			t.Fatalf("expected nil, but got %v", err) | ||||
| 		} | ||||
| 	}() | ||||
| 	injectedFn() | ||||
| } | ||||
| @@ -108,6 +108,12 @@ type ttrpcService interface { | ||||
| 	RegisterTTRPC(*ttrpc.Server) error | ||||
| } | ||||
|  | ||||
| type ttrpcServerOptioner interface { | ||||
| 	ttrpcService | ||||
|  | ||||
| 	UnaryInterceptor() ttrpc.UnaryServerInterceptor | ||||
| } | ||||
|  | ||||
| type taskService struct { | ||||
| 	shimapi.TaskService | ||||
| } | ||||
| @@ -370,6 +376,8 @@ func run(ctx context.Context, manager Manager, initFunc Init, name string, confi | ||||
| 	var ( | ||||
| 		initialized   = plugin.NewPluginSet() | ||||
| 		ttrpcServices = []ttrpcService{} | ||||
|  | ||||
| 		ttrpcUnaryInterceptors = []ttrpc.UnaryServerInterceptor{} | ||||
| 	) | ||||
| 	plugins := plugin.Graph(func(*plugin.Registration) bool { return false }) | ||||
| 	for _, p := range plugins { | ||||
| @@ -409,19 +417,28 @@ func run(ctx context.Context, manager Manager, initFunc Init, name string, confi | ||||
| 		if err != nil { | ||||
| 			if plugin.IsSkipPlugin(err) { | ||||
| 				log.G(ctx).WithError(err).WithField("type", p.Type).Infof("skip loading plugin %q...", id) | ||||
| 			} else { | ||||
| 				log.G(ctx).WithError(err).Warnf("failed to load plugin %s", id) | ||||
| 				continue | ||||
| 			} | ||||
| 			continue | ||||
| 			return fmt.Errorf("failed to load plugin %s: %w", id, err) | ||||
| 		} | ||||
|  | ||||
| 		if src, ok := instance.(ttrpcService); ok { | ||||
| 			logrus.WithField("id", id).Debug("registering ttrpc service") | ||||
| 			ttrpcServices = append(ttrpcServices, src) | ||||
|  | ||||
| 		} | ||||
|  | ||||
| 		if src, ok := instance.(ttrpcServerOptioner); ok { | ||||
| 			ttrpcUnaryInterceptors = append(ttrpcUnaryInterceptors, src.UnaryInterceptor()) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	server, err := newServer() | ||||
| 	if len(ttrpcServices) == 0 { | ||||
| 		return fmt.Errorf("required that ttrpc service") | ||||
| 	} | ||||
|  | ||||
| 	unaryInterceptor := chainUnaryServerInterceptors(ttrpcUnaryInterceptors...) | ||||
| 	server, err := newServer(ttrpc.WithUnaryServerInterceptor(unaryInterceptor)) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed creating server: %w", err) | ||||
| 	} | ||||
|   | ||||
| @@ -18,8 +18,8 @@ package shim | ||||
|  | ||||
| import "github.com/containerd/ttrpc" | ||||
|  | ||||
| func newServer() (*ttrpc.Server, error) { | ||||
| 	return ttrpc.NewServer() | ||||
| func newServer(opts ...ttrpc.ServerOpt) (*ttrpc.Server, error) { | ||||
| 	return ttrpc.NewServer(opts...) | ||||
| } | ||||
|  | ||||
| func subreaper() error { | ||||
|   | ||||
| @@ -18,8 +18,8 @@ package shim | ||||
|  | ||||
| import "github.com/containerd/ttrpc" | ||||
|  | ||||
| func newServer() (*ttrpc.Server, error) { | ||||
| 	return ttrpc.NewServer() | ||||
| func newServer(opts ...ttrpc.ServerOpt) (*ttrpc.Server, error) { | ||||
| 	return ttrpc.NewServer(opts...) | ||||
| } | ||||
|  | ||||
| func subreaper() error { | ||||
|   | ||||
| @@ -21,8 +21,9 @@ import ( | ||||
| 	"github.com/containerd/ttrpc" | ||||
| ) | ||||
|  | ||||
| func newServer() (*ttrpc.Server, error) { | ||||
| 	return ttrpc.NewServer(ttrpc.WithServerHandshaker(ttrpc.UnixSocketRequireSameUser())) | ||||
| func newServer(opts ...ttrpc.ServerOpt) (*ttrpc.Server, error) { | ||||
| 	opts = append(opts, ttrpc.WithServerHandshaker(ttrpc.UnixSocketRequireSameUser())) | ||||
| 	return ttrpc.NewServer(opts...) | ||||
| } | ||||
|  | ||||
| func subreaper() error { | ||||
|   | ||||
| @@ -31,7 +31,7 @@ func setupSignals(config Config) (chan os.Signal, error) { | ||||
| 	return nil, errors.New("not supported") | ||||
| } | ||||
|  | ||||
| func newServer() (*ttrpc.Server, error) { | ||||
| func newServer(opts ...ttrpc.ServerOpt) (*ttrpc.Server, error) { | ||||
| 	return nil, errors.New("not supported") | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -30,6 +30,7 @@ import ( | ||||
| 	"github.com/containerd/containerd/namespaces" | ||||
| 	"github.com/containerd/containerd/protobuf/proto" | ||||
| 	"github.com/containerd/containerd/protobuf/types" | ||||
| 	"github.com/containerd/ttrpc" | ||||
| 	exec "golang.org/x/sys/execabs" | ||||
| ) | ||||
|  | ||||
| @@ -167,3 +168,28 @@ func ReadAddress(path string) (string, error) { | ||||
| 	} | ||||
| 	return string(data), nil | ||||
| } | ||||
|  | ||||
| // chainUnaryServerInterceptors creates a single ttrpc server interceptor from | ||||
| // a chain of many interceptors executed from first to last. | ||||
| func chainUnaryServerInterceptors(interceptors ...ttrpc.UnaryServerInterceptor) ttrpc.UnaryServerInterceptor { | ||||
| 	n := len(interceptors) | ||||
|  | ||||
| 	// force to use default interceptor in ttrpc | ||||
| 	if n == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	return func(ctx context.Context, unmarshal ttrpc.Unmarshaler, info *ttrpc.UnaryServerInfo, method ttrpc.Method) (interface{}, error) { | ||||
| 		currentMethod := method | ||||
|  | ||||
| 		for i := n - 1; i > 0; i-- { | ||||
| 			interceptor := interceptors[i] | ||||
| 			innerMethod := currentMethod | ||||
|  | ||||
| 			currentMethod = func(currentCtx context.Context, currentUnmarshal func(interface{}) error) (interface{}, error) { | ||||
| 				return interceptor(currentCtx, currentUnmarshal, info, innerMethod) | ||||
| 			} | ||||
| 		} | ||||
| 		return interceptors[0](ctx, unmarshal, info, currentMethod) | ||||
| 	} | ||||
| } | ||||
|   | ||||
							
								
								
									
										118
									
								
								runtime/v2/shim/util_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								runtime/v2/shim/util_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,118 @@ | ||||
| /* | ||||
|    Copyright The containerd 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 shim | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"path/filepath" | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/containerd/ttrpc" | ||||
| ) | ||||
|  | ||||
| func TestChainUnaryServerInterceptors(t *testing.T) { | ||||
| 	methodInfo := &ttrpc.UnaryServerInfo{ | ||||
| 		FullMethod: filepath.Join("/", t.Name(), "foo"), | ||||
| 	} | ||||
|  | ||||
| 	type callKey struct{} | ||||
| 	callValue := "init" | ||||
| 	callCtx := context.WithValue(context.Background(), callKey{}, callValue) | ||||
|  | ||||
| 	verifyCallCtxFn := func(ctx context.Context, key interface{}, expected interface{}) { | ||||
| 		got := ctx.Value(key) | ||||
| 		if !reflect.DeepEqual(expected, got) { | ||||
| 			t.Fatalf("[context(key:%s) expected %v, but got %v", key, expected, got) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	verifyInfoFn := func(info *ttrpc.UnaryServerInfo) { | ||||
| 		if !reflect.DeepEqual(methodInfo, info) { | ||||
| 			t.Fatalf("[info] expected %+v, but got %+v", methodInfo, info) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	origUnmarshaler := func(obj interface{}) error { | ||||
| 		v := obj.(*int64) | ||||
| 		*v *= 2 | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	type firstKey struct{} | ||||
| 	firstValue := "from first" | ||||
| 	var firstUnmarshaler ttrpc.Unmarshaler | ||||
| 	first := func(ctx context.Context, unmarshal ttrpc.Unmarshaler, info *ttrpc.UnaryServerInfo, method ttrpc.Method) (interface{}, error) { | ||||
| 		verifyCallCtxFn(ctx, callKey{}, callValue) | ||||
| 		verifyInfoFn(info) | ||||
|  | ||||
| 		ctx = context.WithValue(ctx, firstKey{}, firstValue) | ||||
|  | ||||
| 		firstUnmarshaler = func(obj interface{}) error { | ||||
| 			if err := unmarshal(obj); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			v := obj.(*int64) | ||||
| 			*v *= 2 | ||||
| 			return nil | ||||
| 		} | ||||
|  | ||||
| 		return method(ctx, firstUnmarshaler) | ||||
| 	} | ||||
|  | ||||
| 	type secondKey struct{} | ||||
| 	secondValue := "from second" | ||||
| 	second := func(ctx context.Context, unmarshal ttrpc.Unmarshaler, info *ttrpc.UnaryServerInfo, method ttrpc.Method) (interface{}, error) { | ||||
| 		verifyCallCtxFn(ctx, callKey{}, callValue) | ||||
| 		verifyCallCtxFn(ctx, firstKey{}, firstValue) | ||||
| 		verifyInfoFn(info) | ||||
|  | ||||
| 		v := int64(3) // should return 12 | ||||
| 		if err := unmarshal(&v); err != nil { | ||||
| 			t.Fatalf("unexpected error %v", err) | ||||
| 		} | ||||
| 		if expected := int64(12); v != expected { | ||||
| 			t.Fatalf("expected int64(%v), but got %v", expected, v) | ||||
| 		} | ||||
|  | ||||
| 		ctx = context.WithValue(ctx, secondKey{}, secondValue) | ||||
| 		return method(ctx, unmarshal) | ||||
| 	} | ||||
|  | ||||
| 	methodFn := func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) { | ||||
| 		verifyCallCtxFn(ctx, callKey{}, callValue) | ||||
| 		verifyCallCtxFn(ctx, firstKey{}, firstValue) | ||||
| 		verifyCallCtxFn(ctx, secondKey{}, secondValue) | ||||
|  | ||||
| 		v := int64(2) | ||||
| 		if err := unmarshal(&v); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		return v, nil | ||||
| 	} | ||||
|  | ||||
| 	interceptor := chainUnaryServerInterceptors(first, second) | ||||
| 	v, err := interceptor(callCtx, origUnmarshaler, methodInfo, methodFn) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("expected nil, but got %v", err) | ||||
| 	} | ||||
|  | ||||
| 	if expected := int64(8); v != expected { | ||||
| 		t.Fatalf("expected result is int64(%v), but got %v", expected, v) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										35
									
								
								script/setup/install-failpoint-binaries
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										35
									
								
								script/setup/install-failpoint-binaries
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| #!/usr/bin/env bash | ||||
|  | ||||
| #   Copyright The containerd 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 and install | ||||
| # | ||||
| # * cni-bridge-fp into /opt/cni/bin | ||||
| # * containerd-shim-runc-fp-v1 into /usr/local/bin | ||||
| # | ||||
| set -euo pipefail | ||||
|  | ||||
| base_dir="$(dirname "${BASH_SOURCE[0]}")" | ||||
| root_dir="$( cd "${base_dir}" && pwd )"/../.. | ||||
|  | ||||
| cd "${root_dir}" | ||||
|  | ||||
| CNI_BIN_DIR=${CNI_BIN_DIR:-"/opt/cni/bin"} | ||||
| make bin/cni-bridge-fp | ||||
| sudo install bin/cni-bridge-fp "${CNI_BIN_DIR}" | ||||
|  | ||||
| SHIM_BIN_DIR=${SHIM_BIN_DIR:-"/usr/local/bin"} | ||||
| make bin/containerd-shim-runc-fp-v1 | ||||
| sudo install bin/containerd-shim-runc-fp-v1 "${SHIM_BIN_DIR}" | ||||
| @@ -35,9 +35,10 @@ CONTAINERD_RUNTIME=${CONTAINERD_RUNTIME:-""} | ||||
| if [ -z "${CONTAINERD_CONFIG_FILE}" ]; then | ||||
|   config_file="${CONTAINERD_CONFIG_DIR}/containerd-config-cri.toml" | ||||
|   truncate --size 0 "${config_file}" | ||||
|   echo "version=2" >> ${config_file} | ||||
|  | ||||
|   if command -v sestatus >/dev/null 2>&1; then | ||||
|     cat >>${config_file} <<EOF | ||||
| version=2 | ||||
| [plugins."io.containerd.grpc.v1.cri"] | ||||
|   enable_selinux = true | ||||
| EOF | ||||
| @@ -51,6 +52,59 @@ EOF | ||||
|   CONTAINERD_CONFIG_FILE="${config_file}" | ||||
| fi | ||||
|  | ||||
| if [ $IS_WINDOWS -eq 0 ]; then | ||||
|   FAILPOINT_CONTAINERD_RUNTIME="runc-fp.v1" | ||||
|   FAILPOINT_CNI_CONF_DIR=${FAILPOINT_CNI_CONF_DIR:-"/tmp/failpoint-cni-net.d"} | ||||
|   mkdir -p "${FAILPOINT_CNI_CONF_DIR}" | ||||
|  | ||||
|   # Add runtime with failpoint | ||||
|   cat << EOF | tee -a "${CONTAINERD_CONFIG_FILE}" | ||||
| [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc-fp] | ||||
|   cni_conf_dir = "${FAILPOINT_CNI_CONF_DIR}" | ||||
|   cni_max_conf_num = 1 | ||||
|   pod_annotations = ["io.containerd.runtime.v2.shim.failpoint.*"] | ||||
|   runtime_type = "${FAILPOINT_CONTAINERD_RUNTIME}" | ||||
| EOF | ||||
|  | ||||
|   cat << EOF | tee "${FAILPOINT_CNI_CONF_DIR}/10-containerd-net.conflist" | ||||
| { | ||||
|   "cniVersion": "1.0.0", | ||||
|   "name": "containerd-net-failpoint", | ||||
|   "plugins": [ | ||||
|     { | ||||
|       "type": "cni-bridge-fp", | ||||
|       "bridge": "cni-fp", | ||||
|       "isGateway": true, | ||||
|       "ipMasq": true, | ||||
|       "promiscMode": true, | ||||
|       "ipam": { | ||||
|         "type": "host-local", | ||||
|         "ranges": [ | ||||
|           [{ | ||||
|             "subnet": "10.88.0.0/16" | ||||
|           }], | ||||
|           [{ | ||||
|             "subnet": "2001:4860:4860::/64" | ||||
|           }] | ||||
|         ], | ||||
|         "routes": [ | ||||
|           { "dst": "0.0.0.0/0" }, | ||||
|           { "dst": "::/0" } | ||||
|         ] | ||||
|       }, | ||||
|       "capabilities": { | ||||
|         "io.kubernetes.cri.pod-annotations": true | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "type": "portmap", | ||||
|       "capabilities": {"portMappings": true} | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| EOF | ||||
| fi | ||||
|  | ||||
| # CONTAINERD_TEST_SUFFIX is the suffix appended to the root/state directory used | ||||
| # by test containerd. | ||||
| CONTAINERD_TEST_SUFFIX=${CONTAINERD_TEST_SUFFIX:-"-test"} | ||||
| @@ -183,6 +237,8 @@ test_setup() { | ||||
|   fi | ||||
|   readiness_check run_ctr | ||||
|   readiness_check run_crictl | ||||
|   # Show the config about cri plugin in log when it's ready | ||||
|   run_crictl | ||||
| } | ||||
|  | ||||
| # test_teardown kills containerd. | ||||
|   | ||||
							
								
								
									
										294
									
								
								vendor/github.com/containernetworking/cni/pkg/skel/skel.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										294
									
								
								vendor/github.com/containernetworking/cni/pkg/skel/skel.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,294 @@ | ||||
| // 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 ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 	"log" | ||||
| 	"os" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/containernetworking/cni/pkg/types" | ||||
| 	"github.com/containernetworking/cni/pkg/utils" | ||||
| 	"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, *types.Error) { | ||||
| 	var cmd, contID, netns, ifName, args, path string | ||||
|  | ||||
| 	vars := []struct { | ||||
| 		name      string | ||||
| 		val       *string | ||||
| 		reqForCmd reqForCmdEntry | ||||
| 	}{ | ||||
| 		{ | ||||
| 			"CNI_COMMAND", | ||||
| 			&cmd, | ||||
| 			reqForCmdEntry{ | ||||
| 				"ADD":   true, | ||||
| 				"CHECK": true, | ||||
| 				"DEL":   true, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"CNI_CONTAINERID", | ||||
| 			&contID, | ||||
| 			reqForCmdEntry{ | ||||
| 				"ADD":   true, | ||||
| 				"CHECK": true, | ||||
| 				"DEL":   true, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"CNI_NETNS", | ||||
| 			&netns, | ||||
| 			reqForCmdEntry{ | ||||
| 				"ADD":   true, | ||||
| 				"CHECK": true, | ||||
| 				"DEL":   false, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"CNI_IFNAME", | ||||
| 			&ifName, | ||||
| 			reqForCmdEntry{ | ||||
| 				"ADD":   true, | ||||
| 				"CHECK": true, | ||||
| 				"DEL":   true, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"CNI_ARGS", | ||||
| 			&args, | ||||
| 			reqForCmdEntry{ | ||||
| 				"ADD":   false, | ||||
| 				"CHECK": false, | ||||
| 				"DEL":   false, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"CNI_PATH", | ||||
| 			&path, | ||||
| 			reqForCmdEntry{ | ||||
| 				"ADD":   true, | ||||
| 				"CHECK": true, | ||||
| 				"DEL":   true, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	argsMissing := make([]string, 0) | ||||
| 	for _, v := range vars { | ||||
| 		*v.val = t.Getenv(v.name) | ||||
| 		if *v.val == "" { | ||||
| 			if v.reqForCmd[cmd] || v.name == "CNI_COMMAND" { | ||||
| 				argsMissing = append(argsMissing, v.name) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if len(argsMissing) > 0 { | ||||
| 		joined := strings.Join(argsMissing, ",") | ||||
| 		return "", nil, types.NewError(types.ErrInvalidEnvironmentVariables, fmt.Sprintf("required env variables [%s] missing", joined), "") | ||||
| 	} | ||||
|  | ||||
| 	if cmd == "VERSION" { | ||||
| 		t.Stdin = bytes.NewReader(nil) | ||||
| 	} | ||||
|  | ||||
| 	stdinData, err := ioutil.ReadAll(t.Stdin) | ||||
| 	if err != nil { | ||||
| 		return "", nil, types.NewError(types.ErrIOFailure, fmt.Sprintf("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 (t *dispatcher) checkVersionAndCall(cmdArgs *CmdArgs, pluginVersionInfo version.PluginInfo, toCall func(*CmdArgs) error) *types.Error { | ||||
| 	configVersion, err := t.ConfVersionDecoder.Decode(cmdArgs.StdinData) | ||||
| 	if err != nil { | ||||
| 		return types.NewError(types.ErrDecodingFailure, err.Error(), "") | ||||
| 	} | ||||
| 	verErr := t.VersionReconciler.Check(configVersion, pluginVersionInfo) | ||||
| 	if verErr != nil { | ||||
| 		return types.NewError(types.ErrIncompatibleCNIVersion, "incompatible CNI versions", verErr.Details()) | ||||
| 	} | ||||
|  | ||||
| 	if err = toCall(cmdArgs); err != nil { | ||||
| 		if e, ok := err.(*types.Error); ok { | ||||
| 			// don't wrap Error in Error | ||||
| 			return e | ||||
| 		} | ||||
| 		return types.NewError(types.ErrInternal, err.Error(), "") | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func validateConfig(jsonBytes []byte) *types.Error { | ||||
| 	var conf struct { | ||||
| 		Name string `json:"name"` | ||||
| 	} | ||||
| 	if err := json.Unmarshal(jsonBytes, &conf); err != nil { | ||||
| 		return types.NewError(types.ErrDecodingFailure, fmt.Sprintf("error unmarshall network config: %v", err), "") | ||||
| 	} | ||||
| 	if conf.Name == "" { | ||||
| 		return types.NewError(types.ErrInvalidNetworkConfig, "missing network name", "") | ||||
| 	} | ||||
| 	if err := utils.ValidateNetworkName(conf.Name); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (t *dispatcher) pluginMain(cmdAdd, cmdCheck, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo, about string) *types.Error { | ||||
| 	cmd, cmdArgs, err := t.getCmdArgsFromEnv() | ||||
| 	if err != nil { | ||||
| 		// Print the about string to stderr when no command is set | ||||
| 		if err.Code == types.ErrInvalidEnvironmentVariables && t.Getenv("CNI_COMMAND") == "" && about != "" { | ||||
| 			_, _ = fmt.Fprintln(t.Stderr, about) | ||||
| 			_, _ = fmt.Fprintf(t.Stderr, "CNI protocol versions supported: %s\n", strings.Join(versionInfo.SupportedVersions(), ", ")) | ||||
| 			return nil | ||||
| 		} | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if cmd != "VERSION" { | ||||
| 		if err = validateConfig(cmdArgs.StdinData); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		if err = utils.ValidateContainerID(cmdArgs.ContainerID); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		if err = utils.ValidateInterfaceName(cmdArgs.IfName); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	switch cmd { | ||||
| 	case "ADD": | ||||
| 		err = t.checkVersionAndCall(cmdArgs, versionInfo, cmdAdd) | ||||
| 	case "CHECK": | ||||
| 		configVersion, err := t.ConfVersionDecoder.Decode(cmdArgs.StdinData) | ||||
| 		if err != nil { | ||||
| 			return types.NewError(types.ErrDecodingFailure, err.Error(), "") | ||||
| 		} | ||||
| 		if gtet, err := version.GreaterThanOrEqualTo(configVersion, "0.4.0"); err != nil { | ||||
| 			return types.NewError(types.ErrDecodingFailure, err.Error(), "") | ||||
| 		} else if !gtet { | ||||
| 			return types.NewError(types.ErrIncompatibleCNIVersion, "config version does not allow CHECK", "") | ||||
| 		} | ||||
| 		for _, pluginVersion := range versionInfo.SupportedVersions() { | ||||
| 			gtet, err := version.GreaterThanOrEqualTo(pluginVersion, configVersion) | ||||
| 			if err != nil { | ||||
| 				return types.NewError(types.ErrDecodingFailure, err.Error(), "") | ||||
| 			} else if gtet { | ||||
| 				if err := t.checkVersionAndCall(cmdArgs, versionInfo, cmdCheck); err != nil { | ||||
| 					return err | ||||
| 				} | ||||
| 				return nil | ||||
| 			} | ||||
| 		} | ||||
| 		return types.NewError(types.ErrIncompatibleCNIVersion, "plugin version does not allow CHECK", "") | ||||
| 	case "DEL": | ||||
| 		err = t.checkVersionAndCall(cmdArgs, versionInfo, cmdDel) | ||||
| 	case "VERSION": | ||||
| 		if err := versionInfo.Encode(t.Stdout); err != nil { | ||||
| 			return types.NewError(types.ErrIOFailure, err.Error(), "") | ||||
| 		} | ||||
| 	default: | ||||
| 		return types.NewError(types.ErrInvalidEnvironmentVariables, fmt.Sprintf("unknown CNI_COMMAND: %v", cmd), "") | ||||
| 	} | ||||
|  | ||||
| 	return err | ||||
| } | ||||
|  | ||||
| // PluginMainWithError is the core "main" for a plugin. It accepts | ||||
| // callback functions for add, check, 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, cmdCheck, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo, about string) *types.Error { | ||||
| 	return (&dispatcher{ | ||||
| 		Getenv: os.Getenv, | ||||
| 		Stdin:  os.Stdin, | ||||
| 		Stdout: os.Stdout, | ||||
| 		Stderr: os.Stderr, | ||||
| 	}).pluginMain(cmdAdd, cmdCheck, cmdDel, versionInfo, about) | ||||
| } | ||||
|  | ||||
| // 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. | ||||
| // | ||||
| // The caller can specify an "about" string, which is printed on stderr | ||||
| // when no CNI_COMMAND is specified. The recommended output is "CNI plugin <foo> v<version>" | ||||
| // | ||||
| // When an error occurs in either cmdAdd, cmdCheck, 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, cmdCheck, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo, about string) { | ||||
| 	if e := PluginMainWithError(cmdAdd, cmdCheck, cmdDel, versionInfo, about); e != nil { | ||||
| 		if err := e.Print(); err != nil { | ||||
| 			log.Print("Error writing error JSON to stdout: ", err) | ||||
| 		} | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										1
									
								
								vendor/modules.txt
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								vendor/modules.txt
									
									
									
									
										vendored
									
									
								
							| @@ -134,6 +134,7 @@ github.com/containerd/zfs/plugin | ||||
| ## explicit; go 1.14 | ||||
| github.com/containernetworking/cni/libcni | ||||
| github.com/containernetworking/cni/pkg/invoke | ||||
| github.com/containernetworking/cni/pkg/skel | ||||
| github.com/containernetworking/cni/pkg/types | ||||
| github.com/containernetworking/cni/pkg/types/020 | ||||
| github.com/containernetworking/cni/pkg/types/040 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Derek McGowan
					Derek McGowan