Files
containerd/integration/release_upgrade_exit_status_linux_test.go
Maksym Pavlenko e4e83b3af8 integration: test upgrades from 2.3 and 2.4
With 1.7 gone the matrix was left testing only 2.0, which is neither the
current LTS nor the release immediately preceding main. RELEASES.md
promises two paths into a given release: a sequential minor upgrade and a
sequential LTS upgrade. For main those are 2.4 and 2.3 respectively, so
test both.

Switch the download to the static release tarball. Release binaries are
built on Ubuntu 22.04, and the dynamically linked containerd from 2.3 on
needs GLIBC_2.32 and GLIBC_2.34. The EL8 hosts kept in the VM
integration matrix for their old glibc only have 2.28, so the previous
release never started there and every case timed out waiting for it. The
2.0 binaries needed nothing newer than GLIBC_2.4, which is why the old
matrix did not hit this. The static build has no libc dependency, and
its shim is the same binary the dynamic tarball already shipped.

Also bump the previous-release config to version 3. Version 2 only
mattered while 1.x was in the matrix, and every release now under test
writes version 3 by default.

kill-shim-before-delete-task needs its expected exit status picked per
version. Once the shim has been killed, containerd reports whatever
`<shim> delete` returns, and manager.Stop prefers the exit status
recorded in the bundle, falling back to a synthesized SIGKILL status when
there is no record. Recording landed in v2.4.0, so a bundle left by 2.3
reports 128+SIGKILL while one left by 2.4 reports the status the task
actually exited with. The old matrix predated recording entirely and only
ever covered the fallback; keying the expectation off the previous
version covers both branches.

Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
2026-09-17 16:22:26 -07:00

184 lines
6.5 KiB
Go

