Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -229,3 +229,107 @@ jobs:

- name: Run integration tests
run: go test -v ./integration/...

#
# Integration tests (rootless)
#
integration-rootless:
name: Integration Tests (rootless)
needs: [setup, build-kernels]
if: |
always() &&
(needs.build-kernels.result == 'success' || needs.build-kernels.result == 'skipped')
runs-on: ${{ matrix.os }}
timeout-minutes: 20

strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
arch: x86_64

steps:
- name: Create non-root user with KVM access
run: |
sudo useradd -m testuser
sudo usermod -aG kvm testuser

- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Calculate kernel cache key
id: cache-key
run: |
# Hash the kernel config and patches to create a unique cache key
CONFIG_FILE="kernel/config-${{ needs.setup.outputs.kernel-version }}-${{ matrix.arch }}"

if [ ! -f "$CONFIG_FILE" ]; then
echo "Error: Kernel config file $CONFIG_FILE not found"
exit 1
fi

# Calculate hash of config file and all patches
CONFIG_HASH=$(sha256sum "$CONFIG_FILE" | cut -d' ' -f1)
PATCHES_HASH=$(find kernel/patches -type f -name "*.patch" -exec sha256sum {} \; | sort | sha256sum | cut -d' ' -f1)

# Combine version, arch, config hash, and patches hash
CACHE_KEY="kernel-${{ needs.setup.outputs.kernel-version }}-${{ matrix.arch }}-${CONFIG_HASH:0:8}-${PATCHES_HASH:0:8}"

echo "cache-key=${CACHE_KEY}" >> $GITHUB_OUTPUT
echo "Kernel cache key: ${CACHE_KEY}"

- name: Restore cached kernel
id: cache-kernel
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: _output/nerdbox-kernel-${{ matrix.arch }}
key: ${{ steps.cache-key.outputs.cache-key }}

- name: Verify kernel from cache
run: |
if [ "${{ steps.cache-kernel.outputs.cache-hit }}" = "true" ]; then
echo "✅ Kernel restored from cache"
else
echo "❌ Kernel not in cache - this should not happen after build-kernels-on-demand"
exit 1
fi
ls -lh _output/nerdbox-kernel-${{ matrix.arch }}
file _output/nerdbox-kernel-${{ matrix.arch }}

- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0

- name: Build remaining artifacts (initrd and shim)
run: |
echo "Building host and guest binaries:"
docker buildx bake host-binaries guest-binaries

- name: Verify all artifacts
run: |
echo "Verifying build artifacts:"
ls -lh _output/
echo ""
echo "Kernel:"
file _output/nerdbox-kernel-${{ matrix.arch }}
echo ""
echo "Initrd:"
file _output/nerdbox-initrd
echo ""
echo "Shim:"
file _output/containerd-shim-nerdbox-v1

- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
with:
go-version-file: '.github/.tool-versions'

- name: Compile integration test binary
run: go test -c -o _output/integration.test ./integration

- name: Allow testuser to traverse to workspace
run: sudo chmod a+x /home/runner

- name: Run integration tests as non-root
run: |
sudo -u testuser \
env "PATH=$(pwd)/_output:$PATH" \
"HOME=/home/testuser" \
_output/integration.test -test.v
19 changes: 6 additions & 13 deletions integration/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,25 +32,18 @@ func TestMain(m *testing.M) {
if err != nil {
log.Fatalf("Failed to get executable path: %v", err)
}

// Prepend the directory containing the test binary to PATH so that
// NewInstance can find libkrun.so, the kernel, and initrd located
// alongside the binary (the _output/ directory).
Comment on lines +36 to +38
exeDir := filepath.Dir(e)
paths := filepath.SplitList(os.Getenv("PATH"))
for _, p := range []string{
"../_output",
".",
} {
absPath := filepath.Clean(filepath.Join(exeDir, p))
// Prepend to slice
paths = append(paths, "")
copy(paths[1:], paths)
paths[0] = absPath
}
paths = append([]string{exeDir}, paths...)
if err := os.Setenv("PATH", strings.Join(paths, string(filepath.ListSeparator))); err != nil {
log.Fatalf("Failed to set PATH environment variable: %v", err)
}

r := m.Run()

os.Exit(r)
os.Exit(m.Run())
}

func runWithVM(t *testing.T, runTest func(*testing.T, vm.Instance)) {
Expand Down
94 changes: 94 additions & 0 deletions integration/shim_start_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
//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 integration

import (
"encoding/json"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"syscall"
"testing"
)

const shimBinary = "containerd-shim-nerdbox-v1"

// TestShimStart exercises the shim manager's Start() code path by invoking
// the real shim binary with the \"start\" subcommand. This is the same
// invocation containerd uses to launch a shim.
func TestShimStart(t *testing.T) {
shimPath, err := exec.LookPath(shimBinary)
if err != nil {
t.Skipf("%s not found on PATH: %v", shimBinary, err)
}

bundleDir := t.TempDir()
socketDir := t.TempDir()

// Minimal OCI config.json — Start() only reads annotations.
if err := os.WriteFile(filepath.Join(bundleDir, "config.json"), []byte(`{"annotations":{}}`), 0o644); err != nil {
t.Fatal(err)
}

cmd := exec.Command(shimPath,
"-namespace", "test",
"-id", "test-shim-start",
"-address", filepath.Join(socketDir, "containerd.sock"),
"start",
)
cmd.Dir = bundleDir
cmd.Env = append(os.Environ(),
// The fork reads SHIM_SOCKET_DIR to set StartOpts.SocketDir so
// the shim creates its sockets in a writable directory instead
// of the default /run/containerd/s.
"SHIM_SOCKET_DIR="+socketDir,
)

out, err := cmd.Output()
if err != nil {
stderr := ""
if ee, ok := err.(*exec.ExitError); ok {
stderr = string(ee.Stderr)
}
t.Fatalf("shim start failed: %v\nstderr: %s", err, stderr)
}
Comment on lines +65 to +72

var params struct {
Version int `json:"version"`
Address string `json:"address"`
Protocol string `json:"protocol"`
}
if err := json.Unmarshal(out, &params); err != nil {
t.Fatalf("failed to parse shim output: %v\nraw: %s", err, out)
}
if params.Address == "" {
t.Fatal("shim returned empty address")
}
t.Logf("shim started: version=%d protocol=%s address=%s", params.Version, params.Protocol, params.Address)

// Clean up the child shim process that Start() spawned.
pidData, err := os.ReadFile(filepath.Join(bundleDir, "shim.pid"))
if err == nil {
if pid, err := strconv.Atoi(strings.TrimSpace(string(pidData))); err == nil {
syscall.Kill(pid, syscall.SIGKILL)
}
Comment on lines +87 to +92
}
}
Loading