Skip to content

Commit c2bc25c

Browse files
committed
volume: support in-VM snapshotters
With demux snapshotter, firecracker-containerd can run snapshotters inside VMs. This will allow firecracker-containerd run FUSE-backed lazy-loading snapshotters such as stargz snapshotter. This change supports in-VM snapshotters in volume package. Signed-off-by: Kazuyoshi Kato <[email protected]>
1 parent 0b71a80 commit c2bc25c

File tree

13 files changed

+570
-24
lines changed

13 files changed

+570
-24
lines changed

.buildkite/pipeline.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,11 @@ steps:
101101
DOCKER_IMAGE_TAG: "$BUILDKITE_BUILD_NUMBER"
102102
NUMBER_OF_VMS: 10
103103
EXTRAGOARGS: "-v -count=1 -race"
104+
FICD_DM_VOLUME_GROUP: fcci-vg
104105
artifact_paths:
105106
- "snapshotter/logs/*"
106107
command:
107-
- make -C snapshotter integ-test
108+
- make -C snapshotter integ-test FICD_DM_POOL=build_${BUILDKITE_BUILD_NUMBER}_snapshotter
108109

109110
- wait
110111

Makefile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
# express or implied. See the License for the specific language governing
1212
# permissions and limitations under the License.
1313

14-
SUBDIRS:=agent runtime examples firecracker-control/cmd/containerd snapshotter docker-credential-mmds
14+
SUBDIRS:=agent runtime examples firecracker-control/cmd/containerd snapshotter docker-credential-mmds volume
1515
TEST_SUBDIRS:=$(addprefix test-,$(SUBDIRS))
1616
INTEG_TEST_SUBDIRS:=$(addprefix integ-test-,$(SUBDIRS))
1717

@@ -147,9 +147,10 @@ files_ephemeral: $(RUNC_BIN) agent-in-docker
147147
cp agent/agent tools/image-builder/files_ephemeral/usr/local/bin
148148
touch tools/image-builder/files_ephemeral
149149

150-
files_ephemeral_stargz: $(STARGZ_BIN) docker-credential-mmds-in-docker
150+
files_ephemeral_stargz: $(STARGZ_BIN) docker-credential-mmds-in-docker volume-in-docker
151151
mkdir -p tools/image-builder/files_ephemeral_stargz/usr/local/bin
152152
cp docker-credential-mmds/docker-credential-mmds tools/image-builder/files_ephemeral_stargz/usr/local/bin
153+
cp volume/volume-init tools/image-builder/files_ephemeral_stargz/usr/local/bin
153154
cp $(STARGZ_BIN) tools/image-builder/files_ephemeral_stargz/usr/local/bin
154155

155156
image: files_ephemeral

snapshotter/Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ GOSUM := $(GOMOD:.mod=.sum)
2020
DOCKER_IMAGE_TAG?=latest
2121
GO_CACHE_VOLUME_NAME?=gocache
2222
FIRECRACKER_CONTAINERD_TEST_IMAGE?=localhost/firecracker-containerd-test
23+
FICD_DM_POOL?=fc-test-thinpool
2324

2425
REVISION := $(shell git rev-parse HEAD)
2526

@@ -51,6 +52,7 @@ integ-test:
5152
$(MAKE) $(addprefix integ-test-,$(INTEG_TESTNAMES))
5253

5354
integ-test-%: logs
55+
$(CURDIR)/../tools/thinpool.sh reset "$(FICD_DM_POOL)"
5456
docker run --rm -it \
5557
--privileged \
5658
--ipc=host \
@@ -65,6 +67,8 @@ integ-test-%: logs
6567
--env GOSUMDB=off \
6668
--env GO111MODULE=on \
6769
--env NUMBER_OF_VMS=$(NUMBER_OF_VMS) \
70+
--env FICD_DM_VOLUME_GROUP=$(FICD_DM_VOLUME_GROUP) \
71+
--env FICD_DM_POOL=$(FICD_DM_POOL) \
6872
--workdir="/src/snapshotter" \
6973
--init \
7074
$(FIRECRACKER_CONTAINERD_TEST_IMAGE):$(DOCKER_IMAGE_TAG) \

