Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 201 additions & 0 deletions cmd/unikraft/integration/instance_http_oci_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026, Unikraft GmbH and The Unikraft CLI Authors.
// Licensed under the BSD-3-Clause License (the "License").
// You may not use this file except in compliance with the License.

package integration

import (
"archive/tar"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"io/fs"
"maps"
"os"
"path/filepath"
"strings"
"testing"

ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

integ "unikraft.com/cli/internal/integration"
)

// httpOCIServer serves a volume mounted at /data as http+oci layouts: the node
// only fetches a layout if it is served with the layout media type.
var httpOCIServer = &integ.SharedImage{
Name: "http-oci-server-e2e",
Files: map[string]string{
"main.go": `package main

import "net/http"

func main() {
files := http.FileServer(http.Dir("/data"))
http.ListenAndServe(":8080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/vnd.oci.layout.v1+tar")
files.ServeHTTP(w, r)
}))
}
`,
"Dockerfile": `FROM golang:1.26-alpine AS build
COPY main.go /src/main.go
RUN cd /src && CGO_ENABLED=0 go build -o /server main.go

FROM scratch
COPY --from=build /server /server
`,
"Kraftfile": `spec: v0.7
name: http-oci-server-e2e
runtime: base-compat:latest
rootfs:
format: erofs
source: ./Dockerfile
cmd: ["/server"]
`,
},
}

func TestInstancesHTTPOCI(t *testing.T) {
r := runner(t, true, []string{staging, stable})
payload := integ.Busybox.Build(t, r)
server := httpOCIServer.Build(t, r)
volName := "test-" + uniq()
serverName := "test-" + uniq()
instName := "test-" + uniq()

layout := filepath.Join(t.TempDir(), "layout")
r.Run(t, []string{"unikraft", "image", "copy", payload, "oci-layout://" + layout})

repository := r.Config.Profile.Organization + "/http-oci-e2e"
content := t.TempDir()
dgst := tarLayout(t, layout, filepath.Join(content, repository))

r.Run(t, []string{
"unikraft", "volume", "create", "--output", "quiet",
"--set", "name=" + volName, "--set", "size=64", "--set", "metro=" + r.Config.MetroName,
})
r.Run(t, []string{"unikraft", "--timeout", "30s", "volume", "wait", "--until", "state==available", volName})
r.Run(t, []string{"unikraft", "volume", "import", volName, "--source", content})

r.Run(t, []string{
"unikraft", "run", "--name", serverName, "--metro", r.Config.MetroName, "--output", "quiet",
"--image", server, "-p", "80:8080/http", "-v", volName + ":/data",
})
r.Run(t, []string{"unikraft", "--timeout", "30s", "instance", "wait", "--until", "state==running", serverName})
fqdn := strings.TrimSpace(r.Run(t, []string{
"unikraft", "instance", "inspect", serverName,
"--output", "template={{ range .service.domains }}{{ .fqdn }}{{ end }}",
}))
require.NotEmpty(t, fqdn)

image := "http+oci://" + fqdn + "/" + repository + "/@" + dgst
r.Run(t, []string{
"unikraft", "run", "--name", instName, "--metro", r.Config.MetroName, "--output", "quiet",
"--image", image, "--args", "echo UNIKRAFT_HTTP_OCI_OK",
})
r.Run(t, []string{"unikraft", "--timeout", "60s", "instance", "wait", "--until", "state==stopped", instName})

out := r.Run(t, []string{"unikraft", "instance", "inspect", instName})
assert.Contains(t, out, image)
out = r.Run(t, []string{"unikraft", "instance", "logs", instName})
assert.Contains(t, out, "UNIKRAFT_HTTP_OCI_OK")

r.Run(t, []string{"unikraft", "instance", "delete", instName})
r.Run(t, []string{"unikraft", "instance", "delete", serverName})
}

// tarLayout packs the layout at src into dir, named by its digest as http+oci
// addresses it.
//
// HACK: the node's unpacker only takes an index.json that lists the manifest
// itself, and a tarball with every directory before its contents, root
// included - neither of which the CLI's layouts give it (TOOL-369).
func tarLayout(t *testing.T, src, dir string) string {
t.Helper()
flattenIndex(t, src)
require.NoError(t, os.MkdirAll(dir, 0o755))

f, err := os.CreateTemp(dir, "layout-*")
require.NoError(t, err)
defer f.Close()

h := sha256.New()
tw := tar.NewWriter(io.MultiWriter(f, h))
require.NoError(t, filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
info, err := d.Info()
if err != nil {
return err
}
hdr, err := tar.FileInfoHeader(info, "")
if err != nil {
return err
}
rel, err := filepath.Rel(src, path)
if err != nil {
return err
}
hdr.Name = "./" + filepath.ToSlash(rel)
if rel == "." {
hdr.Name = "./"
} else if d.IsDir() {
hdr.Name += "/"
}
if err := tw.WriteHeader(hdr); err != nil {
return err
}
if d.IsDir() {
return nil
}
r, err := os.Open(path)
if err != nil {
return err
}
defer r.Close()
_, err = io.Copy(tw, r)
return err
}))
require.NoError(t, tw.Close())

