Skip to content

Commit 7005764

Browse files
authored
Merge pull request #636 from kzys/volume-from
Add volume.FromImage
2 parents a2cc5bf + 06bbb36 commit 7005764

File tree

6 files changed

+417
-10
lines changed

6 files changed

+417
-10
lines changed

runtime/volume_integ_test.go

+108
Original file line numberDiff line numberDiff line change
@@ -174,3 +174,111 @@ func testVolumes(t *testing.T, runtime string) {
174174
assert.Equal(t, "hello from host\nhello from c1\nhello from c2\n", stdout.String())
175175
assert.Equal(t, "", stderr.String())
176176
}
177+
178+
func TestVolumeFrom_Isolated(t *testing.T) {
179+
integtest.Prepare(t)
180+
181+
runtimes := []string{firecrackerRuntime, "io.containerd.runc.v2"}
182+
183+
for _, rt := range runtimes {
184+
t.Run(rt, func(t *testing.T) {
185+
testVolumeFrom(t, rt)
186+
})
187+
}
188+
}
189+
190+
func testVolumeFrom(t *testing.T, runtime string) {
191+
const vmID = 0
192+
193+
ctx := namespaces.WithNamespace(context.Background(), "default")
194+
195+
client, err := containerd.New(integtest.ContainerdSockPath, containerd.WithDefaultRuntime(runtime))
196+
require.NoError(t, err, "unable to create client to containerd service at %s, is containerd running?", integtest.ContainerdSockPath)
197+
defer client.Close()
198+
199+
image, err := alpineImage(ctx, client, defaultSnapshotterName)
200+
require.NoError(t, err, "failed to get alpine image")
201+
202+
fcClient, err := integtest.NewFCControlClient(integtest.ContainerdSockPath)
203+
require.NoError(t, err, "failed to create fccontrol client")
204+
205+
// TODO: Create and host own images with non-empty volumes.
206+
ref := "docker.io/library/postgres:14.3"
207+
vs := volume.NewSet(runtime)
208+
provider := volume.FromImage(client, ref, "vfc-snapshot", volume.WithSnapshotter(defaultSnapshotterName))
209+
defer func() {
210+
err := provider.Delete(ctx)
211+
require.NoError(t, err)
212+
}()
213+
err = vs.AddFrom(ctx, provider)
214+
require.NoError(t, err)
215+
216+
containers := []string{"c1", "c2"}
217+
218+
if runtime == firecrackerRuntime {
219+
mount, err := vs.PrepareDriveMount(ctx, 10*mib)
220+
require.NoError(t, err)
221+
222+
_, err = fcClient.CreateVM(ctx, &proto.CreateVMRequest{
223+
VMID: strconv.Itoa(vmID),
224+
ContainerCount: int32(len(containers)),
225+
DriveMounts: []*proto.FirecrackerDriveMount{mount},
226+
})
227+
require.NoError(t, err, "failed to create VM")
228+
} else {
229+
err := vs.PrepareDirectory(ctx)
230+
require.NoError(t, err)
231+
}
232+
233+
// Make volmes from a container.
234+
volumesFromContainerImage, err := vs.WithMountsFromProvider(ref)
235+
require.NoError(t, err)
236+
237+
dir := "/var/lib/postgresql/data"
238+
239+
for _, name := range containers {
240+
snapshotName := fmt.Sprintf("%s-snapshot", name)
241+
242+
sh := fmt.Sprintf("echo hello from %s >> %s/hello.txt", name, dir)
243+
container, err := client.NewContainer(ctx,
244+
name,
245+
containerd.WithSnapshotter(defaultSnapshotterName),
246+
containerd.WithNewSnapshot(snapshotName, image),
247+
containerd.WithNewSpec(
248+
firecrackeroci.WithVMID(strconv.Itoa(vmID)),
249+
oci.WithProcessArgs("sh", "-c", sh),
250+
oci.WithDefaultPathEnv,
251+
volumesFromContainerImage,
252+
),
253+
)
254+
require.NoError(t, err, "failed to create container %s", name)
255+
defer container.Delete(ctx, containerd.WithSnapshotCleanup)
256+
257+
result, err := integtest.RunTask(ctx, container)
258+
require.NoError(t, err)
259+
assert.Equalf(t, uint32(0), result.ExitCode, "stdout=%q stderr=%q", result.Stdout, result.Stderr)
260+
}
261+
262+
name := "cat"
263+
snapshotName := fmt.Sprintf("%s-snapshot", name)
264+
container, err := client.NewContainer(ctx,
265+
name,
266+
containerd.WithSnapshotter(defaultSnapshotterName),
267+
containerd.WithNewSnapshot(snapshotName, image),
268+
containerd.WithNewSpec(
269+
firecrackeroci.WithVMID(strconv.Itoa(vmID)),
270+
oci.WithProcessArgs("cat", fmt.Sprintf("%s/hello.txt", dir)),
271+
oci.WithDefaultPathEnv,
272+
volumesFromContainerImage,
273+
),
274+
)
275+
require.NoError(t, err, "failed to create container %s", name)
276+
defer container.Delete(ctx, containerd.WithSnapshotCleanup)
277+
278+
result, err := integtest.RunTask(ctx, container)
279+
require.NoError(t, err)
280+
281+
assert.Equal(t, uint32(0), result.ExitCode)
282+
assert.Equal(t, "hello from c1\nhello from c2\n", result.Stdout)
283+
assert.Equal(t, "", result.Stderr)
284+
}