snapshotter/service_integ_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import (
3434
const (
3535
snapshotterName = "demux"
3636

37-
imageRef = "ghcr.io/firecracker-microvm/firecracker-containerd/amazonlinux:latest-esgz"
37+
al2stargz = "ghcr.io/firecracker-microvm/firecracker-containerd/amazonlinux:latest-esgz"
3838

3939
noAuth = ""
4040

@@ -138,10 +138,10 @@ func launchContainerWithRemoteSnapshotterInVM(ctx context.Context, vmID string)
138138
return fmt.Errorf("Failed to configure VM metadata for registry resolution [%v]", err)
139139
}
140140

141-
image, err := client.Pull(ctx, imageRef,
141+
image, err := client.Pull(ctx, al2stargz,
142142
containerd.WithPullUnpack,
143143
containerd.WithPullSnapshotter(snapshotterName),
144-
containerd.WithImageHandlerWrapper(source.AppendDefaultLabelsHandlerWrapper(imageRef, 10*1024*1024)),
144+
containerd.WithImageHandlerWrapper(source.AppendDefaultLabelsHandlerWrapper(al2stargz, 10*1024*1024)),
145145
)
146146
if err != nil {
147147
return fmt.Errorf("Failed to pull image for VM: %s [%v]", vmID, err)

snapshotter/volume_integ_test.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"). You may
4+
// not use this file except in compliance with the License. A copy of the
5+
// License is located at
6+
//
7+
// http://aws.amazon.com/apache2.0/
8+
//
9+
// or in the "license" file accompanying this file. This file is distributed
10+
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
// express or implied. See the License for the specific language governing
12+
// permissions and limitations under the License.
13+
14+
package main
15+
16+
import (
17+
"context"
18+
"fmt"
19+
"testing"
20+
21+
"github.com/containerd/containerd"
22+
"github.com/containerd/containerd/namespaces"
23+
"github.com/containerd/containerd/oci"
24+
"github.com/firecracker-microvm/firecracker-containerd/internal/integtest"
25+
"github.com/firecracker-microvm/firecracker-containerd/proto"
26+
"github.com/firecracker-microvm/firecracker-containerd/runtime/firecrackeroci"
27+
"github.com/firecracker-microvm/firecracker-containerd/snapshotter/internal/integtest/stargz/fs/source"
28+
"github.com/firecracker-microvm/firecracker-containerd/volume"
29+
"github.com/stretchr/testify/assert"
30+
"github.com/stretchr/testify/require"
31+
)
32+
33+
const mib = 1024 * 1024
34+
35+
func TestGuestVolumeFrom_Isolated(t *testing.T) {
36+
integtest.Prepare(t, integtest.WithDefaultNetwork())
37+
const (
38+
vmID = "default"
39+
postgres = "docker.io/library/postgres:14.3"
40+
alpine = "docker.io/library/alpine:3.10.1"
41+
runtime = "aws.firecracker"
42+
)
43+
44+
ctx := namespaces.WithNamespace(context.Background(), vmID)
45+
46+
client, err := containerd.New(integtest.ContainerdSockPath, containerd.WithDefaultRuntime(runtime))
47+
require.NoError(t, err, "unable to create client to containerd service at %s, is containerd running?", integtest.ContainerdSockPath)
48+
defer client.Close()
49+
fcClient, err := integtest.NewFCControlClient(integtest.ContainerdSockPath)
50+
require.NoError(t, err, "Failed to create fccontrol client")
51+
52+
vs := volume.NewSet(runtime)
53+
54+
// Add a non-stargz image with volumes.
55+
localImage := volume.FromImage(client, postgres, "postgres-snapshot", volume.WithSnapshotter("devmapper"))
56+
err = vs.AddFrom(ctx, localImage)
57+
require.NoError(t, err)
58+
59+
// Add a stargz image.
60+
// The volume directories must be specified since the host's containerd doesn't know about the image.
61+
remoteImage := volume.FromGuestImage(
62+
client, vmID, al2stargz, "al2-snapshot", []string{"/etc/yum"},
63+
volume.WithSnapshotter("demux"),
64+
)
65+
mount, err := vs.PrepareDriveMount(ctx, 10*mib)
66+
require.NoError(t, err)
67+
68+
_, err = fcClient.CreateVM(ctx, &proto.CreateVMRequest{
69+
VMID: vmID,
70+
RootDrive: &proto.FirecrackerRootDrive{
71+
HostPath: "/var/lib/firecracker-containerd/runtime/rootfs-stargz.img",
72+
},
73+
NetworkInterfaces: []*proto.FirecrackerNetworkInterface{
74+
{
75+
AllowMMDS: true,
76+
CNIConfig: &proto.CNIConfiguration{
77+
NetworkName: "fcnet",
78+
InterfaceName: "veth0",
79+
},
80+
},
81+
},
82+
MachineCfg: &proto.FirecrackerMachineConfiguration{
83+
VcpuCount: 2,
84+
MemSizeMib: 2048,
85+
},
86+
ContainerCount: 1,
87+
DriveMounts: []*proto.FirecrackerDriveMount{mount},
88+
})
89+
require.NoErrorf(t, err, "Failed to create microVM[%s]", vmID)
90+
defer fcClient.StopVM(ctx, &proto.StopVMRequest{VMID: vmID})
91+
92+
_, err = fcClient.SetVMMetadata(ctx, &proto.SetVMMetadataRequest{
93+
VMID: vmID,
94+
Metadata: fmt.Sprintf(dockerMetadataTemplate, "ghcr.io", noAuth, noAuth),
95+
})
96+
require.NoError(t, err, "Failed to configure VM metadata for registry resolution")
97+
98+
err = remoteImage.Pull(ctx,
99+
containerd.WithImageHandlerWrapper(source.AppendDefaultLabelsHandlerWrapper(al2stargz, 10*mib)),
100+
)
101+
require.NoError(t, err)
102+
103+
err = remoteImage.Copy(ctx, "image")
104+
require.NoError(t, err)
105+
106+
err = vs.AddFrom(ctx, remoteImage)
107+
require.NoError(t, err)
108+
109+
image, err := client.Pull(ctx,
110+
alpine,
111+
containerd.WithPullUnpack,
112+
containerd.WithPullSnapshotter("devmapper"),
113+
)
114+
require.NoError(t, err)
115+
116+
mountsFromAL2, err := vs.WithMountsFromProvider(al2stargz)
117+
require.NoError(t, err)
118+
119+
mountsFromPostgres, err := vs.WithMountsFromProvider(postgres)
120+
require.NoError(t, err)
121+
122+
name := "cat"
123+
snapshotName := fmt.Sprintf("%s-snapshot", name)
124+
container, err := client.NewContainer(ctx,
125+
name,
126+
containerd.WithSnapshotter("devmapper"),
127+
containerd.WithNewSnapshot(snapshotName, image),
128+
containerd.WithNewSpec(
129+
firecrackeroci.WithVMID(vmID),
130+
oci.WithProcessArgs("sh", "-c", "ls -d /var/lib/postgresql/data; ls /etc/yum"),
131+
oci.WithDefaultPathEnv,
132+
mountsFromAL2,
133+
mountsFromPostgres,
134+
),
135+
)
136+
require.NoError(t, err, "failed to create container %s", name)
137+
defer container.Delete(ctx, containerd.WithSnapshotCleanup)
138+
139+
result, err := integtest.RunTask(ctx, container)
140+
require.NoError(t, err)
141+
142+
assert.Equal(t, uint32(0), result.ExitCode)
143+
assert.Equal(t, "/var/lib/postgresql/data\nfssnap.d\npluginconf.d\nprotected.d\nvars\nversion-groups.conf\n", result.Stdout)
144+
assert.Equal(t, "", result.Stderr)
145+
}

tools/image-builder/files_debootstrap/sbin/overlay-init

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ fi
5757

5858
do_overlay
5959

60+
# firecracker-containerd itself doesn't need /volumes but volume package
61+
# uses that to share files between in-VM snapshotters.
62+
mkdir /volumes
63+
6064
# invoke the actual system init program and procede with the boot
6165
# process.
6266
exec /usr/sbin/init $@

volume/Makefile

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"). You may
4+
# not use this file except in compliance with the License. A copy of the
5+
# License is located at
6+
#
7+
# http://aws.amazon.com/apache2.0/
8+
#
9+
# or in the "license" file accompanying this file. This file is distributed
10+
# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
# express or implied. See the License for the specific language governing
12+
# permissions and limitations under the License.
13+
14+
volume-init: $(shell find ./cmd -name '*.go')
15+
go build -o $@ ./cmd/volume-init
16+
17+
test:
18+
go test ./...
19+
20+
clean:
21+
rm -f volume-init
22+
23+
install:
24+
25+
.PHONY: test clean install

volume/cmd/volume-init/main.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"). You may
4+
// not use this file except in compliance with the License. A copy of the
5+
// License is located at
6+
//
7+
// http://aws.amazon.com/apache2.0/
8+
//
9+
// or in the "license" file accompanying this file. This file is distributed
10+
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
// express or implied. See the License for the specific language governing
12+
// permissions and limitations under the License.
13+
14+
package main
15+
16+
import (
17+
"encoding/json"
18+
"fmt"
19+
"io"
20+
"os"
21+
22+
"github.com/containerd/continuity/fs"
23+
"github.com/firecracker-microvm/firecracker-containerd/volume"
24+
)
25+
26+
func copy(in volume.GuestVolumeImageInput) error {
27+
for _, v := range in.Volumes {
28+
to, err := fs.RootPath(in.To, v)
29+
if err != nil {
30+
return fmt.Errorf("failed to join %q and %q: %w", in.To, v, err)
31+
}
32+
33+
from, err := fs.RootPath(in.From, v)
34+
if err != nil {
35+
return fmt.Errorf("failed to join %q and %q: %w", in.From, v, err)
36+
}
37+
38+
err = os.MkdirAll(to, 0700)
39+
if err != nil {
40+
return err
41+
}
42+
43+
err = fs.CopyDir(to, from)
44+
if err != nil {
45+
return err
46+
}
47+
}
48+
return nil
49+
}
50+
51+
func realMain() error {
52+
b, err := io.ReadAll(os.Stdin)
53+
if err != nil {
54+
return err
55+
}
56+
57+
var in volume.GuestVolumeImageInput
58+
err = json.Unmarshal(b, &in)
59+
if err != nil {
60+
return err
61+
}
62+
63+
return copy(in)
64+
}
65+
66+
func main() {
67+
err := realMain()
68+
if err != nil {
69+
out := volume.GuestVolumeImageOutput{Error: err.Error()}
70+
b, err := json.Marshal(out)
71+
if err != nil {
72+
fmt.Fprintf(os.Stderr, "failed to unmarshal %+v: %s", out, err)
73+
os.Exit(2)
74+
}
75+
fmt.Printf("%s", string(b))
76+
os.Exit(1)
77+
}
78+
}

volume/cmd/volume-init/main_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"). You may
4+
// not use this file except in compliance with the License. A copy of the
5+
// License is located at
6+
//
7+
// http://aws.amazon.com/apache2.0/
8+
//
9+
// or in the "license" file accompanying this file. This file is distributed
10+
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
// express or implied. See the License for the specific language governing
12+
// permissions and limitations under the License.
13+
14+
package main
15+
16+
import (
17+
"os"
18+
"path/filepath"
19+
"testing"
20+
21+
"github.com/firecracker-microvm/firecracker-containerd/volume"
22+
"github.com/stretchr/testify/require"
23+
)
24+
25+
func mkdirAndTouch(path, content string) error {
26+
err := os.MkdirAll(filepath.Dir(path), 0700)
27+
if err != nil {
28+
return err
29+
}
30+
31+
return os.WriteFile(path, []byte(content), 0600)
32+
}
33+
34+
func TestCopy(t *testing.T) {
35+
from := t.TempDir()
36+
to := t.TempDir()
37+
38+
err := mkdirAndTouch(filepath.Join(from, "/etc/foobar/hello"), "helloworld")
39+
require.NoError(t, err)
40+
41+
err = copy(volume.GuestVolumeImageInput{
42+
From: from,
43+
To: to,
44+
Volumes: []string{"/etc/foobar"},
45+
})
46+
require.NoError(t, err)
47+
48+
b, err := os.ReadFile(filepath.Join(to, "/etc/foobar/hello"))
49+
require.NoError(t, err)
50+
require.Equal(t, "helloworld", string(b))
51+
}

0 commit comments

Comments
 (0)