Skip to content

Commit 07e69e4

Browse files
committed
feat(run): Enable running LLB-built docker images
WIP This introduces a `dockerImage` package to run docker images containing a unikernel built with the LLB plugin. The hello world target runs successfully. A new package manager, "docker", has been added. If the target OCI image is missing, this package manager searches for it. There's an ongoing discussion about how to handle image references, especially when an image could be in both local/remote OCI storage and local docker storage. Currently, the process first checks the OCI storage and then Docker. In case of conflicts, the user might need to choose. To-Do: - Fix targets that don't build due to missing dependencies or artifact naming issues. - Provide architecture/platform info via image, as Docker doesn't support non-standard OS like qemu. - Consider support for remote docker registries. - Refactor some interfaces; we could make some of them smaller and avoid the "not implemented" errors. - Explore testing approaches for this functionality. Signed-off-by: Jakub Ciolek <jakub@ciolek.dev>
1 parent d4d0537 commit 07e69e4

File tree

8 files changed

+512
-1
lines changed

8 files changed

+512
-1
lines changed

docker/docker.go

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
// SPDX-License-Identifier: BSD-3-Clause
2+
// Copyright (c) 2022, Unikraft GmbH and The KraftKit Authors.
3+
// Licensed under the BSD-3-Clause License (the "License").
4+
// You may not use this file except in compliance with the License.
5+
package docker
6+
7+
import (
8+
"archive/tar"
9+
"context"
10+
"encoding/json"
11+
"errors"
12+
"fmt"
13+
"io"
14+
"os"
15+
"path/filepath"
16+
17+
"github.com/docker/docker/api/types"
18+
"github.com/google/go-containerregistry/pkg/name"
19+
"github.com/moby/moby/client"
20+
21+
"kraftkit.sh/initrd"
22+
"kraftkit.sh/kconfig"
23+
"kraftkit.sh/oci"
24+
"kraftkit.sh/pack"
25+
"kraftkit.sh/unikraft"
26+
"kraftkit.sh/unikraft/arch"
27+
"kraftkit.sh/unikraft/plat"
28+
)
29+
30+
type DockerImage struct {
31+
ID string
32+
ref name.Reference
33+
34+
// Embedded attributes which represent target.Target
35+
arch arch.Architecture
36+
plat plat.Platform
37+
kconfig kconfig.KeyValueMap
38+
kernel string
39+
initrd *initrd.InitrdConfig
40+
command []string
41+
}
42+
43+
// Type implements unikraft.Nameable
44+
func (dockerImage *DockerImage) Type() unikraft.ComponentType {
45+
return unikraft.ComponentTypeApp
46+
}
47+
48+
// Name implements unikraft.Nameable
49+
func (dockerImage *DockerImage) Name() string {
50+
return dockerImage.ref.Context().Name()
51+
}
52+
53+
// Version implements unikraft.Nameable
54+
func (dockerImage *DockerImage) Version() string {
55+
return dockerImage.ref.Identifier()
56+
}
57+
58+
// Metadata implements pack.Package
59+
func (dockerImage *DockerImage) Metadata() any {
60+
return nil
61+
}
62+
63+
// Push implements pack.Package
64+
func (dockerImage *DockerImage) Push(ctx context.Context, opts ...pack.PushOption) error {
65+
return errors.New("not implemented")
66+
}
67+
68+
func (dockerImage *DockerImage) Pull(ctx context.Context, opts ...pack.PullOption) error {
69+
targetImageID := dockerImage.ID
70+
71+
// Setup the pull options
72+
popts, err := pack.NewPullOptions(opts...)
73+
if err != nil {
74+
return err
75+
}
76+
77+
// Connect to Docker client
78+
dockerClient, err := client.NewClientWithOpts(client.FromEnv)
79+
if err != nil {
80+
return err
81+
}
82+
defer dockerClient.Close()
83+
84+
// Check if image is in local Docker storage
85+
images, err := dockerClient.ImageList(ctx, types.ImageListOptions{})
86+
if err != nil {
87+
return err
88+
}
89+
90+
imageFound := false
91+
for _, img := range images {
92+
if img.ID == targetImageID {
93+
imageFound = true
94+
break
95+
}
96+
}
97+
98+
if !imageFound {
99+
return errors.New("target image not found in local Docker storage")
100+
}
101+
102+
if len(popts.Workdir()) > 0 {
103+
// Unpack the image to the provided working directory
104+
reader, err := dockerClient.ImageSave(context.Background(), []string{targetImageID})
105+
if err != nil {
106+
return err
107+
}
108+
defer reader.Close()
109+
110+
// Create a unique path to save the image tarball in the temp directory
111+
file, err := os.CreateTemp(os.TempDir(), "docker_image_*.tar")
112+
if err != nil {
113+
return err
114+
}
115+
savePath := file.Name()
116+
defer file.Close()
117+
118+
// Copy the image data from reader to file
119+
_, err = io.Copy(file, reader)
120+
if err != nil {
121+
return err
122+
}
123+
124+
if err := unpackTar(savePath, popts.Workdir()); err != nil {
125+
return err
126+
}
127+
128+
if err := mergeLayersFromManifest(popts.Workdir()); err != nil {
129+
return err
130+
}
131+
132+
// Set the kernel path
133+
dockerImage.kernel = filepath.Join(popts.Workdir(), oci.WellKnownKernelPath)
134+
}
135+
136+
return nil
137+
}
138+
139+
type ImageManifest struct {
140+
Layers []string `json:"Layers"`
141+
}
142+
143+
func mergeLayersFromManifest(workdir string) error {
144+
// Read manifest.json to get the order of layers
145+
manifestData, err := os.ReadFile(filepath.Join(workdir, "manifest.json"))
146+
if err != nil {
147+
return err
148+
}
149+
150+
var manifests []ImageManifest
151+
if err := json.Unmarshal(manifestData, &manifests); err != nil {
152+
return err
153+
}
154+
155+
for _, layerPath := range manifests[0].Layers {
156+
layerTarPath := filepath.Join(workdir, layerPath)
157+
if err := unpackTar(layerTarPath, workdir); err != nil {
158+
return err
159+
}
160+
}
161+
return nil
162+
}
163+
164+
func unpackTar(src, dest string) error {
165+
tarFile, err := os.Open(src)
166+
if err != nil {
167+
return err
168+
}
169+
defer tarFile.Close()
170+
171+
tr := tar.NewReader(tarFile)
172+
for {
173+
header, err := tr.Next()
174+
if err == io.EOF {
175+
break
176+
}
177+
if err != nil {
178+
return err
179+
}
180+
181+
targetPath := filepath.Join(dest, header.Name)
182+
switch header.Typeflag {
183+
case tar.TypeDir:
184+
if err := os.MkdirAll(targetPath, header.FileInfo().Mode()); err != nil {
185+
return err
186+
}
187+
case tar.TypeReg:
188+
f, err := os.OpenFile(targetPath, os.O_CREATE|os.O_RDWR, header.FileInfo().Mode())
189+
if err != nil {
190+
return err
191+
}
192+
if _, err := io.Copy(f, tr); err != nil {
193+
f.Close()
194+
return err
195+
}
196+
f.Close()
197+
}
198+
}
199+
return nil
200+
}
201+
202+
// Pull implements pack.Package
203+
func (dockerImage *DockerImage) Format() pack.PackageFormat {
204+
return DockerFormat
205+
}
206+
207+
// Source implements unikraft.component.Component
208+
func (dockerImage *DockerImage) Source() string {
209+
return ""
210+
}
211+
212+
// Path implements unikraft.component.Component
213+
func (dockerImage *DockerImage) Path() string {
214+
return ""
215+
}
216+
217+
// KConfigTree implements unikraft.component.Component
218+
func (dockerImage DockerImage) KConfigTree(context.Context, ...*kconfig.KeyValue) (*kconfig.KConfigFile, error) {
219+
return nil, fmt.Errorf("not implemented: docker.dockerImage.KConfigTree")
220+
}
221+
222+
// KConfig implements unikraft.component.Component
223+
func (dockerImage DockerImage) KConfig() kconfig.KeyValueMap {
224+
return dockerImage.kconfig
225+
}
226+
227+
// PrintInfo implements unikraft.component.Component
228+
func (dockerImage DockerImage) PrintInfo(context.Context) string {
229+
return "not implemented: docker.dockerImage.PrintInfo"
230+
}
231+
232+
// Architecture implements unikraft.target.Target
233+
func (dockerImage DockerImage) Architecture() arch.Architecture {
234+
return dockerImage.arch
235+
}
236+
237+
// Platform implements unikraft.target.Target
238+
func (dockerImage DockerImage) Platform() plat.Platform {
239+
return dockerImage.plat
240+
}
241+
242+
// Kernel implements unikraft.target.Target
243+
func (dockerImage DockerImage) Kernel() string {
244+
return dockerImage.kernel
245+
}
246+
247+
// KernelDbg implements unikraft.target.Target
248+
func (dockerImage DockerImage) KernelDbg() string {
249+
return dockerImage.kernel
250+
}
251+
252+
// Initrd implements unikraft.target.Target
253+
func (dockerImage DockerImage) Initrd() *initrd.InitrdConfig {
254+
return dockerImage.initrd
255+
}
256+
257+
// Command implements unikraft.target.Target
258+
func (dockerImage DockerImage) Command() []string {
259+
if len(dockerImage.command) == 0 {
260+
return []string{"--"}
261+
}
262+
263+
return dockerImage.command
264+
}
265+
266+
// ConfigFilename implements unikraft.target.Target
267+
func (dockerImage DockerImage) ConfigFilename() string {
268+
return ""
269+
}
270+
271+
// MarshalYAML implements unikraft.target.Target (yaml.Marshaler)
272+
func (dockerImage *DockerImage) MarshalYAML() (interface{}, error) {
273+
if dockerImage == nil {
274+
return nil, nil
275+
}
276+
277+
return map[string]interface{}{
278+
"architecture": dockerImage.arch.Name(),
279+
"platform": dockerImage.plat.Name(),
280+
}, nil
281+
}

0 commit comments

Comments
 (0)