tools/docker/Dockerfile.integ-test

+2-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ RUN --mount=type=bind,target=/src \
4646
ln -sv /usr/local/bin/firecracker-ctr /usr/local/bin/ctr
4747
RUN containerd 2>/dev/null & \
4848
ctr --address /run/firecracker-containerd/containerd.sock content fetch docker.io/library/alpine:3.10.1 >/dev/null && \
49-
ctr --address /run/firecracker-containerd/containerd.sock content fetch docker.io/mlabbe/iperf3:3.6-r0 >/dev/null
49+
ctr --address /run/firecracker-containerd/containerd.sock content fetch docker.io/mlabbe/iperf3:3.6-r0 >/dev/null && \
50+
ctr --address /run/firecracker-containerd/containerd.sock content fetch docker.io/library/postgres:14.3 >/dev/null
5051

5152
# Install critest
5253
ENV VERSION="v1.23.0"

volume/image.go

+188
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
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 volume
15+
16+
import (
17+
"context"
18+
"encoding/json"
19+
"fmt"
20+
"os"
21+
"time"
22+
23+
"github.com/containerd/containerd"
24+
"github.com/containerd/containerd/content"
25+
"github.com/containerd/containerd/images"
26+
"github.com/containerd/containerd/mount"
27+
"github.com/containerd/containerd/snapshots"
28+
"github.com/containerd/continuity/fs"
29+
"github.com/hashicorp/go-multierror"
30+
"github.com/opencontainers/image-spec/identity"
31+
v1 "github.com/opencontainers/image-spec/specs-go/v1"
32+
)
33+
34+
type imageVolumeProvider struct {
35+
client *containerd.Client
36+
image string
37+
target string
38+
snapshotter string
39+
snapshot string
40+
}
41+
42+
// ImageOpt allows setting optional properties of the provider.
43+
type ImageOpt func(*imageVolumeProvider)
44+
45+
// WithSnapshotter sets the snapshotter to pull images.
46+
func WithSnapshotter(ss string) ImageOpt {
47+
return func(p *imageVolumeProvider) {
48+
p.snapshotter = ss
49+
}
50+
}
51+
52+
func getV1ImageConfig(ctx context.Context, image containerd.Image) (*v1.Image, error) {
53+
var result v1.Image
54+
55+
ic, err := image.Config(ctx)
56+
if err != nil {
57+
return nil, err
58+
}
59+
60+
switch ic.MediaType {
61+
case v1.MediaTypeImageConfig, images.MediaTypeDockerSchema2Config:
62+
p, err := content.ReadBlob(ctx, image.ContentStore(), ic)
63+
if err != nil {
64+
return nil, err
65+
}
66+
if err := json.Unmarshal(p, &result); err != nil {
67+
return nil, err
68+
}
69+
default:
70+
return nil, fmt.Errorf("unknown type: %s", ic.MediaType)
71+
}
72+
return &result, nil
73+
}
74+
75+
// FromImage returns a new provider to that expose the volumes on the given image.
76+
func FromImage(client *containerd.Client, image, snapshot string, opts ...ImageOpt) Provider {
77+
p := imageVolumeProvider{
78+
snapshotter: containerd.DefaultSnapshotter,
79+
client: client,
80+
image: image,
81+
snapshot: snapshot,
82+
}
83+
84+
for _, opt := range opts {
85+
opt(&p)
86+
}
87+
88+
return &p
89+
}
90+
91+
func (p *imageVolumeProvider) Name() string {
92+
return p.image
93+
}
94+
95+
func (p *imageVolumeProvider) Delete(ctx context.Context) error {
96+
var retErr error
97+
98+
if p.target != "" {
99+
retErr = mount.UnmountAll(p.target, 0)
100+
}
101+
102+
ss := p.client.SnapshotService(p.snapshotter)
103+
err := ss.Remove(ctx, p.snapshot)
104+
if err != nil {
105+
retErr = multierror.Append(retErr, err)
106+
}
107+
defer ss.Close()
108+
109+
return retErr
110+
}
111+
112+
func (p *imageVolumeProvider) CreateVolumesUnder(ctx context.Context, tempDir string) ([]*Volume, error) {
113+
image, err := p.client.GetImage(ctx, p.image)
114+
if err != nil {
115+
return nil, err
116+
}
117+
118+
unpacked, err := image.IsUnpacked(ctx, p.snapshotter)
119+
if err != nil {
120+
return nil, err
121+
}
122+
123+
if !unpacked {
124+
err := image.Unpack(ctx, p.snapshotter)
125+
if err != nil {
126+
return nil, err
127+
}
128+
}
129+
130+
config, err := getV1ImageConfig(ctx, image)
131+
if err != nil {
132+
return nil, err
133+
}
134+
135+
if len(config.Config.Volumes) == 0 {
136+
return nil, nil
137+
}
138+
139+
root, err := p.mountImage(ctx, tempDir)
140+
if err != nil {
141+
return nil, err
142+
}
143+
p.target = root
144+
145+
result := make([]*Volume, 0, len(config.Config.Volumes))
146+
for path := range config.Config.Volumes {
147+
hostPath, err := fs.RootPath(root, path)
148+
if err != nil {
149+
return nil, err
150+
}
151+
result = append(result, &Volume{hostPath: hostPath, containerPath: path})
152+
}
153+
return result, nil
154+
}
155+
156+
func (p *imageVolumeProvider) mountImage(ctx context.Context, tempDir string) (string, error) {
157+
image, err := p.client.GetImage(ctx, p.image)
158+
if err != nil {
159+
return "", err
160+
}
161+
162+
ids, err := image.RootFS(ctx)
163+
if err != nil {
164+
return "", err
165+
}
166+
167+
parent := identity.ChainID(ids).String()
168+
169+
ss := p.client.SnapshotService(p.snapshotter)
170+
mounts, err := ss.View(ctx, p.snapshot, parent, snapshots.WithLabels(map[string]string{
171+
"containerd.io/gc.root": time.Now().UTC().Format(time.RFC3339),
172+
}))
173+
if err != nil {
174+
return "", err
175+
}
176+
defer ss.Close()
177+
178+
target, err := os.MkdirTemp(tempDir, "mount")
179+
if err != nil {
180+
return "", err
181+
}
182+
err = mount.All(mounts, target)
183+
if err != nil {
184+
return "", err
185+
}
186+
187+
return target, nil
188+
}

0 commit comments

Comments
 (0)