Skip to content

Commit 36beb7c

Browse files
committed
Add builder OCI layout build context
Signed-off-by: Austin Vazquez <[email protected]>
1 parent 7dd3050 commit 36beb7c

File tree

4 files changed

+158
-1
lines changed

4 files changed

+158
-1
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package main
18+
19+
import (
20+
"fmt"
21+
"testing"
22+
23+
"github.com/containerd/nerdctl/v2/pkg/testutil"
24+
"gotest.tools/v3/assert"
25+
)
26+
27+
func TestBuildContextWithOCILayout(t *testing.T) {
28+
// Docker driver does not support OCI exporter.
29+
testutil.DockerIncompatible(t)
30+
testutil.RequiresBuild(t)
31+
testutil.RegisterBuildCacheCleanup(t)
32+
33+
base := testutil.NewBase(t)
34+
imageName := testutil.Identifier(t)
35+
t.Cleanup(func() { base.Cmd("rmi", imageName) })
36+
37+
dockerfile := fmt.Sprintf(`FROM %s
38+
LABEL layer=oci-layout
39+
CMD ["echo", "nerdctl-build-oci-layout-build-context"]`, testutil.CommonImage)
40+
buildCtx := createBuildContext(t, dockerfile)
41+
42+
tarPath := fmt.Sprintf("%s/%s", buildCtx, "test.tar")
43+
base.Cmd("build", buildCtx, fmt.Sprintf("--output=type=oci,dest=%s", tarPath)).Run()
44+
45+
ociLayoutDir := t.TempDir()
46+
47+
err := extractTarFile(ociLayoutDir, tarPath)
48+
assert.NilError(t, err)
49+
50+
ociLayout := "test"
51+
dockerfile = fmt.Sprintf(`FROM %s
52+
CMD ["echo", "nerdctl-test-build-context-oci-layout"]`, ociLayout)
53+
buildCtx = createBuildContext(t, dockerfile)
54+
55+
base.Cmd("build", buildCtx, fmt.Sprintf("--build-context=%s=oci-layout://%s", ociLayout, ociLayoutDir), "-t", imageName).AssertOK()
56+
base.Cmd("run", "--rm", imageName).AssertOutContains("nerdctl-test-build-context-oci-layout")
57+
}

docs/command-reference.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -708,7 +708,7 @@ Flags:
708708
- :nerd_face: `--ipfs`: Build image with pulling base images from IPFS. See [`ipfs.md`](./ipfs.md) for details.
709709
- :whale: `--label`: Set metadata for an image
710710
- :whale: `--network=(default|host|none)`: Set the networking mode for the RUN instructions during build.(compatible with `buildctl build`)
711-
- :whale: --build-context: Set additional contexts for build (e.g. dir2=/path/to/dir2, myorg/myapp=docker-image://path/to/myorg/myapp)
711+
- :whale: `--build-context`: Set additional contexts for build (e.g. dir2=/path/to/dir2, myorg/myapp=docker-image://path/to/myorg/myapp)
712712

713713
Unimplemented `docker build` flags: `--add-host`, `--squash`
714714

pkg/cmd/builder/build.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"github.com/containerd/errdefs"
3737
"github.com/containerd/log"
3838
"github.com/containerd/platforms"
39+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
3940

4041
"github.com/containerd/nerdctl/v2/pkg/api/types"
4142
"github.com/containerd/nerdctl/v2/pkg/buildkitutil"
@@ -300,6 +301,16 @@ func generateBuildctlArgs(ctx context.Context, client *containerd.Client, option
300301
continue
301302
}
302303

304+
if isOCILayout := strings.HasPrefix(v, "oci-layout://"); isOCILayout {
305+
args, err := parseBuildContextFromOCILayout(k, v)
306+
if err != nil {
307+
return "", nil, false, "", nil, nil, err
308+
}
309+
310+
buildctlArgs = append(buildctlArgs, args...)
311+
continue
312+
}
313+
303314
path, err := filepath.Abs(v)
304315
if err != nil {
305316
return "", nil, false, "", nil, nil, err
@@ -534,3 +545,61 @@ func parseContextNames(values []string) (map[string]string, error) {
534545
}
535546
return result, nil
536547
}
548+
549+
var (
550+
errOCILayoutPrefixNotFound = errors.New("OCI layout prefix not found")
551+
errOCILayoutEmptyDigest = errors.New("OCI layout cannot have empty digest")
552+
)
553+
554+
func parseBuildContextFromOCILayout(name, path string) ([]string, error) {
555+
path, found := strings.CutPrefix(path, "oci-layout://")
556+
if !found {
557+
return []string{}, errOCILayoutPrefixNotFound
558+
}
559+
560+
abspath, err := filepath.Abs(path)
561+
if err != nil {
562+
return []string{}, err
563+
}
564+
565+
ociIndex, err := readOCIIndexFromPath(abspath)
566+
if err != nil {
567+
return []string{}, err
568+
}
569+
570+
var digest string
571+
for _, manifest := range ociIndex.Manifests {
572+
if manifest.MediaType == ocispec.MediaTypeImageManifest {
573+
digest = manifest.Digest.String()
574+
}
575+
}
576+
577+
if digest == "" {
578+
return []string{}, errOCILayoutEmptyDigest
579+
}
580+
581+
return []string{
582+
fmt.Sprintf("--oci-layout=parent-image-key=%s", abspath),
583+
fmt.Sprintf("--opt=context:%s=oci-layout:parent-image-key@%s", name, digest),
584+
}, nil
585+
}
586+
587+
func readOCIIndexFromPath(path string) (*ocispec.Index, error) {
588+
ociIndexJSONFile, err := os.Open(path + "/index.json")
589+
if err != nil {
590+
return nil, err
591+
}
592+
defer ociIndexJSONFile.Close()
593+
594+
rawBytes, err := io.ReadAll(ociIndexJSONFile)
595+
if err != nil {
596+
return nil, err
597+
}
598+
599+
var ociIndex *ocispec.Index
600+
err = json.Unmarshal(rawBytes, &ociIndex)
601+
if err != nil {
602+
return nil, err
603+
}
604+
return ociIndex, nil
605+
}

pkg/cmd/builder/build_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,3 +187,34 @@ func TestIsBuildPlatformDefault(t *testing.T) {
187187
})
188188
}
189189
}
190+
191+
func TestParseBuildctlArgsForOCILayout(t *testing.T) {
192+
tests := []struct {
193+
name string
194+
ociLayoutName string
195+
ociLayoutPath string
196+
expectedArgs []string
197+
expectedErr error
198+
}{
199+
{
200+
name: "PrefixNotFoundError",
201+
ociLayoutName: "test",
202+
ociLayoutPath: "/tmp/oci-layout/",
203+
expectedArgs: []string{},
204+
expectedErr: errOCILayoutPrefixNotFound,
205+
},
206+
}
207+
208+
for _, test := range tests {
209+
t.Run(test.name, func(t *testing.T) {
210+
args, err := parseBuildContextFromOCILayout(test.ociLayoutName, test.ociLayoutPath)
211+
if test.expectedErr == nil {
212+
assert.NilError(t, err)
213+
} else {
214+
assert.ErrorIs(t, err, test.expectedErr)
215+
}
216+
assert.Equal(t, len(args), len(test.expectedArgs))
217+
assert.DeepEqual(t, args, test.expectedArgs)
218+
})
219+
}
220+
}

0 commit comments

Comments
 (0)