-
Notifications
You must be signed in to change notification settings - Fork 63
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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>
- Loading branch information
1 parent
d4d0537
commit 07e69e4
Showing
8 changed files
with
512 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,281 @@ | ||
// SPDX-License-Identifier: BSD-3-Clause | ||
// Copyright (c) 2022, Unikraft GmbH and The KraftKit Authors. | ||
// Licensed under the BSD-3-Clause License (the "License"). | ||
// You may not use this file except in compliance with the License. | ||
package docker | ||
|
||
import ( | ||
"archive/tar" | ||
"context" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"os" | ||
"path/filepath" | ||
|
||
"github.com/docker/docker/api/types" | ||
"github.com/google/go-containerregistry/pkg/name" | ||
"github.com/moby/moby/client" | ||
|
||
"kraftkit.sh/initrd" | ||
"kraftkit.sh/kconfig" | ||
"kraftkit.sh/oci" | ||
"kraftkit.sh/pack" | ||
"kraftkit.sh/unikraft" | ||
"kraftkit.sh/unikraft/arch" | ||
"kraftkit.sh/unikraft/plat" | ||
) | ||
|
||
type DockerImage struct { | ||
ID string | ||
ref name.Reference | ||
|
||
// Embedded attributes which represent target.Target | ||
arch arch.Architecture | ||
plat plat.Platform | ||
kconfig kconfig.KeyValueMap | ||
kernel string | ||
initrd *initrd.InitrdConfig | ||
command []string | ||
} | ||
|
||
// Type implements unikraft.Nameable | ||
func (dockerImage *DockerImage) Type() unikraft.ComponentType { | ||
return unikraft.ComponentTypeApp | ||
} | ||
|
||
// Name implements unikraft.Nameable | ||
func (dockerImage *DockerImage) Name() string { | ||
return dockerImage.ref.Context().Name() | ||
} | ||
|
||
// Version implements unikraft.Nameable | ||
func (dockerImage *DockerImage) Version() string { | ||
return dockerImage.ref.Identifier() | ||
} | ||
|
||
// Metadata implements pack.Package | ||
func (dockerImage *DockerImage) Metadata() any { | ||
return nil | ||
} | ||
|
||
// Push implements pack.Package | ||
func (dockerImage *DockerImage) Push(ctx context.Context, opts ...pack.PushOption) error { | ||
return errors.New("not implemented") | ||
} | ||
|
||
func (dockerImage *DockerImage) Pull(ctx context.Context, opts ...pack.PullOption) error { | ||
targetImageID := dockerImage.ID | ||
|
||
// Setup the pull options | ||
popts, err := pack.NewPullOptions(opts...) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// Connect to Docker client | ||
dockerClient, err := client.NewClientWithOpts(client.FromEnv) | ||
if err != nil { | ||
return err | ||
} | ||
defer dockerClient.Close() | ||
|
||
// Check if image is in local Docker storage | ||
images, err := dockerClient.ImageList(ctx, types.ImageListOptions{}) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
imageFound := false | ||
for _, img := range images { | ||
if img.ID == targetImageID { | ||
imageFound = true | ||
break | ||
} | ||
} | ||
|
||
if !imageFound { | ||
return errors.New("target image not found in local Docker storage") | ||
} | ||
|
||
if len(popts.Workdir()) > 0 { | ||
// Unpack the image to the provided working directory | ||
reader, err := dockerClient.ImageSave(context.Background(), []string{targetImageID}) | ||
if err != nil { | ||
return err | ||
} | ||
defer reader.Close() | ||
|
||
// Create a unique path to save the image tarball in the temp directory | ||
file, err := os.CreateTemp(os.TempDir(), "docker_image_*.tar") | ||
if err != nil { | ||
return err | ||
} | ||
savePath := file.Name() | ||
defer file.Close() | ||
|
||
// Copy the image data from reader to file | ||
_, err = io.Copy(file, reader) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if err := unpackTar(savePath, popts.Workdir()); err != nil { | ||
return err | ||
} | ||
|
||
if err := mergeLayersFromManifest(popts.Workdir()); err != nil { | ||
return err | ||
} | ||
|
||
// Set the kernel path | ||
dockerImage.kernel = filepath.Join(popts.Workdir(), oci.WellKnownKernelPath) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
type ImageManifest struct { | ||
Layers []string `json:"Layers"` | ||
} | ||
|
||
func mergeLayersFromManifest(workdir string) error { | ||
// Read manifest.json to get the order of layers | ||
manifestData, err := os.ReadFile(filepath.Join(workdir, "manifest.json")) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
var manifests []ImageManifest | ||
if err := json.Unmarshal(manifestData, &manifests); err != nil { | ||
return err | ||
} | ||
|
||
for _, layerPath := range manifests[0].Layers { | ||
layerTarPath := filepath.Join(workdir, layerPath) | ||
if err := unpackTar(layerTarPath, workdir); err != nil { | ||
return err | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
func unpackTar(src, dest string) error { | ||
tarFile, err := os.Open(src) | ||
if err != nil { | ||
return err | ||
} | ||
defer tarFile.Close() | ||
|
||
tr := tar.NewReader(tarFile) | ||
for { | ||
header, err := tr.Next() | ||
if err == io.EOF { | ||
break | ||
} | ||
if err != nil { | ||
return err | ||
} | ||
|
||
targetPath := filepath.Join(dest, header.Name) | ||
switch header.Typeflag { | ||
case tar.TypeDir: | ||
if err := os.MkdirAll(targetPath, header.FileInfo().Mode()); err != nil { | ||
return err | ||
} | ||
case tar.TypeReg: | ||
f, err := os.OpenFile(targetPath, os.O_CREATE|os.O_RDWR, header.FileInfo().Mode()) | ||
if err != nil { | ||
return err | ||
} | ||
if _, err := io.Copy(f, tr); err != nil { | ||
f.Close() | ||
return err | ||
} | ||
f.Close() | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
// Pull implements pack.Package | ||
func (dockerImage *DockerImage) Format() pack.PackageFormat { | ||
return DockerFormat | ||
} | ||
|
||
// Source implements unikraft.component.Component | ||
func (dockerImage *DockerImage) Source() string { | ||
return "" | ||
} | ||
|
||
// Path implements unikraft.component.Component | ||
func (dockerImage *DockerImage) Path() string { | ||
return "" | ||
} | ||
|
||
// KConfigTree implements unikraft.component.Component | ||
func (dockerImage DockerImage) KConfigTree(context.Context, ...*kconfig.KeyValue) (*kconfig.KConfigFile, error) { | ||
return nil, fmt.Errorf("not implemented: docker.dockerImage.KConfigTree") | ||
} | ||
|
||
// KConfig implements unikraft.component.Component | ||
func (dockerImage DockerImage) KConfig() kconfig.KeyValueMap { | ||
return dockerImage.kconfig | ||
} | ||
|
||
// PrintInfo implements unikraft.component.Component | ||
func (dockerImage DockerImage) PrintInfo(context.Context) string { | ||
return "not implemented: docker.dockerImage.PrintInfo" | ||
} | ||
|
||
// Architecture implements unikraft.target.Target | ||
func (dockerImage DockerImage) Architecture() arch.Architecture { | ||
return dockerImage.arch | ||
} | ||
|
||
// Platform implements unikraft.target.Target | ||
func (dockerImage DockerImage) Platform() plat.Platform { | ||
return dockerImage.plat | ||
} | ||
|
||
// Kernel implements unikraft.target.Target | ||
func (dockerImage DockerImage) Kernel() string { | ||
return dockerImage.kernel | ||
} | ||
|
||
// KernelDbg implements unikraft.target.Target | ||
func (dockerImage DockerImage) KernelDbg() string { | ||
return dockerImage.kernel | ||
} | ||
|
||
// Initrd implements unikraft.target.Target | ||
func (dockerImage DockerImage) Initrd() *initrd.InitrdConfig { | ||
return dockerImage.initrd | ||
} | ||
|
||
// Command implements unikraft.target.Target | ||
func (dockerImage DockerImage) Command() []string { | ||
if len(dockerImage.command) == 0 { | ||
return []string{"--"} | ||
} | ||
|
||
return dockerImage.command | ||
} | ||
|
||
// ConfigFilename implements unikraft.target.Target | ||
func (dockerImage DockerImage) ConfigFilename() string { | ||
return "" | ||
} | ||
|
||
// MarshalYAML implements unikraft.target.Target (yaml.Marshaler) | ||
func (dockerImage *DockerImage) MarshalYAML() (interface{}, error) { | ||
if dockerImage == nil { | ||
return nil, nil | ||
} | ||
|
||
return map[string]interface{}{ | ||
"architecture": dockerImage.arch.Name(), | ||
"platform": dockerImage.plat.Name(), | ||
}, nil | ||
} |
Oops, something went wrong.