/*
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 (
"fmt"
"os"
"path/filepath"
"strings"
"syscall"
"testing"
"time"
"github.com/containerd/continuity/fs"
"github.com/containerd/errdefs"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
"golang.org/x/mod/semver"
"google.golang.org/protobuf/testing/protocmp"
eventtypes "github.com/containerd/containerd/api/events"
containerd "github.com/containerd/containerd/v2/client"
ctrdruntime "github.com/containerd/containerd/v2/core/runtime"
"github.com/containerd/containerd/v2/integration/images"
"github.com/containerd/containerd/v2/integration/remote"
"github.com/containerd/containerd/v2/pkg/cio"
"github.com/containerd/containerd/v2/pkg/oci"
"github.com/containerd/containerd/v2/plugins"
)
func shouldHandleShimExitStatusAfterUpgrade(previousReleaseBinDir, previousVersion string) setupUpgradeVerifyCase {
const (
shimExitStatus uint32 = 42
shimExitStatusNamespace = "kill-shim-before-delete-task"
)
// Shims from v2.4.0 on record the task's exit status in the bundle, so
// cleaning up after the killed shim reports the status the task actually
// exited with. Bundles left by older shims carry no such record and the
// cleanup falls back to a synthesized SIGKILL status. Both branches live in
// manager.Stop in cmd/containerd-shim-runc-v2/manager.
killedShimExitStatus := uint32(128 + syscall.SIGKILL)
if semver.Compare("v"+previousVersion, "v2.4") >= 0 {
killedShimExitStatus = shimExitStatus
}
return func(t *testing.T, _ int, rSvc *remote.RuntimeService, _ *remote.ImageService) ([]upgradeVerifyCaseFunc, beforeUpgradeHookFunc) {
client := newUpgradeContainerdClient(t, rSvc, shimExitStatusNamespace)
ctx := t.Context()
image, err := client.Pull(ctx, images.Get(images.BusyBox), containerd.WithPullUnpack)
require.NoError(t, err)
const id = "old-shim-exit-status"
// previousReleaseBinDir is shared by all upgrade cases, so use a private
// copy that can be replaced without affecting later tests.
oldShim := filepath.Join(t.TempDir(), "containerd-shim-runc-v2")
require.NoError(t, fs.CopyFile(oldShim,
filepath.Join(previousReleaseBinDir, "bin", "containerd-shim-runc-v2")))
require.NoError(t, os.Chmod(oldShim, 0755))
container, err := client.NewContainer(ctx, id,
containerd.WithNewSnapshot(id, image),
containerd.WithNewSpec(oci.WithImageConfig(image),
oci.WithProcessArgs("sh", "-c", fmt.Sprintf("exit %d", shimExitStatus))),
containerd.WithRuntime(plugins.RuntimeRuncV2, nil),
)
require.NoError(t, err)
_, err = container.NewTask(ctx, cio.NullIO, containerd.WithRuntimePath(oldShim))
require.NoError(t, err)
beforeUpgrade := func(t *testing.T) {
// Replace the shim binary to verify that the current shim can delete an old bundle.
require.NoError(t, os.Remove(oldShim))
require.NoError(t, fs.CopyFile(oldShim,
filepath.Join(*buildDir, "containerd-shim-runc-v2")))
require.NoError(t, os.Chmod(oldShim, 0755))
}
verify := func(t *testing.T, rSvc *remote.RuntimeService, _ *remote.ImageService) {
client := newUpgradeContainerdClient(t, rSvc, shimExitStatusNamespace)
ctx := t.Context()
container, err := client.LoadContainer(ctx, id)
require.NoError(t, err)
task, err := container.Task(ctx, nil)
require.NoError(t, err)
taskPID := task.Pid()
require.NotZero(t, taskPID)
eventCh, eventErrCh := client.EventService().Subscribe(ctx,
fmt.Sprintf(`topic=="%s",event.container_id=="%s"`, ctrdruntime.TaskExitEventTopic, id),
fmt.Sprintf(`topic=="%s",event.container_id=="%s"`, ctrdruntime.TaskDeleteEventTopic, id),
)
shim := buildShimClientFromNamespacedBundle(t, rSvc, shimExitStatusNamespace, id)
shimProcess, err := os.FindProcess(int(shimPid(ctx, t, shim.cli)))
require.NoError(t, err)
defer shimProcess.Release()
defer shimProcess.Signal(syscall.SIGKILL)
require.NoError(t, task.Start(ctx))
gotEvents := waitForTaskExitDeleteEvents(t, id, eventCh, eventErrCh, 1, 30*time.Second)
wantEvents := []taskEvent{{Exit: &eventtypes.TaskExit{
ContainerID: id,
ID: id,
Pid: taskPID,
ExitStatus: shimExitStatus,
}}}
cmpOpts := []cmp.Option{
protocmp.Transform(),
protocmp.IgnoreFields(&eventtypes.TaskExit{}, "exited_at"),
protocmp.IgnoreFields(&eventtypes.TaskDelete{}, "exited_at"),
}
if diff := cmp.Diff(wantEvents, gotEvents, cmpOpts...); diff != "" {
t.Fatalf("unexpected task exit event (-want +got):\n%s", diff)
}
// We should have two events after killed shim
require.NoError(t, shimProcess.Signal(syscall.SIGKILL))
gotEvents = waitForTaskExitDeleteEvents(t, id, eventCh, eventErrCh, 2, 30*time.Second)
wantEvents = []taskEvent{
{Exit: &eventtypes.TaskExit{
ContainerID: id,
ID: id,
Pid: taskPID,
ExitStatus: killedShimExitStatus,
}},
{Delete: &eventtypes.TaskDelete{
ContainerID: id,
Pid: taskPID,
ExitStatus: killedShimExitStatus,
}},
}
if diff := cmp.Diff(wantEvents, gotEvents, cmpOpts...); diff != "" {
t.Fatalf("unexpected task cleanup events (-want +got):\n%s", diff)
}
require.NoError(t, Eventually(func() (bool, error) {
_, err := container.Task(ctx, nil)
if err == nil {
return false, nil
}
if errdefs.IsNotFound(err) {
return true, nil
}
return false, err
}, 100*time.Millisecond, 10*time.Second))
require.NoError(t, container.Delete(ctx, containerd.WithSnapshotCleanup))
}
return []upgradeVerifyCaseFunc{verify}, beforeUpgrade
}
}
func newUpgradeContainerdClient(t *testing.T, rSvc *remote.RuntimeService, namespace string) *containerd.Client {
t.Helper()
endpoint := criRuntimeInfo(t, rSvc)["containerdEndpoint"].(string)
client, err := containerd.New(
strings.TrimPrefix(endpoint, "unix://"),
containerd.WithDefaultNamespace(namespace))
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, client.Close()) })
return client
}