From 36beb7c54d6c36381a4175cd853021d4dec4bbd7 Mon Sep 17 00:00:00 2001 From: Austin Vazquez Date: Sat, 17 Aug 2024 06:44:33 +0000 Subject: [PATCH] Add builder OCI layout build context Signed-off-by: Austin Vazquez --- cmd/nerdctl/builder_build_linux_test.go | 57 ++++++++++++++++++++ docs/command-reference.md | 2 +- pkg/cmd/builder/build.go | 69 +++++++++++++++++++++++++ pkg/cmd/builder/build_test.go | 31 +++++++++++ 4 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 cmd/nerdctl/builder_build_linux_test.go diff --git a/cmd/nerdctl/builder_build_linux_test.go b/cmd/nerdctl/builder_build_linux_test.go new file mode 100644 index 00000000000..5f25d38a0cb --- /dev/null +++ b/cmd/nerdctl/builder_build_linux_test.go @@ -0,0 +1,57 @@ +/* + Copyright The containerd Authors. + + 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 main + +import ( + "fmt" + "testing" + + "github.com/containerd/nerdctl/v2/pkg/testutil" + "gotest.tools/v3/assert" +) + +func TestBuildContextWithOCILayout(t *testing.T) { + // Docker driver does not support OCI exporter. + testutil.DockerIncompatible(t) + testutil.RequiresBuild(t) + testutil.RegisterBuildCacheCleanup(t) + + base := testutil.NewBase(t) + imageName := testutil.Identifier(t) + t.Cleanup(func() { base.Cmd("rmi", imageName) }) + + dockerfile := fmt.Sprintf(`FROM %s +LABEL layer=oci-layout +CMD ["echo", "nerdctl-build-oci-layout-build-context"]`, testutil.CommonImage) + buildCtx := createBuildContext(t, dockerfile) + + tarPath := fmt.Sprintf("%s/%s", buildCtx, "test.tar") + base.Cmd("build", buildCtx, fmt.Sprintf("--output=type=oci,dest=%s", tarPath)).Run() + + ociLayoutDir := t.TempDir() + + err := extractTarFile(ociLayoutDir, tarPath) + assert.NilError(t, err) + + ociLayout := "test" + dockerfile = fmt.Sprintf(`FROM %s +CMD ["echo", "nerdctl-test-build-context-oci-layout"]`, ociLayout) + buildCtx = createBuildContext(t, dockerfile) + + base.Cmd("build", buildCtx, fmt.Sprintf("--build-context=%s=oci-layout://%s", ociLayout, ociLayoutDir), "-t", imageName).AssertOK() + base.Cmd("run", "--rm", imageName).AssertOutContains("nerdctl-test-build-context-oci-layout") +} diff --git a/docs/command-reference.md b/docs/command-reference.md index 4508b65f8c8..ba782efba9a 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -708,7 +708,7 @@ Flags: - :nerd_face: `--ipfs`: Build image with pulling base images from IPFS. See [`ipfs.md`](./ipfs.md) for details. - :whale: `--label`: Set metadata for an image - :whale: `--network=(default|host|none)`: Set the networking mode for the RUN instructions during build.(compatible with `buildctl build`) -- :whale: --build-context: Set additional contexts for build (e.g. dir2=/path/to/dir2, myorg/myapp=docker-image://path/to/myorg/myapp) +- :whale: `--build-context`: Set additional contexts for build (e.g. dir2=/path/to/dir2, myorg/myapp=docker-image://path/to/myorg/myapp) Unimplemented `docker build` flags: `--add-host`, `--squash` diff --git a/pkg/cmd/builder/build.go b/pkg/cmd/builder/build.go index 03d87848fe1..98b4f1f9e6b 100644 --- a/pkg/cmd/builder/build.go +++ b/pkg/cmd/builder/build.go @@ -36,6 +36,7 @@ import ( "github.com/containerd/errdefs" "github.com/containerd/log" "github.com/containerd/platforms" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/buildkitutil" @@ -300,6 +301,16 @@ func generateBuildctlArgs(ctx context.Context, client *containerd.Client, option continue } + if isOCILayout := strings.HasPrefix(v, "oci-layout://"); isOCILayout { + args, err := parseBuildContextFromOCILayout(k, v) + if err != nil { + return "", nil, false, "", nil, nil, err + } + + buildctlArgs = append(buildctlArgs, args...) + continue + } + path, err := filepath.Abs(v) if err != nil { return "", nil, false, "", nil, nil, err @@ -534,3 +545,61 @@ func parseContextNames(values []string) (map[string]string, error) { } return result, nil } + +var ( + errOCILayoutPrefixNotFound = errors.New("OCI layout prefix not found") + errOCILayoutEmptyDigest = errors.New("OCI layout cannot have empty digest") +) + +func parseBuildContextFromOCILayout(name, path string) ([]string, error) { + path, found := strings.CutPrefix(path, "oci-layout://") + if !found { + return []string{}, errOCILayoutPrefixNotFound + } + + abspath, err := filepath.Abs(path) + if err != nil { + return []string{}, err + } + + ociIndex, err := readOCIIndexFromPath(abspath) + if err != nil { + return []string{}, err + } + + var digest string + for _, manifest := range ociIndex.Manifests { + if manifest.MediaType == ocispec.MediaTypeImageManifest { + digest = manifest.Digest.String() + } + } + + if digest == "" { + return []string{}, errOCILayoutEmptyDigest + } + + return []string{ + fmt.Sprintf("--oci-layout=parent-image-key=%s", abspath), + fmt.Sprintf("--opt=context:%s=oci-layout:parent-image-key@%s", name, digest), + }, nil +} + +func readOCIIndexFromPath(path string) (*ocispec.Index, error) { + ociIndexJSONFile, err := os.Open(path + "/index.json") + if err != nil { + return nil, err + } + defer ociIndexJSONFile.Close() + + rawBytes, err := io.ReadAll(ociIndexJSONFile) + if err != nil { + return nil, err + } + + var ociIndex *ocispec.Index + err = json.Unmarshal(rawBytes, &ociIndex) + if err != nil { + return nil, err + } + return ociIndex, nil +} diff --git a/pkg/cmd/builder/build_test.go b/pkg/cmd/builder/build_test.go index f7fb00e539f..5e70bc2e612 100644 --- a/pkg/cmd/builder/build_test.go +++ b/pkg/cmd/builder/build_test.go @@ -187,3 +187,34 @@ func TestIsBuildPlatformDefault(t *testing.T) { }) } } + +func TestParseBuildctlArgsForOCILayout(t *testing.T) { + tests := []struct { + name string + ociLayoutName string + ociLayoutPath string + expectedArgs []string + expectedErr error + }{ + { + name: "PrefixNotFoundError", + ociLayoutName: "test", + ociLayoutPath: "/tmp/oci-layout/", + expectedArgs: []string{}, + expectedErr: errOCILayoutPrefixNotFound, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + args, err := parseBuildContextFromOCILayout(test.ociLayoutName, test.ociLayoutPath) + if test.expectedErr == nil { + assert.NilError(t, err) + } else { + assert.ErrorIs(t, err, test.expectedErr) + } + assert.Equal(t, len(args), len(test.expectedArgs)) + assert.DeepEqual(t, args, test.expectedArgs) + }) + } +}