dgst := "sha256:" + hex.EncodeToString(h.Sum(nil))
require.NoError(t, os.Rename(f.Name(), filepath.Join(dir, "@"+dgst)))
return dgst
}

// flattenIndex lifts the manifests of a nested index up into index.json.
func flattenIndex(t *testing.T, layout string) {
t.Helper()

var index ocispec.Index
data, err := os.ReadFile(filepath.Join(layout, ocispec.ImageIndexFile))
require.NoError(t, err)
require.NoError(t, json.Unmarshal(data, &index))
require.Len(t, index.Manifests, 1)
if index.Manifests[0].MediaType != ocispec.MediaTypeImageIndex {
return
}

var nested ocispec.Index
desc := index.Manifests[0]
data, err = os.ReadFile(filepath.Join(layout, ocispec.ImageBlobsDir, desc.Digest.Algorithm().String(), desc.Digest.Encoded()))
require.NoError(t, err)
require.NoError(t, json.Unmarshal(data, &nested))

index.Manifests = nested.Manifests
for i := range index.Manifests {
if index.Manifests[i].Annotations == nil {
index.Manifests[i].Annotations = map[string]string{}
}
maps.Copy(index.Manifests[i].Annotations, desc.Annotations)
}
data, err = json.Marshal(index)
require.NoError(t, err)
require.NoError(t, os.WriteFile(filepath.Join(layout, ocispec.ImageIndexFile), data, 0o644))
}
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,12 @@ require (
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
github.com/charmbracelet/colorprofile v0.4.3
github.com/charmbracelet/x/ansi v0.11.8
github.com/containerd/containerd/v2 v2.4.0
github.com/containerd/containerd/v2 v2.4.1
github.com/containerd/continuity v0.5.0
github.com/containerd/errdefs v1.0.0
github.com/containerd/log v0.2.0
github.com/containerd/platforms v1.0.0-rc.5
github.com/cpuguy83/go-md2man/v2 v2.0.7
github.com/distribution/reference v0.6.0
github.com/docker/cli v29.8.1+incompatible
github.com/docker/go-units v0.5.0
github.com/ettle/strcase v0.2.0
Expand Down Expand Up @@ -58,7 +57,7 @@ require (
unikraft.com/x/filters v0.0.0-20260929150216-7e946e02e58b
unikraft.com/x/fingerprint v0.0.0-20260126094137-ab6e717e5679
unikraft.com/x/guesstermwidth v0.0.0-20260904140856-1944f6df62e1
unikraft.com/x/image-spec v0.0.0-20260924124354-cba1e2dc1921
unikraft.com/x/image-spec v0.0.0-20260930145451-e11e6b4df7f4
unikraft.com/x/io v0.0.0-20260917141509-02a61e7c1812
unikraft.com/x/joinerrgroup v0.0.0-20260304162956-523940cab1de
unikraft.com/x/kingkong v0.0.0-20260824095305-c69507b68d29
Expand Down Expand Up @@ -100,6 +99,7 @@ require (
github.com/containerd/typeurl/v2 v2.3.0 // indirect
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect
github.com/denisbrodbeck/machineid v1.0.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker-credential-helpers v0.9.8 // indirect
github.com/docker/go-connections v0.7.0 // indirect
github.com/felixge/httpsnoop v1.1.0 // indirect
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/q
github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/containerd/containerd/api v1.12.0 h1:kuQm82SbDrCuO4n7hf2L8zsBtZLuympyq5X/VotfX2A=
github.com/containerd/containerd/api v1.12.0/go.mod h1:EBcSzoi9Vl18cdODaXUCskf3D2NT8lsSXeZJnU5jIUc=
github.com/containerd/containerd/v2 v2.4.0 h1:mZLYWkPAgF4tfTjinClrRfvUxmxHxg1PQL7cjrrpTtk=
github.com/containerd/containerd/v2 v2.4.0/go.mod h1:gHZz+v5y8mGt9grF3ynuaa3r6bXLmghBZWTAftHAUtg=
github.com/containerd/containerd/v2 v2.4.1 h1:DUx/ZJN7cEu0WuzHClDB+68H/bqMEH5pWoEjf0ae4hc=
github.com/containerd/containerd/v2 v2.4.1/go.mod h1:vgLdtvl3prFk1d3ZVqQcsDD5Q9IKM+vAIdU54U85w+I=
github.com/containerd/continuity v0.5.0 h1:7a85HZpCSs+1Zps0Ee3DPSuAWY+0SJM1JNM51nlEVDg=
github.com/containerd/continuity v0.5.0/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
Expand Down Expand Up @@ -482,8 +482,8 @@ unikraft.com/x/fingerprint v0.0.0-20260126094137-ab6e717e5679 h1:zdvJjNkjsriS8RM
unikraft.com/x/fingerprint v0.0.0-20260126094137-ab6e717e5679/go.mod h1:FP7uOxux/W5PKqSRQsR4tyjNuLq4Cfio7mc5QVH1kW8=
unikraft.com/x/guesstermwidth v0.0.0-20260904140856-1944f6df62e1 h1:1gkowpZPI25+G+lpuNbjdcUxQZPXnAuJA1e03WAt998=
unikraft.com/x/guesstermwidth v0.0.0-20260904140856-1944f6df62e1/go.mod h1:q3EH6bLqLAJ1PqZgHlCJPpvvP6scnjJJspBMyGQtMt4=
unikraft.com/x/image-spec v0.0.0-20260924124354-cba1e2dc1921 h1:/r4nQMA0K7pq3dgtZLilMVrN7+RlijDYjuKFADYdV74=
unikraft.com/x/image-spec v0.0.0-20260924124354-cba1e2dc1921/go.mod h1:MbRzNPIIPOHJ0x/SNt/rVWHbIg+1mx8EABFXqt+LehY=
unikraft.com/x/image-spec v0.0.0-20260930145451-e11e6b4df7f4 h1:hGAgHUd8NW8wJSgUCtTzPBp1P9Xe/W9s9kquWQpvf0I=
unikraft.com/x/image-spec v0.0.0-20260930145451-e11e6b4df7f4/go.mod h1:Zn33DV6nsRwJj57I5Trch99KxGLmfcw3chTEOJIjI3Y=
unikraft.com/x/io v0.0.0-20260917141509-02a61e7c1812 h1:OZcX6u+KB5AnO7svzHLf9UYfKUDqxtknzakoW4EaTMs=
unikraft.com/x/io v0.0.0-20260917141509-02a61e7c1812/go.mod h1:KU1WcMXAREtyS+e/K0zuYZABwAAuOiu/ezTTcirAIxM=
unikraft.com/x/joinerrgroup v0.0.0-20260304162956-523940cab1de h1:cm4FnPvnahRIK0derbI+T4ds1LsD5CFeyyAvIqcOCek=
Expand Down
5 changes: 3 additions & 2 deletions internal/builder/kernel.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/containerd/platforms"
imagespec "unikraft.com/x/image-spec"
"unikraft.com/x/image-spec/schemes"

"unikraft.com/cli/internal/images"
)
Expand All @@ -21,11 +22,11 @@ func BuildKernel(ctx context.Context, opts BuildOpts) ([]*imagespec.Image, error
return nil, err
}

runtime, err := imagespec.ParseURIDefault(opts.Runtime)
runtime, err := imagespec.ParseLocationDefault(opts.Runtime)
if err != nil {
return nil, fmt.Errorf("parsing runtime reference: %w", err)
}
if runtime.Scheme != imagespec.URISchemeOCI {
if runtime.Scheme != schemes.OCI {
return nil, fmt.Errorf("unsupported runtime reference scheme: %s", runtime.Scheme)
}

Expand Down
5 changes: 3 additions & 2 deletions internal/cmd/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"unikraft.com/cli/internal/images"
"unikraft.com/cli/internal/resource"
imagespec "unikraft.com/x/image-spec"
"unikraft.com/x/image-spec/schemes"
"unikraft.com/x/kingkong"
)

Expand Down Expand Up @@ -133,7 +134,7 @@ func (c *ImageBuildCmd) Run(ctx context.Context, cfg *config.Config, partition *
if c.Output == "" {
return nil
}
output, err := imagespec.GuessURI(c.Output)
output, err := imagespec.GuessLocation(c.Output)
if err != nil {
return err
}
Expand All @@ -156,7 +157,7 @@ func (c *ImageBuildCmd) Run(ctx context.Context, cfg *config.Config, partition *
return err
}

if partition != nil && output.Scheme == imagespec.URISchemeOCI {
if partition != nil && output.Scheme == schemes.OCI {
if err := addImageToPartition(ctx, partition, output.Path); err != nil {
return fmt.Errorf("adding built image to partition: %w", err)
}
Expand Down
Loading
Loading