Skip to content
Open
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
2 changes: 2 additions & 0 deletions cmd/unikraft/testdata/TestHelp/build

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions cmd/unikraft/testdata/TestHelp/images

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ require (
github.com/muesli/mango-pflag v0.1.0 // indirect
github.com/muesli/roff v0.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkg/errors v0.9.1
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
Expand Down
9 changes: 5 additions & 4 deletions internal/builder/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,11 @@ type BuildOpts struct {
Labels map[string]string

// Buildkit params
BuildArg []string
Target string
Secrets []*buildflags.Secret
SSH []*buildflags.SSH
BuildArg []string
Target string
BuildContexts map[string]string
Secrets []*buildflags.Secret
SSH []*buildflags.SSH

NoCache bool
}
Expand Down
49 changes: 49 additions & 0 deletions internal/builder/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2025, 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 builder

import (
"strings"

"github.com/distribution/reference"
"github.com/pkg/errors"
)

// ParseContextNames parses --build-context values in the form name=value.
func ParseContextNames(values []string) (map[string]string, error) {
if len(values) == 0 {
return nil, nil
}

result := make(map[string]string, len(values))
for _, value := range values {
if value == "" {
continue
}

kv := strings.SplitN(value, "=", 2)
if len(kv) != 2 {
return nil, errors.Errorf(
"invalid context value: %s, expected key=value",
value,
)
}

named, err := reference.ParseNormalizedNamed(kv[0])
if err != nil {
return nil, errors.Wrapf(err, "invalid context name %s", kv[0])
}

name := strings.TrimSuffix(
reference.FamiliarString(named),
":latest",
)
Comment on lines +35 to +43

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Buildkit also supports build-contexts that are local directories? Or git repositories? We shouldn't just support images.


result[name] = kv[1]
}

return result, nil
}
70 changes: 70 additions & 0 deletions internal/builder/context_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2025, 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 builder

import (
"testing"
)

func TestParseContextNames(t *testing.T) {
tests := []struct {
name string
values []string
want map[string]string
wantErr bool
}{
{
name: "single context",
values: []string{"foo=./foo"},
want: map[string]string{
"foo": "./foo",
},
},
{
name: "multiple contexts",
values: []string{
"foo=./foo",
"bar=../bar",
},
want: map[string]string{
"foo": "./foo",
"bar": "../bar",
},
},
{
name: "missing value",
values: []string{"foo"},
wantErr: true,
},
{
name: "empty",
values: []string{""},
want: map[string]string{},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseContextNames(tt.values)

if (err != nil) != tt.wantErr {
t.Fatalf("ParseContextNames() error = %v, wantErr %v", err, tt.wantErr)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why no use of require/assert?

}

if !tt.wantErr {
if len(got) != len(tt.want) {
t.Fatalf("got %v, want %v", got, tt.want)
}

for name, wantPath := range tt.want {
if got[name] != wantPath {
t.Errorf("context %q = %q, want %q", name, got[name], wantPath)
}
}
}
})
}
}
5 changes: 5 additions & 0 deletions internal/builder/rootfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,11 @@ func applyBuildOpts(attrs map[string]string, localDirs map[string]string, sessio
attrs["no-cache"] = ""
}

for name, path := range opts.BuildContexts {
localDirs[name] = path
attrs["context:"+name] = "local:" + name
}

for _, buildArg := range opts.BuildArg {
if buildArg == "" {
continue
Expand Down
36 changes: 36 additions & 0 deletions internal/builder/rootfs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -837,3 +837,39 @@ func runBuildRootfs(t *testing.T, opts BuildOpts) []*imagespec.Image {
})
return imgs
}

func TestRootfsDockerfileBuildContextIntegration(t *testing.T) {
ctx := rootfsIntegrationContext(t)

contextDir := t.TempDir()
require.NoError(t, os.WriteFile(
filepath.Join(contextDir, "hello.txt"),
[]byte("hello from context\n"),
0o644,
))

dockerfile := `
FROM scratch
COPY --from=shared hello.txt /hello.txt
`

rootfsPath := writeDockerfile(t, dockerfile)

imgs := runBuildRootfsIntegration(t, ctx, BuildOpts{
Rootfs: FSOpts{
Format: kraftfile.FsTypeCpio,
Path: rootfsPath,
Type: kraftfile.SourceTypeDockerfile,
},
BuildContexts: map[string]string{
"shared": contextDir,
},
Platform: []ocispec.Platform{{OS: "fc", Architecture: "x86_64"}},
})

require.Len(t, imgs, 1)

files := readCpioInitrd(t, imgs[0])
require.Contains(t, files, "./hello.txt")
require.Equal(t, "hello from context\n", files["./hello.txt"])
}
16 changes: 12 additions & 4 deletions internal/cmd/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@ type ImageBuildCmd struct {
Arch []string `help:"Only build the Kraftfile targets of these architectures. Defaults to every declared target; required when none are declared." example:"x86_64,arm64"`

// similar to docker compose build
BuildArg []string `sep:"none" help:"Set build-time variables."`
NoCache bool `help:"Do not use cache when building the image."`
Secret []string `sep:"none" help:"Secret to expose to the build (format: \"id=mysecret[,src=/local/secret]\")."`
SSH []string `sep:"none" help:"SSH agent socket or keys to expose to the build (format: \"default|<id>[=<socket>|<key>[,<key>]]\")."`
BuildArg []string `sep:"none" help:"Set build-time variables."`
NoCache bool `help:"Do not use cache when building the image."`
Secret []string `sep:"none" help:"Secret to expose to the build (format: \"id=mysecret[,src=/local/secret]\")."`
SSH []string `sep:"none" help:"SSH agent socket or keys to expose to the build (format: \"default|<id>[=<socket>|<key>[,<key>]]\")."`
BuildContext []string `sep:"none" help:"Additional build contexts (e.g., name=path)."`

Insecure []string `help:"Allow insecure (HTTP/unverified TLS) connections to registries. Specify hostnames to restrict, or omit to apply to all." type:"optional"`
}
Expand Down Expand Up @@ -115,6 +116,13 @@ func (c *ImageBuildCmd) Run(ctx context.Context, cfg *config.Config, sandbox *re
if len(c.BuildArg) > 0 {
buildOpts.BuildArg = append(buildOpts.BuildArg, c.BuildArg...)
}
if len(c.BuildContext) > 0 {
contexts, err := builder.ParseContextNames(c.BuildContext)
if err != nil {
return err
}
buildOpts.BuildContexts = contexts
}
if len(c.Secret) > 0 {
secrets, err := buildflags.ParseSecretSpecs(c.Secret)
if err != nil {
Expand Down
Loading