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
59 changes: 16 additions & 43 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,16 @@ package main

import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"runtime"
"strings"

"bunny/hops"

"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/exporter/containerimage/exptypes"
"github.com/moby/buildkit/frontend/gateway/client"
"github.com/moby/buildkit/frontend/gateway/grpcclient"
"github.com/moby/buildkit/util/appcontext"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
)

const (
Expand Down Expand Up @@ -105,40 +100,6 @@ func readFileFromLLB(ctx context.Context, c client.Client, filename string) ([]b
return fileBytes, nil
}

func annotateRes(annots map[string]string, res *client.Result) (*client.Result, error) {
ref, err := res.SingleRef()
if err != nil {
return nil, fmt.Errorf("Failed te get reference build result: %v", err)
}

config := ocispecs.Image{
Platform: ocispecs.Platform{
Architecture: runtime.GOARCH,
OS: "linux",
},
RootFS: ocispecs.RootFS{
Type: "layers",
},
Config: ocispecs.ImageConfig{
WorkingDir: "/",
Cmd: strings.Fields(annots["com.urunc.unikernel.cmdline"]),
Labels: annots,
},
}

imageConfig, err := json.Marshal(config)
if err != nil {
return nil, fmt.Errorf("Failed to marshal image config: %v", err)
}
res.AddMeta(exptypes.ExporterImageConfigKey, imageConfig)
for annot, val := range annots {
res.AddMeta(exptypes.AnnotationManifestKey(nil, annot), []byte(val))
}
res.SetRef(ref)

return res, nil
}

func bunnyBuilder(ctx context.Context, c client.Client) (*client.Result, error) {
// Get the Build options from buildkit
buildOpts := c.BuildOpts().Opts
Expand Down Expand Up @@ -167,21 +128,33 @@ func bunnyBuilder(ctx context.Context, c client.Client) (*client.Result, error)
return nil, fmt.Errorf("Could not create LLB definition: %v", err)
}

rc := &hops.ResultAndConfig{}

// Pass LLB to buildkit
result, err := c.Solve(ctx, client.SolveRequest{
rc.Res, err = c.Solve(ctx, client.SolveRequest{
Definition: dt.ToPB(),
})
if err != nil {
return nil, fmt.Errorf("Failed to resolve LLB: %v", err)
}

// Add annotations and Labels in output image
result, err = annotateRes(packInst.Annots, result)
// Get the OCI Image config of the base Image if there is any
err = rc.GetBaseConfig(ctx, c, packInst.Config.BaseRef, packInst.Config.Monitor)
if err != nil {
return nil, fmt.Errorf("Failed to get OCI config of base image: %v", err)
}

// Set some default values in the Image config
// and add cmdline and Labels
rc.UpdateConfig(packInst.Annots)

// Apply annotations and the new config to the solver's result
err = rc.ApplyConfig(packInst.Annots)
if err != nil {
return nil, fmt.Errorf("Failed to annotate final image: %v", err)
}

return result, nil
return rc.Res, nil
}

func main() {
Expand Down
119 changes: 119 additions & 0 deletions hops/image_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright (c) 2023-2025, Nubificus LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package hops

import (
"context"
"encoding/json"
"fmt"
"runtime"
"strings"

"github.com/distribution/reference"
"github.com/moby/buildkit/client/llb/sourceresolver"
"github.com/moby/buildkit/exporter/containerimage/exptypes"
"github.com/moby/buildkit/frontend/gateway/client"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
)

type ResultAndConfig struct {
// The result
Res *client.Result
// The OCI config of the final image
OCIConfig ocispecs.Image
}

func (rc *ResultAndConfig) GetBaseConfig(ctx context.Context, c client.Client, ref string, mon string) error {
if ref == "" || ref == "scratch" {
return nil
}

baseRef, err := reference.ParseNormalizedNamed(ref)
if err != nil {
return fmt.Errorf("Failed to parse image name %s: %v", ref, err)
}
baseImageName := reference.TagNameOnly(baseRef).String()

plat := ocispecs.Platform{
Architecture: runtime.GOARCH,
}
if strings.HasPrefix(ref, unikraftHub) {
// Define the platform to qemu/amd64 so we can pull unikraft images
plat.OS = mon
} else {
plat.OS = "linux"
}
_, _, config, err := c.ResolveImageConfig(ctx, baseImageName,
sourceresolver.Opt{
LogName: "resolving image metadata for " + baseImageName,
Platform: &plat,
})
if err != nil {
return fmt.Errorf("Failed to get image config from %s: %v", baseImageName, err)
}

err = json.Unmarshal(config, &rc.OCIConfig)
if err != nil {
return fmt.Errorf("Failed to unmarshal image config of %ss: %v", baseImageName, err)
}

return nil
}

func (rc *ResultAndConfig) UpdateConfig(annots map[string]string) {
plat := ocispecs.Platform{
Architecture: runtime.GOARCH,
OS: "linux",
}
rfs := ocispecs.RootFS{
Type: "layers",
}

// Overwrite platform and rootfs to remove unikraft specific platform
// and initialize empty configs.
rc.OCIConfig.Platform = plat
rc.OCIConfig.RootFS = rfs
// Overwrite Cmd and entrypoint based on the values of bunnyfile
rc.OCIConfig.Config.Cmd = strings.Fields(annots["com.urunc.unikernel.cmdline"])
rc.OCIConfig.Config.Entrypoint = []string{}

if rc.OCIConfig.Config.Labels == nil {
rc.OCIConfig.Config.Labels = make(map[string]string)
}
for k, v := range annots {
rc.OCIConfig.Config.Labels[k] = v
}
}

func (rc *ResultAndConfig) ApplyConfig(annots map[string]string) error {
res := rc.Res
ref, err := res.SingleRef()
if err != nil {
return fmt.Errorf("Failed te get reference build result: %v", err)
}

imageConfig, err := json.Marshal(rc.OCIConfig)
if err != nil {
return fmt.Errorf("Failed to marshal image config: %v", err)
}
res.AddMeta(exptypes.ExporterImageConfigKey, imageConfig)
for annot, val := range annots {
res.AddMeta(exptypes.AnnotationManifestKey(nil, annot), []byte(val))
}
res.SetRef(ref)

rc.Res = res
return nil
}
26 changes: 26 additions & 0 deletions hops/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,26 @@ type PackCopies struct {
DstPath string
}

type PackConfig struct {
// The reference of the final base image
BaseRef string
// The monitor on top of the guest will execute
Monitor string
// The Entrypoint of the container image
Entrypoint []string
// The arguments of the entrypoint
Cmd string
}

type PackInstructions struct {
// The Base image to use
Base llb.State
// The files to copy inside the final image
Copies []PackCopies
// Annotations
Annots map[string]string
// Important information for the configuration of the final image
Config PackConfig
}

type PackEntry struct {
Expand Down Expand Up @@ -178,9 +191,11 @@ func (i *PackInstructions) SetBaseAndGetPaths(kEntry *PackEntry, rEntry *PackEnt
i.Copies = append(i.Copies,
makeCopy(*kEntry, DefaultKernelPath))
i.Base = llb.Scratch()
i.Config.BaseRef = ""
kernelCopy = true
default:
i.Base = kEntry.SourceState
i.Config.BaseRef = kEntry.SourceRef
}

rootfsCopy := false
Expand All @@ -196,13 +211,15 @@ func (i *PackInstructions) SetBaseAndGetPaths(kEntry *PackEntry, rEntry *PackEnt
rootfsCopy = true
} else {
i.Base = rEntry.SourceState
i.Config.BaseRef = rEntry.SourceRef
}
case "local":
i.Copies = append(i.Copies,
makeCopy(*rEntry, DefaultRootfsPath))
rootfsCopy = true
default:
i.Base = rEntry.SourceState
i.Config.BaseRef = rEntry.SourceRef
}

// There are cases where both kernel and rootfs come from an existing
Expand Down Expand Up @@ -266,6 +283,13 @@ func (i *PackInstructions) SetAnnotations(p Platform, cmd string, kernelPath str
return nil
}

// UpdateConfig fills all the information given by the user for the
// fileds in PackConfig.
func (i *PackInstructions) UpdateConfig(cmd string, p Platform) {
i.Config.Cmd = cmd
i.Config.Monitor = p.Monitor
}

// ToPack converts Hops into PackInstructions
func ToPack(h *Hops, buildContext string) (*PackInstructions, error) {
var framework Framework
Expand Down Expand Up @@ -308,6 +332,8 @@ func ToPack(h *Hops, buildContext string) (*PackInstructions, error) {
return nil, fmt.Errorf("Error setting annotations: %v", err)
}

instr.UpdateConfig(h.Cmd, h.Platform)

return instr, nil
}

Expand Down
5 changes: 5 additions & 0 deletions hops/parse_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ func ParseContainerfile(fileBytes []byte, buildContext string) (*PackInstruction

}
instr.Base = GetSourceState(BaseString, instr.Annots["com.urunc.unikernel.hypervisor"])
// TODO This check also takes place in GetSourceState, so we should merge them
if BaseString != "scratch" && BaseString != "" {
instr.Config.BaseRef = BaseString
instr.Config.Monitor = instr.Annots["com.urunc.unikernel.hypervisor"]
}

return instr, nil
}
Expand Down
Loading