mirror of
https://github.com/containerd/containerd.git
synced 2026-08-08 17:11:15 +00:00
279 lines
7.1 KiB
Go
279 lines
7.1 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 client
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/opencontainers/image-spec/identity"
|
|
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
|
"github.com/opencontainers/runtime-spec/specs-go"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/containerd/errdefs"
|
|
|
|
containerd "github.com/containerd/containerd/v2/client"
|
|
"github.com/containerd/containerd/v2/core/containers"
|
|
"github.com/containerd/containerd/v2/core/content"
|
|
"github.com/containerd/containerd/v2/core/images"
|
|
"github.com/containerd/containerd/v2/core/mount"
|
|
"github.com/containerd/containerd/v2/defaults"
|
|
"github.com/containerd/containerd/v2/pkg/oci"
|
|
)
|
|
|
|
func TestMountManager(t *testing.T) {
|
|
client, err := newClient(t, address)
|
|
require.NoError(t, err)
|
|
defer client.Close()
|
|
|
|
var (
|
|
ctx, cancel = testContext(t)
|
|
)
|
|
defer cancel()
|
|
|
|
// Test MountManager functionality
|
|
mounts, err := client.MountManager().List(ctx)
|
|
require.NoError(t, err)
|
|
|
|
assert.Empty(t, mounts, "Mounts should be empty")
|
|
|
|
}
|
|
|
|
func TestMountAtRuntime(t *testing.T) {
|
|
client, err := newClient(t, address)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer client.Close()
|
|
|
|
var (
|
|
ctx, cancel = testContext(t)
|
|
id1 = fmt.Sprintf("%s-1", t.Name())
|
|
id2 = fmt.Sprintf("%s-2", t.Name())
|
|
testDev = createImgFile(t, "test-dev.img")
|
|
workDir = filepath.Join(t.TempDir(), "work")
|
|
)
|
|
defer cancel()
|
|
|
|
require.NoError(t, os.MkdirAll(workDir, 0755))
|
|
|
|
ctx, done, err := client.WithLease(ctx)
|
|
require.NoError(t, err)
|
|
defer done(ctx)
|
|
|
|
img, err := client.GetImage(ctx, testImage)
|
|
require.NoError(t, err)
|
|
|
|
snapshotter := client.SnapshotService(defaults.DefaultSnapshotter)
|
|
diffIDs, err := img.RootFS(ctx)
|
|
require.NoError(t, err)
|
|
|
|
mnt, err := snapshotter.View(ctx, id1, identity.ChainID(diffIDs).String())
|
|
require.NoError(t, err)
|
|
if len(mnt) > 1 {
|
|
t.Skipf("Test can only be run with single mount")
|
|
}
|
|
m := append(mnt, mount.Mount{
|
|
Type: "xfs",
|
|
Source: testDev,
|
|
Options: []string{"loop"},
|
|
}, mount.Mount{
|
|
Type: "format/overlay",
|
|
Source: "overlay",
|
|
Options: []string{"lowerdir={{ mount 0 }}", "upperdir={{ mount 1 }}/root1", "workdir={{ mount 1 }}/work", "index=off"},
|
|
})
|
|
|
|
container1, err := client.NewContainer(ctx, id1, containerd.WithNewSpec(withImage(img, "sh", "-c", `echo -n 'Test mount at runtime'> /etc/test-name.txt`)))
|
|
require.NoError(t, err)
|
|
defer func() { assert.NoError(t, container1.Delete(ctx)) }()
|
|
|
|
task, err := container1.NewTask(ctx, empty(), containerd.WithRootFS(m))
|
|
require.NoError(t, err)
|
|
defer func() {
|
|
if task != nil {
|
|
task.Delete(ctx)
|
|
}
|
|
}()
|
|
|
|
info, err := client.MountManager().Info(ctx, id1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 2, len(info.Active), "Expected two mounts for container")
|
|
|
|
statusC, err := task.Wait(ctx)
|
|
require.NoError(t, err)
|
|
require.NoError(t, task.Start(ctx))
|
|
<-statusC
|
|
_, err = task.Delete(ctx)
|
|
task = nil
|
|
require.NoError(t, err)
|
|
|
|
_, err = client.MountManager().Info(ctx, id1)
|
|
require.ErrorIs(t, err, errdefs.ErrNotFound, "Expected not found error")
|
|
|
|
// Start another container with results created from first container
|
|
|
|
container2, err := client.NewContainer(ctx, id2, containerd.WithNewSpec(withImage(img, "cat", "/etc/test-name.txt")))
|
|
require.NoError(t, err)
|
|
defer func() { assert.NoError(t, container2.Delete(ctx)) }()
|
|
|
|
m = append(mnt, mount.Mount{
|
|
Type: "xfs",
|
|
Source: testDev,
|
|
Options: []string{"loop"},
|
|
}, mount.Mount{
|
|
Type: "format/overlay",
|
|
Source: "overlay",
|
|
Options: []string{"lowerdir={{ mount 1 }}/root1:{{ mount 0 }}", "upperdir={{ mount 1 }}/root2", "workdir={{ mount 1 }}/work", "index=off"},
|
|
})
|
|
|
|
direct, err := newDirectIO(ctx, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer direct.Delete()
|
|
var (
|
|
wg sync.WaitGroup
|
|
buf = bytes.NewBuffer(nil)
|
|
)
|
|
wg.Go(func() {
|
|
io.Copy(buf, direct.Stdout)
|
|
})
|
|
|
|
task, err = container2.NewTask(ctx, direct.IOCreate, containerd.WithRootFS(m))
|
|
require.NoError(t, err)
|
|
defer func() {
|
|
if task != nil {
|
|
task.Delete(ctx)
|
|
}
|
|
}()
|
|
|
|
info, err = client.MountManager().Info(ctx, id2)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 2, len(info.Active), "Expected two mounts for container")
|
|
|
|
statusC, err = task.Wait(ctx)
|
|
require.NoError(t, err)
|
|
require.NoError(t, task.Start(ctx))
|
|
<-statusC
|
|
_, err = task.Delete(ctx)
|
|
task = nil
|
|
require.NoError(t, err)
|
|
|
|
wg.Wait()
|
|
|
|
require.Equal(t, "Test mount at runtime", buf.String(), "Expected output from mounted file")
|
|
}
|
|
|
|
func createImgFile(t *testing.T, name string) string {
|
|
mkfs, err := exec.LookPath("mkfs.xfs")
|
|
if err != nil {
|
|
t.Skipf("Could not find mkfs.xfs: %v", err)
|
|
}
|
|
|
|
loopbackSize := int64(300 << 20) // 300 MB
|
|
|
|
f := filepath.Join(t.TempDir(), name)
|
|
devFile, err := os.OpenFile(f, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
|
|
require.NoError(t, err)
|
|
|
|
if err := devFile.Truncate(loopbackSize); err != nil {
|
|
devFile.Close()
|
|
t.Fatalf("failed to resize %s file: %v", f, err)
|
|
}
|
|
|
|
if err := devFile.Sync(); err != nil {
|
|
devFile.Close()
|
|
t.Fatalf("failed to sync %s file: %v", f, err)
|
|
}
|
|
devFile.Close()
|
|
|
|
if out, err := exec.Command(mkfs, f).CombinedOutput(); err != nil {
|
|
t.Fatalf("failed to make xfs filesystem (out: %q): %v", out, err)
|
|
}
|
|
|
|
setupMount(t, f)
|
|
|
|
return f
|
|
}
|
|
|
|
func setupMount(t *testing.T, f string) {
|
|
root := t.TempDir()
|
|
m := []mount.Mount{
|
|
{
|
|
Type: "xfs",
|
|
Source: f,
|
|
Options: []string{"loop", "sync"},
|
|
},
|
|
}
|
|
|
|
require.NoError(t, mount.All(m, root))
|
|
require.NoError(t, os.Mkdir(filepath.Join(root, "root1"), 0755))
|
|
require.NoError(t, os.Mkdir(filepath.Join(root, "root2"), 0755))
|
|
require.NoError(t, os.Mkdir(filepath.Join(root, "work"), 0755))
|
|
require.NoError(t, mount.UnmountAll(root, 0))
|
|
}
|
|
|
|
func withImage(image containerd.Image, args ...string) oci.SpecOpts {
|
|
return func(ctx context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error {
|
|
ic, err := image.Config(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !images.IsConfigType(ic.MediaType) {
|
|
return fmt.Errorf("unknown image config media type %s", ic.MediaType)
|
|
}
|
|
|
|
var (
|
|
imageConfigBytes []byte
|
|
ociimage v1.Image
|
|
)
|
|
imageConfigBytes, err = content.ReadBlob(ctx, image.ContentStore(), ic)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = json.Unmarshal(imageConfigBytes, &ociimage); err != nil {
|
|
return err
|
|
}
|
|
if s.Linux == nil {
|
|
return fmt.Errorf("linux image section missing")
|
|
}
|
|
|
|
s.Process = &specs.Process{}
|
|
s.Process.Env = ociimage.Config.Env
|
|
s.Process.Args = append(ociimage.Config.Entrypoint, args...)
|
|
|
|
cwd := ociimage.Config.WorkingDir
|
|
if cwd == "" {
|
|
cwd = "/"
|
|
}
|
|
s.Process.Cwd = cwd
|
|
|
|
return nil
|
|
}
|
|
}
|