Skip to content

Commit 06bbb36

Browse files
committed
Add volume.FromImage
OCI images can have multiple volumes. Orchestrators such as ECS import the volumes from the images in addition to. volume.FromImage() will be used to implement such a feature. https://github.com/opencontainers/image-spec/blob/v1.0.2/config.md#properties https://docs.aws.amazon.com/AmazonECS/latest/developerguide/bind-mounts.html#bind-mount-examples Signed-off-by: Kazuyoshi Kato <[email protected]>
1 parent 369afa7 commit 06bbb36

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)