*: add runc-fp as runc wrapper to inject failpoint
Signed-off-by: Wei Fu <fuweid89@gmail.com>
This commit is contained in:
		
							
								
								
									
										5
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								Makefile
									
									
									
									
									
								
							| @@ -236,6 +236,11 @@ 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 | ||||
|  | ||||
| # build runc-fp as runc wrapper to support failpoint, only used by integration test | ||||
| bin/runc-fp: integration/failpoint/cmd/runc-fp FORCE | ||||
| 	@echo "$(WHALE) $@" | ||||
| 	@$(GO) build ${GO_BUILD_FLAGS} -o $@ ./integration/failpoint/cmd/runc-fp | ||||
|  | ||||
| benchmark: ## run benchmarks tests | ||||
| 	@echo "$(WHALE) $@" | ||||
| 	@$(GO) test ${TESTFLAGS} -bench . -run Benchmark -test.root | ||||
|   | ||||
| @@ -41,7 +41,9 @@ import ( | ||||
| 	"github.com/containerd/containerd/plugin" | ||||
| 	"github.com/containerd/containerd/runtime/v2/runc/options" | ||||
| 	"github.com/containerd/containerd/sys" | ||||
|  | ||||
| 	"github.com/opencontainers/runtime-spec/specs-go" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| 	exec "golang.org/x/sys/execabs" | ||||
| 	"golang.org/x/sys/unix" | ||||
| ) | ||||
| @@ -1417,3 +1419,80 @@ func TestShimOOMScore(t *testing.T) { | ||||
| 	case <-statusC: | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // TestIssue9103 is used as regression case for issue 9103. | ||||
| // | ||||
| // The runc-fp will kill the init process so that the shim should return stopped | ||||
| // status after container.NewTask. It's used to simulate that the runc-init | ||||
| // might be killed by oom-kill. | ||||
| func TestIssue9103(t *testing.T) { | ||||
| 	if os.Getenv("RUNC_FLAVOR") == "crun" { | ||||
| 		t.Skip("skip it when using crun") | ||||
| 	} | ||||
|  | ||||
| 	client, err := newClient(t, address) | ||||
| 	require.NoError(t, err) | ||||
| 	defer client.Close() | ||||
|  | ||||
| 	var ( | ||||
| 		image       Image | ||||
| 		ctx, cancel = testContext(t) | ||||
| 		id          = t.Name() | ||||
| 	) | ||||
| 	defer cancel() | ||||
|  | ||||
| 	image, err = client.GetImage(ctx, testImage) | ||||
| 	require.NoError(t, err) | ||||
|  | ||||
| 	for idx, tc := range []struct { | ||||
| 		desc           string | ||||
| 		cntrOpts       []NewContainerOpts | ||||
| 		expectedStatus ProcessStatus | ||||
| 	}{ | ||||
| 		{ | ||||
| 			desc: "should be created status", | ||||
| 			cntrOpts: []NewContainerOpts{ | ||||
| 				WithNewSpec(oci.WithImageConfig(image), | ||||
| 					withProcessArgs("sleep", "30"), | ||||
| 				), | ||||
| 			}, | ||||
| 			expectedStatus: Created, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc: "should be stopped status if init has been killed", | ||||
| 			cntrOpts: []NewContainerOpts{ | ||||
| 				WithNewSpec(oci.WithImageConfig(image), | ||||
| 					withProcessArgs("sleep", "30"), | ||||
| 					oci.WithAnnotations(map[string]string{ | ||||
| 						"oci.runc.failpoint.profile": "issue9103", | ||||
| 					}), | ||||
| 				), | ||||
| 				WithRuntime(client.Runtime(), &options.Options{ | ||||
| 					BinaryName: "runc-fp", | ||||
| 				}), | ||||
| 			}, | ||||
| 			expectedStatus: Stopped, | ||||
| 		}, | ||||
| 	} { | ||||
| 		tc := tc | ||||
| 		tName := fmt.Sprintf("%s%d", id, idx) | ||||
| 		t.Run(tc.desc, func(t *testing.T) { | ||||
| 			container, err := client.NewContainer(ctx, tName, | ||||
| 				append([]NewContainerOpts{WithNewSnapshot(tName, image)}, tc.cntrOpts...)..., | ||||
| 			) | ||||
| 			require.NoError(t, err) | ||||
| 			defer container.Delete(ctx, WithSnapshotCleanup) | ||||
|  | ||||
| 			cctx, ccancel := context.WithTimeout(ctx, 30*time.Second) | ||||
| 			task, err := container.NewTask(cctx, empty()) | ||||
| 			ccancel() | ||||
| 			require.NoError(t, err) | ||||
|  | ||||
| 			defer task.Delete(ctx, WithProcessKill) | ||||
|  | ||||
| 			status, err := task.Status(ctx) | ||||
| 			require.NoError(t, err) | ||||
| 			require.Equal(t, status.Status, tc.expectedStatus) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|   | ||||
							
								
								
									
										69
									
								
								integration/failpoint/cmd/runc-fp/issue9103.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								integration/failpoint/cmd/runc-fp/issue9103.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | ||||
| //go: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 main | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"syscall" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| // issue9103KillInitAfterCreate kills the runc.Init process after creating | ||||
| // command returns successfully. | ||||
| // | ||||
| // REF: https://github.com/containerd/containerd/issues/9103 | ||||
| func issue9103KillInitAfterCreate(ctx context.Context, method invoker) error { | ||||
| 	isCreated := strings.Contains(strings.Join(os.Args, ","), ",create,") | ||||
|  | ||||
| 	if err := method(ctx); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if !isCreated { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	initPidPath := "init.pid" | ||||
| 	data, err := os.ReadFile(initPidPath) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to read %s: %w", initPidPath, err) | ||||
| 	} | ||||
|  | ||||
| 	pid, err := strconv.Atoi(string(data)) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to get init pid from string %s: %w", string(data), err) | ||||
| 	} | ||||
|  | ||||
| 	if pid <= 0 { | ||||
| 		return fmt.Errorf("unexpected init pid %v", pid) | ||||
| 	} | ||||
|  | ||||
| 	if err := syscall.Kill(pid, syscall.SIGKILL); err != nil { | ||||
| 		return fmt.Errorf("failed to kill the init pid %v: %w", pid, err) | ||||
| 	} | ||||
|  | ||||
| 	// Ensure that the containerd-shim has received the SIGCHLD and start | ||||
| 	// to cleanup | ||||
| 	time.Sleep(3 * time.Second) | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										108
									
								
								integration/failpoint/cmd/runc-fp/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								integration/failpoint/cmd/runc-fp/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | ||||
| //go: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 main | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| 	"syscall" | ||||
|  | ||||
| 	"github.com/containerd/containerd/oci" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	failpointProfileKey = "oci.runc.failpoint.profile" | ||||
| ) | ||||
|  | ||||
| type invoker func(context.Context) error | ||||
|  | ||||
| type invokerInterceptor func(context.Context, invoker) error | ||||
|  | ||||
| var ( | ||||
| 	failpointProfiles = map[string]invokerInterceptor{ | ||||
| 		"issue9103": issue9103KillInitAfterCreate, | ||||
| 	} | ||||
| ) | ||||
|  | ||||
| // setupLog setups messages into log file. | ||||
| func setupLog() { | ||||
| 	// containerd/go-runc always add --log option | ||||
| 	idx := 2 | ||||
| 	for ; idx < len(os.Args); idx++ { | ||||
| 		if os.Args[idx] == "--log" { | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if idx >= len(os.Args)-1 || os.Args[idx] != "--log" { | ||||
| 		panic("option --log required") | ||||
| 	} | ||||
|  | ||||
| 	logFile := os.Args[idx+1] | ||||
| 	f, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND|os.O_SYNC, 0o644) | ||||
| 	if err != nil { | ||||
| 		panic(fmt.Errorf("failed to open %s: %w", logFile, err)) | ||||
| 	} | ||||
|  | ||||
| 	logrus.SetOutput(f) | ||||
| 	logrus.SetFormatter(new(logrus.JSONFormatter)) | ||||
| } | ||||
|  | ||||
| func main() { | ||||
| 	setupLog() | ||||
|  | ||||
| 	fpProfile, err := failpointProfileFromOCIAnnotation() | ||||
| 	if err != nil { | ||||
| 		logrus.WithError(err).Fatal("failed to get failpoint profile") | ||||
| 	} | ||||
|  | ||||
| 	ctx := context.Background() | ||||
| 	if err := fpProfile(ctx, defaultRuncInvoker); err != nil { | ||||
| 		logrus.WithError(err).Fatal("failed to exec failpoint profile") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // defaultRuncInvoker is to call the runc command with same arguments. | ||||
| func defaultRuncInvoker(ctx context.Context) error { | ||||
| 	cmd := exec.CommandContext(ctx, "runc", os.Args[1:]...) | ||||
| 	cmd.SysProcAttr = &syscall.SysProcAttr{Pdeathsig: syscall.SIGKILL} | ||||
| 	return cmd.Run() | ||||
| } | ||||
|  | ||||
| // failpointProfileFromOCIAnnotation gets the profile from OCI annotations. | ||||
| func failpointProfileFromOCIAnnotation() (invokerInterceptor, error) { | ||||
| 	spec, err := oci.ReadSpec(oci.ConfigFilename) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to read %s: %w", oci.ConfigFilename, err) | ||||
| 	} | ||||
|  | ||||
| 	profileName, ok := spec.Annotations[failpointProfileKey] | ||||
| 	if !ok { | ||||
| 		return nil, fmt.Errorf("failpoint profile is required") | ||||
| 	} | ||||
|  | ||||
| 	fp, ok := failpointProfiles[profileName] | ||||
| 	if !ok { | ||||
| 		return nil, fmt.Errorf("no such failpoint profile %s", profileName) | ||||
| 	} | ||||
| 	return fp, nil | ||||
| } | ||||
| @@ -33,3 +33,7 @@ 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}" | ||||
|  | ||||
| RUNCFP_BIN_DIR=${RUNCFP_BIN_DIR:-"/usr/local/bin"} | ||||
| make bin/runc-fp | ||||
| sudo install bin/runc-fp "${RUNCFP_BIN_DIR}" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Wei Fu
					Wei Fu